From 0db96d56aa914423f2cca5be2dbf7ff16d19aed6 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 12 Aug 2024 14:20:24 -0500 Subject: [PATCH 1/7] fix: err check not needed --- pkg/oci/referrers.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/oci/referrers.go b/pkg/oci/referrers.go index b74b4a8..0cb82e0 100644 --- a/pkg/oci/referrers.go +++ b/pkg/oci/referrers.go @@ -54,9 +54,6 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy return nil, fmt.Errorf("failed to get descriptor: %w", err) } subjectDigest := desc.Digest.String() - if err != nil { - return nil, fmt.Errorf("failed to get digest: %w", err) - } var referrersSubjectRef name.Digest if r.referrersRepo != "" { referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, RegistryPrefix), subjectDigest)) From 1febc55a197f5fe110883837a579f6bd4a112b6a Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 12 Aug 2024 14:49:52 -0500 Subject: [PATCH 2/7] fix: cyclical imports --- internal/test/test.go | 116 +---------- pkg/attest/README.md | 2 + pkg/attest/sign.go | 4 +- pkg/attest/sign_test.go | 27 +-- pkg/attest/verify.go | 2 +- pkg/attest/verify_test.go | 10 +- pkg/attestation/README.md | 4 + pkg/attestation/attestation.go | 180 +++++++++++++----- .../attestation/attestation_test.go | 13 +- .../example_attestation_manifest_test.go | 8 +- pkg/{oci => attestation}/layout.go | 37 ++-- pkg/{oci => attestation}/layout_test.go | 4 +- pkg/{oci => attestation}/mock.go | 10 +- pkg/{oci => attestation}/referrers.go | 29 ++- pkg/attestation/referrers_test.go | 17 +- pkg/attestation/registry.go | 101 ++++++++++ pkg/{oci => attestation}/registry_test.go | 4 +- pkg/attestation/resolver.go | 12 ++ pkg/attestation/types.go | 12 ++ pkg/mirror/README.md | 2 + pkg/oci/README.md | 2 + pkg/{mirror => oci}/authn_test.go | 13 +- pkg/oci/oci.go | 50 +---- pkg/oci/oci_test.go | 53 +++--- pkg/oci/output.go | 17 +- pkg/oci/output_test.go | 16 +- pkg/oci/registry.go | 80 -------- pkg/oci/resolver.go | 6 - pkg/oci/types.go | 47 ++++- pkg/policy/README.md | 2 + pkg/policy/evaluator.go | 4 +- pkg/policy/mock.go | 8 +- pkg/policy/policy.go | 13 +- pkg/policy/policy_test.go | 14 +- pkg/policy/rego.go | 15 +- pkg/signerverifier/README.md | 2 + pkg/tlog/README.md | 2 + pkg/tuf/README.md | 2 + scripts/README.md | 4 + test/README.md | 2 + 40 files changed, 486 insertions(+), 460 deletions(-) create mode 100644 pkg/attest/README.md create mode 100644 pkg/attestation/README.md rename internal/test/test_test.go => pkg/attestation/attestation_test.go (56%) rename pkg/{oci => attestation}/layout.go (74%) rename pkg/{oci => attestation}/layout_test.go (95%) rename pkg/{oci => attestation}/mock.go (87%) rename pkg/{oci => attestation}/referrers.go (81%) create mode 100644 pkg/attestation/registry.go rename pkg/{oci => attestation}/registry_test.go (94%) create mode 100644 pkg/attestation/resolver.go create mode 100644 pkg/mirror/README.md create mode 100644 pkg/oci/README.md rename pkg/{mirror => oci}/authn_test.go (61%) create mode 100644 pkg/policy/README.md create mode 100644 pkg/signerverifier/README.md create mode 100644 pkg/tlog/README.md create mode 100644 pkg/tuf/README.md create mode 100644 scripts/README.md create mode 100644 test/README.md diff --git a/internal/test/test.go b/internal/test/test.go index f769476..6bb22c5 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -2,22 +2,13 @@ package test import ( "context" - "encoding/base64" - "encoding/json" - "fmt" "os" "path/filepath" - "strings" "testing" - "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/signerverifier" "github.com/docker/attest/pkg/tlog" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/layout" - "github.com/google/go-containerregistry/pkg/v1/partial" - intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/secure-systems-lab/go-securesystemslib/dsse" ) @@ -30,6 +21,8 @@ const ( AWSKMSKeyARN = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox ) +var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") + func CreateTempDir(t *testing.T, dir, pattern string) string { // Create a temporary directory for output oci layout tempDir, err := os.MkdirTemp(dir, pattern) @@ -89,108 +82,3 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) { return ctx, signer } - -type AnnotatedStatement struct { - OCIDescriptor *v1.Descriptor - InTotoStatement *intoto.Statement - Annotations map[string]string -} - -func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) { - mfs2, err := idx.IndexManifest() - if err != nil { - return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err) - } - - var statements []*AnnotatedStatement - - for i := range mfs2.Manifests { - mf := &mfs2.Manifests[i] - if mf.Annotations[attestation.DockerReferenceType] != "attestation-manifest" { - continue - } - - attestationImage, err := idx.Image(mf.Digest) - if err != nil { - return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err) - } - layers, err := attestationImage.Layers() - if err != nil { - return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err) - } - - for _, layer := range layers { - // parse layer blob as json - mt, err := layer.MediaType() - if err != nil { - return nil, fmt.Errorf("failed to get layer media type: %w", err) - } - - if string(mt) != mediaType { - continue - } - r, err := layer.Uncompressed() - if err != nil { - return nil, fmt.Errorf("failed to get layer contents: %w", err) - } - defer r.Close() - inTotoStatement := new(intoto.Statement) - var desc *v1.Descriptor - if strings.HasSuffix(string(mt), "+dsse") { - env := new(attestation.Envelope) - err = json.NewDecoder(r).Decode(env) - if err != nil { - return nil, fmt.Errorf("failed to decode env: %w", err) - } - payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload) - if err != nil { - return nil, fmt.Errorf("failed to decode payload: %w", err) - } - err = json.Unmarshal([]byte(payload), inTotoStatement) - if err != nil { - return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err) - } - } else { - desc := new(v1.Descriptor) - err = json.NewDecoder(r).Decode(desc) - if err != nil { - return nil, fmt.Errorf("failed to decode statement: %w", err) - } - } - - layerDesc, err := partial.Descriptor(layer) - if err != nil { - return nil, fmt.Errorf("failed to get descriptor for layer: %w", err) - } - annotations := make(map[string]string) - for k, v := range layerDesc.Annotations { - annotations[k] = v - } - statements = append(statements, &AnnotatedStatement{ - OCIDescriptor: desc, - InTotoStatement: inTotoStatement, - Annotations: annotations, - }) - } - } - return statements, nil -} - -func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) { - idx, err := layout.ImageIndexFromPath(path) - if err != nil { - return nil, fmt.Errorf("failed to load image index: %w", err) - } - - idxm, err := idx.IndexManifest() - if err != nil { - return nil, fmt.Errorf("failed to get digest: %w", err) - } - idxDigest := idxm.Manifests[0].Digest - - mfs, err := idx.ImageIndex(idxDigest) - if err != nil { - return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err) - } - return ExtractStatementsFromIndex(mfs, mediaType) -} diff --git a/pkg/attest/README.md b/pkg/attest/README.md new file mode 100644 index 0000000..c9bff82 --- /dev/null +++ b/pkg/attest/README.md @@ -0,0 +1,2 @@ +## attest +This package implements the top-level signing and verification methods. \ No newline at end of file diff --git a/pkg/attest/sign.go b/pkg/attest/sign.go index 5e9971d..6ea2d3c 100644 --- a/pkg/attest/sign.go +++ b/pkg/attest/sign.go @@ -12,14 +12,14 @@ import ( // this is only relevant if there are (unsigned) in-toto statements. func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.Manifest, error) { // extract attestation manifests from index - attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx) + attestationManifests, err := attestation.ManifestsFromIndex(idx) if err != nil { return nil, fmt.Errorf("failed to load attestation manifests from index: %w", err) } // sign every attestation layer in each manifest for _, manifest := range attestationManifests { for _, layer := range manifest.OriginalLayers { - err = manifest.AddAttestation(ctx, signer, layer.Statement, opts) + err = manifest.Add(ctx, signer, layer.Statement, opts) if err != nil { return nil, fmt.Errorf("failed to sign attestation layer %w", err) } diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index c10265f..ac3f9df 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -23,7 +23,6 @@ import ( ) var ( - UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image") PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass") PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror") @@ -42,8 +41,8 @@ func TestSignVerifyOCILayout(t *testing.T) { expectedAttestations int replace bool }{ - {"signed replaced", UnsignedTestImage, 0, 4, true}, - {"without replace", UnsignedTestImage, 4, 4, false}, + {"signed replaced", test.UnsignedTestImage, 0, 4, true}, + {"without replace", test.UnsignedTestImage, 4, 4, false}, // image without provenance doesn't fail {"no provenance (replace)", NoProvenanceImage, 0, 2, true}, {"no provenance (no replace)", NoProvenanceImage, 2, 2, false}, @@ -70,10 +69,10 @@ func TestSignVerifyOCILayout(t *testing.T) { require.NoError(t, err) assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found") - var allEnvelopes []*test.AnnotatedStatement + var allEnvelopes []*attestation.AnnotatedStatement for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} { mt, _ := attestation.DSSEMediaType(predicate) - statements, err := test.ExtractAnnotatedStatements(outputLayout, mt) + statements, err := attestation.ExtractAnnotatedStatements(outputLayout, mt) require.NoError(t, err) allEnvelopes = append(allEnvelopes, statements...) @@ -83,7 +82,7 @@ func TestSignVerifyOCILayout(t *testing.T) { } } assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes)) - statements, err := test.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType) + statements, err := attestation.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType) require.NoError(t, err) assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements)) }) @@ -125,10 +124,10 @@ func TestAddSignedLayerAnnotations(t *testing.T) { }, SubjectDescriptor: &v1.Descriptor{}, } - err := manifest.AddAttestation(ctx, signer, originalLayer.Statement, opts) + err := manifest.Add(ctx, signer, originalLayer.Statement, opts) require.NoError(t, err) - newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace)) + newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace)) require.NoError(t, err) mf, _ := newImg.RawManifest() type Annotations struct { @@ -178,19 +177,19 @@ func TestSimpleStatementSigning(t *testing.T) { } manifest, err := NewAttestationManifest(subject) require.NoError(t, err) - err = manifest.AddAttestation(ctx, signer, statement, opts) + err = manifest.Add(ctx, signer, statement, opts) require.NoError(t, err) - err = manifest.AddAttestation(ctx, signer, statement2, opts) + err = manifest.Add(ctx, signer, statement2, opts) require.NoError(t, err) // fake that the manfifest was loaded from a real image manifest.OriginalLayers = manifest.SignedLayers - envelopes, err := oci.ExtractEnvelopes(manifest, attestation.VSAPredicateType) + envelopes, err := attestation.ExtractEnvelopes(manifest, attestation.VSAPredicateType) require.NoError(t, err) assert.Len(t, envelopes, 2) - newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace)) + newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace)) require.NoError(t, err) layers, err := newImg.Layers() require.NoError(t, err) @@ -225,7 +224,9 @@ func TestSimpleStatementSigning(t *testing.T) { indexName := fmt.Sprintf("%s/repo:root", u.Host) output, err := oci.ParseImageSpecs(indexName) require.NoError(t, err) - err = oci.SaveReferrers(manifest, output) + artifacts, err := manifest.BuildReferringArtifacts() + require.NoError(t, err) + err = oci.SaveImagesNoTag(artifacts, output) require.NoError(t, err) }) } diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index b6f5ec7..963570c 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -112,7 +112,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy. }, nil } -func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy) (*VerificationResult, error) { +func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx *policy.Policy) (*VerificationResult, error) { desc, err := resolver.ImageDescriptor(ctx) if err != nil { return nil, fmt.Errorf("failed to get image descriptor: %w", err) diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 5034e0c..abf8048 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -31,7 +31,7 @@ func TestVerifyAttestations(t *testing.T) { env := new(attestation.Envelope) err = json.Unmarshal(ex, env) assert.NoError(t, err) - resolver := &oci.MockResolver{ + resolver := &attestation.MockResolver{ Envs: []*attestation.Envelope{env}, } @@ -47,7 +47,7 @@ func TestVerifyAttestations(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockPE := policy.MockPolicyEvaluator{ - EvaluateFunc: func(_ context.Context, _ oci.AttestationResolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) { + EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) { return policy.AllowedResult(), tc.policyEvaluationError }, } @@ -72,7 +72,7 @@ func TestVSA(t *testing.T) { outputLayout := test.CreateTempDir(t, "", TestTempDir) opts := &attestation.SigningOptions{} - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) assert.NoError(t, err) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) require.NoError(t, err) @@ -122,7 +122,7 @@ func TestVerificationFailure(t *testing.T) { outputLayout := test.CreateTempDir(t, "", TestTempDir) opts := &attestation.SigningOptions{} - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) assert.NoError(t, err) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) require.NoError(t, err) @@ -185,7 +185,7 @@ func TestSignVerify(t *testing.T) { {name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true}, } - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) assert.NoError(t, err) for _, tc := range testCases { diff --git a/pkg/attestation/README.md b/pkg/attestation/README.md new file mode 100644 index 0000000..75b7e54 --- /dev/null +++ b/pkg/attestation/README.md @@ -0,0 +1,4 @@ +## attestations +This package is for components that deal with the creation, storage, and retrieval of signed attestions using OCI. + +For more generic OCI components see the `oci` package. \ No newline at end of file diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index c7050dc..4a2dc3d 100644 --- a/pkg/attestation/attestation.go +++ b/pkg/attestation/attestation.go @@ -1,14 +1,17 @@ package attestation import ( - "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "maps" + "strings" + "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/match" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/partial" @@ -18,8 +21,8 @@ import ( "github.com/secure-systems-lab/go-securesystemslib/dsse" ) -// GetAttestationManifestsFromIndex extracts all attestation manifests from an index. -func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) { +// ManifestsFromIndex extracts all attestation manifests from an index. +func ManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) { idx, err := index.IndexManifest() if err != nil { return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err) @@ -42,7 +45,7 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) if err != nil { return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", desc.Digest.String(), err) } - attestationLayers, err := GetAttestationsFromImage(attestationImage) + attestationLayers, err := layersFromImage(attestationImage) if err != nil { return nil, fmt.Errorf("failed to get attestations from image: %w", err) } @@ -57,8 +60,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) return attestationManifests, nil } -// GetAttestationsFromImage extracts all attestation layers from an image. -func GetAttestationsFromImage(image v1.Image) ([]*Layer, error) { +// LayersFromImage extracts all attestation layers from an image. +func layersFromImage(image v1.Image) ([]*Layer, error) { layers, err := image.Layers() if err != nil { return nil, fmt.Errorf("failed to extract layers from image: %w", err) @@ -94,7 +97,7 @@ func GetAttestationsFromImage(image v1.Image) ([]*Layer, error) { return attestationLayers, nil } -func (manifest *Manifest) AddAttestation(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error { +func (manifest *Manifest) Add(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error { layer, err := createSignedImageLayer(ctx, statement, signer, opts) if err != nil { return fmt.Errorf("failed to create signed layer: %w", err) @@ -105,7 +108,7 @@ func (manifest *Manifest) AddAttestation(ctx context.Context, signer dsse.Signer func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Layer, error) { // sign the statement - env, err := SignInTotoStatement(ctx, statement, signer, opts) + env, err := signInTotoStatement(ctx, statement, signer, opts) if err != nil { return nil, fmt.Errorf("failed to sign statement: %w", err) } @@ -128,7 +131,7 @@ func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, si }, nil } -func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) { +func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) { payload, err := json.Marshal(statement) if err != nil { return nil, fmt.Errorf("failed to marshal statement: %w", err) @@ -140,12 +143,12 @@ func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signe return env, nil } -func UpdateIndexImage( +func updateImageIndex( idx v1.ImageIndex, manifest *Manifest, options ...func(*ManifestImageOptions) error, ) (v1.ImageIndex, error) { - image, err := manifest.BuildAttestationImage(options...) + image, err := manifest.BuildImage(options...) if err != nil { return nil, fmt.Errorf("failed to build image: %w", err) } @@ -170,7 +173,7 @@ func UpdateIndexImage( func UpdateIndexImages(idx v1.ImageIndex, manifest []*Manifest, options ...func(*ManifestImageOptions) error) (v1.ImageIndex, error) { var err error for _, m := range manifest { - idx, err = UpdateIndexImage(idx, m, options...) + idx, err = updateImageIndex(idx, m, options...) if err != nil { return nil, fmt.Errorf("failed to add image to index: %w", err) } @@ -204,7 +207,7 @@ func WithReplacedLayers(replaceLayers bool) func(*ManifestImageOptions) error { } // build an image with signed attestations, optionally replacing existing layers with signed layers. -func (manifest *Manifest) BuildAttestationImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) { +func (manifest *Manifest) BuildImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) { opts, err := newOptions(options...) if err != nil { return nil, fmt.Errorf("failed to create options: %w", err) @@ -229,7 +232,7 @@ func (manifest *Manifest) BuildAttestationImage(options ...func(*ManifestImageOp } // so that we attach all attestations to a single attestations image - as per current buildkit opts.laxReferrers = true - newImg, err := buildImage(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts) + newImg, err := buildImageFromLayers(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts) if err != nil { return nil, fmt.Errorf("failed to build image: %w", err) } @@ -241,7 +244,7 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) { var images []v1.Image for _, layer := range manifest.SignedLayers { opts := &ManifestImageOptions{} - newImg, err := buildImage([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts) + newImg, err := buildImageFromLayers([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts) if err != nil { return nil, fmt.Errorf("failed to build image: %w", err) } @@ -250,8 +253,8 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) { return images, nil } -// build an image containing only layers. -func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) { +// build an image containing only layers provided. +func buildImageFromLayers(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) { newImg := empty.Image var err error if len(layers) == 0 { @@ -296,46 +299,135 @@ func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor } if !opts.laxReferrers { // as per https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor - newImg = &EmptyConfigImage{newImg} + newImg = &oci.EmptyConfigImage{Image: newImg} } return newImg, nil } -type EmptyConfigImage struct { - v1.Image -} - -func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) { - return []byte("{}"), nil -} - -func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) { - mf, err := i.Image.Manifest() +func ExtractEnvelopes(manifest *Manifest, predicateType string) ([]*Envelope, error) { + var envs []*Envelope + dsseMediaType, err := DSSEMediaType(predicateType) if err != nil { - return nil, fmt.Errorf("failed to get manifest: %w", err) + return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err) } - mf.Config = v1.Descriptor{ - MediaType: "application/vnd.oci.empty.v1+json", - Size: 2, - Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"}, - Data: []byte("{}"), + for _, attestationLayer := range manifest.OriginalLayers { + mt, err := attestationLayer.Layer.MediaType() + if err != nil { + return nil, fmt.Errorf("failed to get layer media type: %w", err) + } + if string(mt) == dsseMediaType { + reader, err := attestationLayer.Layer.Uncompressed() + if err != nil { + return nil, fmt.Errorf("failed to get layer contents: %w", err) + } + defer reader.Close() + env := new(Envelope) + err = json.NewDecoder(reader).Decode(&env) + if err != nil { + return nil, fmt.Errorf("failed to decode envelope: %w", err) + } + envs = append(envs, env) + } } - return mf, nil + + return envs, nil } -func (i *EmptyConfigImage) RawManifest() ([]byte, error) { - mf, err := i.Manifest() +func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) { + mfs2, err := idx.IndexManifest() if err != nil { - return nil, fmt.Errorf("failed to get manifest: %w", err) + return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err) } - return json.Marshal(mf) + + var statements []*AnnotatedStatement + + for i := range mfs2.Manifests { + mf := &mfs2.Manifests[i] + if mf.Annotations[DockerReferenceType] != "attestation-manifest" { + continue + } + + attestationImage, err := idx.Image(mf.Digest) + if err != nil { + return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err) + } + layers, err := attestationImage.Layers() + if err != nil { + return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err) + } + + for _, layer := range layers { + // parse layer blob as json + mt, err := layer.MediaType() + if err != nil { + return nil, fmt.Errorf("failed to get layer media type: %w", err) + } + + if string(mt) != mediaType { + continue + } + r, err := layer.Uncompressed() + if err != nil { + return nil, fmt.Errorf("failed to get layer contents: %w", err) + } + defer r.Close() + inTotoStatement := new(intoto.Statement) + var desc *v1.Descriptor + if strings.HasSuffix(string(mt), "+dsse") { + env := new(Envelope) + err = json.NewDecoder(r).Decode(env) + if err != nil { + return nil, fmt.Errorf("failed to decode env: %w", err) + } + payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload) + if err != nil { + return nil, fmt.Errorf("failed to decode payload: %w", err) + } + err = json.Unmarshal([]byte(payload), inTotoStatement) + if err != nil { + return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err) + } + } else { + desc := new(v1.Descriptor) + err = json.NewDecoder(r).Decode(desc) + if err != nil { + return nil, fmt.Errorf("failed to decode statement: %w", err) + } + } + + layerDesc, err := partial.Descriptor(layer) + if err != nil { + return nil, fmt.Errorf("failed to get descriptor for layer: %w", err) + } + annotations := make(map[string]string) + for k, v := range layerDesc.Annotations { + annotations[k] = v + } + statements = append(statements, &AnnotatedStatement{ + OCIDescriptor: desc, + InTotoStatement: inTotoStatement, + Annotations: annotations, + }) + } + } + return statements, nil } -func (i *EmptyConfigImage) Digest() (v1.Hash, error) { - mb, err := i.RawManifest() +func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) { + idx, err := layout.ImageIndexFromPath(path) if err != nil { - return v1.Hash{}, err + return nil, fmt.Errorf("failed to load image index: %w", err) } - digest, _, err := v1.SHA256(bytes.NewReader(mb)) - return digest, err + + idxm, err := idx.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to get digest: %w", err) + } + idxDigest := idxm.Manifests[0].Digest + + mfs, err := idx.ImageIndex(idxDigest) + if err != nil { + return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err) + } + return ExtractStatementsFromIndex(mfs, mediaType) } diff --git a/internal/test/test_test.go b/pkg/attestation/attestation_test.go similarity index 56% rename from internal/test/test_test.go rename to pkg/attestation/attestation_test.go index 8852268..32e73d8 100644 --- a/internal/test/test_test.go +++ b/pkg/attestation/attestation_test.go @@ -1,21 +1,18 @@ -package test +package attestation_test import ( - "path/filepath" "testing" + "github.com/docker/attest/internal/test" + "github.com/docker/attest/pkg/attestation" intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/stretchr/testify/assert" ) -var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") - -const ( - ExpectedStatements = 4 -) +const ExpectedStatements = 4 func TestExtractAnnotatedStatements(t *testing.T) { - statements, err := ExtractAnnotatedStatements(UnsignedTestImage, intoto.PayloadType) + statements, err := attestation.ExtractAnnotatedStatements(test.UnsignedTestImage, intoto.PayloadType) assert.NoError(t, err) assert.Equalf(t, len(statements), ExpectedStatements, "expected %d statement, got %d", ExpectedStatements, len(statements)) } diff --git a/pkg/attestation/example_attestation_manifest_test.go b/pkg/attestation/example_attestation_manifest_test.go index 040501d..4b21229 100644 --- a/pkg/attestation/example_attestation_manifest_test.go +++ b/pkg/attestation/example_attestation_manifest_test.go @@ -68,7 +68,7 @@ func ExampleManifest() { } // sign and add the attestation to the manifest - err = manifest.AddAttestation(context.Background(), signer, statement, opts) + err = manifest.Add(context.Background(), signer, statement, opts) if err != nil { panic(err) } @@ -79,7 +79,11 @@ func ExampleManifest() { } // save the manifest to the registry as a referrers artifact - err = oci.SaveReferrers(manifest, output) + artifacts, err := manifest.BuildReferringArtifacts() + if err != nil { + panic(err) + } + err = oci.SaveImagesNoTag(artifacts, output) if err != nil { panic(err) } diff --git a/pkg/oci/layout.go b/pkg/attestation/layout.go similarity index 74% rename from pkg/oci/layout.go rename to pkg/attestation/layout.go index 634be9e..971a081 100644 --- a/pkg/oci/layout.go +++ b/pkg/attestation/layout.go @@ -1,36 +1,35 @@ -package oci +package attestation import ( "context" "encoding/json" "fmt" - "github.com/docker/attest/pkg/attestation" - att "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/layout" ) -// implementation of AttestationResolver that closes over attestations from an oci layout. +// implementation of Resolver that closes over attestations from an oci layout. type LayoutResolver struct { - *attestation.Manifest - *ImageSpec + *Manifest + *oci.ImageSpec } -func NewOCILayoutAttestationResolver(src *ImageSpec) (*LayoutResolver, error) { +func NewOCILayoutResolver(src *oci.ImageSpec) (*LayoutResolver, error) { r := &LayoutResolver{ ImageSpec: src, } - _, err := r.fetchAttestationManifest() + _, err := r.fetchManifest() if err != nil { return nil, err } return r, nil } -func (r *LayoutResolver) fetchAttestationManifest() (*attestation.Manifest, error) { +func (r *LayoutResolver) fetchManifest() (*Manifest, error) { if r.Manifest == nil { - m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform) + m, err := manifestFromOCILayout(r.Identifier, r.ImageSpec.Platform) if err != nil { return nil, err } @@ -40,9 +39,9 @@ func (r *LayoutResolver) fetchAttestationManifest() (*attestation.Manifest, erro return r.Manifest, nil } -func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ([]*att.Envelope, error) { - var envs []*att.Envelope - dsseMediaType, err := attestation.DSSEMediaType(predicateType) +func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ([]*Envelope, error) { + var envs []*Envelope + dsseMediaType, err := DSSEMediaType(predicateType) if err != nil { return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err) } @@ -55,7 +54,7 @@ func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ( if mts != dsseMediaType { continue } - env := new(att.Envelope) + env := new(Envelope) // parse layer blob as json r, err := attestationLayer.Layer.Uncompressed() if err != nil { @@ -83,7 +82,7 @@ func (r *LayoutResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) return r.ImageSpec.Platform, nil } -func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*attestation.Manifest, error) { +func manifestFromOCILayout(path string, platform *v1.Platform) (*Manifest, error) { idx, err := layout.ImageIndexFromPath(path) if err != nil { return nil, err @@ -120,11 +119,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte } for i := range mfs2.Manifests { mf := &mfs2.Manifests[i] - if mf.Annotations[att.DockerReferenceType] != attestation.AttestationManifestType { + if mf.Annotations[DockerReferenceType] != AttestationManifestType { continue } - if mf.Annotations[att.DockerReferenceDigest] != subjectDescriptor.Digest.String() { + if mf.Annotations[DockerReferenceDigest] != subjectDescriptor.Digest.String() { continue } @@ -132,11 +131,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte if err != nil { return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err) } - layers, err := attestation.GetAttestationsFromImage(attestationImage) + layers, err := layersFromImage(attestationImage) if err != nil { return nil, fmt.Errorf("failed to get attestations from image: %w", err) } - attest := &attestation.Manifest{ + attest := &Manifest{ OriginalLayers: layers, OriginalDescriptor: mf, SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"], diff --git a/pkg/oci/layout_test.go b/pkg/attestation/layout_test.go similarity index 95% rename from pkg/oci/layout_test.go rename to pkg/attestation/layout_test.go index 870b091..4877029 100644 --- a/pkg/oci/layout_test.go +++ b/pkg/attestation/layout_test.go @@ -1,4 +1,4 @@ -package oci_test +package attestation_test import ( "strings" @@ -24,7 +24,7 @@ func TestAttestationFromOCILayout(t *testing.T) { } opts := &attestation.SigningOptions{} - attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) require.NoError(t, err) diff --git a/pkg/oci/mock.go b/pkg/attestation/mock.go similarity index 87% rename from pkg/oci/mock.go rename to pkg/attestation/mock.go index 537d9ae..a877e89 100644 --- a/pkg/oci/mock.go +++ b/pkg/attestation/mock.go @@ -1,17 +1,17 @@ -package oci +package attestation import ( "context" - "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" ) type MockResolver struct { - Envs []*attestation.Envelope + Envs []*Envelope } -func (r MockResolver) Attestations(_ context.Context, _ string) ([]*attestation.Envelope, error) { +func (r MockResolver) Attestations(_ context.Context, _ string) ([]*Envelope, error) { return r.Envs, nil } @@ -32,7 +32,7 @@ func (r MockResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) } func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) { - return ParsePlatform("linux/amd64") + return oci.ParsePlatform("linux/amd64") } type MockRegistryResolver struct { diff --git a/pkg/oci/referrers.go b/pkg/attestation/referrers.go similarity index 81% rename from pkg/oci/referrers.go rename to pkg/attestation/referrers.go index 0cb82e0..b46444c 100644 --- a/pkg/oci/referrers.go +++ b/pkg/attestation/referrers.go @@ -1,22 +1,21 @@ -package oci +package attestation import ( "context" "fmt" "strings" - "github.com/docker/attest/pkg/attestation" - att "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/oci" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" ) type ReferrersResolver struct { referrersRepo string - ImageDetailsResolver + oci.ImageDetailsResolver } -func NewReferrersAttestationResolver(src ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) { +func NewReferrersResolver(src oci.ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) { res := &ReferrersResolver{ ImageDetailsResolver: src, } @@ -36,8 +35,8 @@ func WithReferrersRepo(repo string) func(*ReferrersResolver) error { } } -func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*attestation.Manifest, error) { - dsseMediaType, err := attestation.DSSEMediaType(predicateType) +func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*Manifest, error) { + dsseMediaType, err := DSSEMediaType(predicateType) if err != nil { return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err) } @@ -56,14 +55,14 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy subjectDigest := desc.Digest.String() var referrersSubjectRef name.Digest if r.referrersRepo != "" { - referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, RegistryPrefix), subjectDigest)) + referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, oci.RegistryPrefix), subjectDigest)) if err != nil { return nil, fmt.Errorf("failed to create referrers reference: %w", err) } } else { referrersSubjectRef = subjectRef.Context().Digest(subjectDigest) } - options := WithOptions(ctx, nil) + options := oci.WithOptions(ctx, nil) options = append(options, remote.WithFilter("artifactType", dsseMediaType)) referrersIndex, err := remote.Referrers(referrersSubjectRef, options...) if err != nil { @@ -73,16 +72,16 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy if err != nil { return nil, fmt.Errorf("failed to get index manifest: %w", err) } - aManifests := make([]*attestation.Manifest, 0) + aManifests := make([]*Manifest, 0) for i := range referrersIndexManifest.Manifests { m := referrersIndexManifest.Manifests[i] remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String()) - options = WithOptions(ctx, nil) + options = oci.WithOptions(ctx, nil) attestationImage, err := remote.Image(remoteRef, options...) if err != nil { return nil, fmt.Errorf("failed to get referred image: %w", err) } - layers, err := attestation.GetAttestationsFromImage(attestationImage) + layers, err := layersFromImage(attestationImage) if err != nil { return nil, fmt.Errorf("failed to get attestations from image: %w", err) } @@ -96,7 +95,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy if string(mt) != dsseMediaType { return nil, fmt.Errorf("expected layer media type %s, got %s", dsseMediaType, mt) } - attest := &attestation.Manifest{ + attest := &Manifest{ SubjectName: imageName, OriginalLayers: layers, OriginalDescriptor: &m, @@ -107,12 +106,12 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy return aManifests, nil } -func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) { +func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*Envelope, error) { manifests, err := r.resolveAttestations(ctx, predicateType) if err != nil { return nil, fmt.Errorf("failed to resolve attestations: %w", err) } - var envs []*att.Envelope + var envs []*Envelope for _, attest := range manifests { es, err := ExtractEnvelopes(attest, predicateType) if err != nil { diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index 8c02c38..28cb25b 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -23,7 +23,6 @@ import ( ) var ( - UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image") PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass") LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy") @@ -94,7 +93,7 @@ func TestAttestationReferenceTypes(t *testing.T) { opts := &attestation.SigningOptions{ SkipTL: true, } - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) indexName := fmt.Sprintf("%s/repo:root", u.Host) @@ -120,7 +119,9 @@ func TestAttestationReferenceTypes(t *testing.T) { output, err := oci.ParseImageSpec(outputRepo) require.NoError(t, err) for _, attIdx := range signedManifests { - err = oci.SaveReferrers(attIdx, []*oci.ImageSpec{output}) + images, err := attIdx.BuildReferringArtifacts() + require.NoError(t, err) + err = oci.SaveImagesNoTag(images, []*oci.ImageSpec{output}) require.NoError(t, err) } @@ -213,7 +214,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { opts := &attestation.SigningOptions{ SkipTL: true, } - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) @@ -226,7 +227,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { // push signed attestation image to the ref server for _, signedManifest := range signedManifests { // push references using subject-digest.att convention - image, err := signedManifest.BuildAttestationImage() + image, err := signedManifest.BuildImage() require.NoError(t, err) err = oci.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName)) require.NoError(t, err) @@ -239,7 +240,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { opts := &attestation.SigningOptions{ SkipTL: true, } - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) @@ -294,7 +295,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) { opts := &attestation.SigningOptions{ SkipTL: true, } - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) @@ -330,7 +331,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) { func TestEmptyConfigImageDigest(t *testing.T) { empty := empty.Image - img := attestation.EmptyConfigImage{empty} + img := oci.EmptyConfigImage{Image: empty} mf, err := img.RawManifest() require.NoError(t, err) hash := util.SHA256Hex(mf) diff --git a/pkg/attestation/registry.go b/pkg/attestation/registry.go new file mode 100644 index 0000000..ad71ba0 --- /dev/null +++ b/pkg/attestation/registry.go @@ -0,0 +1,101 @@ +package attestation + +import ( + "context" + "fmt" + + "github.com/docker/attest/pkg/oci" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +type RegistryResolver struct { + *oci.RegistryImageDetailsResolver + *Manifest +} + +func NewRegistryResolver(src *oci.RegistryImageDetailsResolver) (*RegistryResolver, error) { + return &RegistryResolver{ + RegistryImageDetailsResolver: src, + }, nil +} + +func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*Envelope, error) { + if r.Manifest == nil { + attest, err := FetchManifest(ctx, r.Identifier, r.ImageSpec.Platform) + if err != nil { + return nil, err + } + r.Manifest = attest + } + return ExtractEnvelopes(r.Manifest, predicateType) +} + +func attestationDigestForImage(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) { + for i := range ix.Manifests { + m := &ix.Manifests[i] + if v, ok := m.Annotations[DockerReferenceType]; ok && v == attestType { + if d, ok := m.Annotations[DockerReferenceDigest]; ok && d == imageDigest { + return m.Digest.String(), nil + } + } + } + return "", fmt.Errorf("no attestation found for image %s", imageDigest) +} + +func FetchManifest(ctx context.Context, image string, platform *v1.Platform) (*Manifest, error) { + // we want to get to the image index, so ignoring platform for now + options := oci.WithOptions(ctx, nil) + ref, err := name.ParseReference(image) + if err != nil { + return nil, fmt.Errorf("failed to parse reference: %w", err) + } + index, err := remote.Index(ref, options...) + if err != nil { + return nil, fmt.Errorf("failed to get index: %w", err) + } + indexManifest, err := index.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to get index manifest: %w", err) + } + subjectDescriptor, err := oci.ImageDescriptor(indexManifest, platform) + if err != nil { + return nil, fmt.Errorf("failed to obtain image for platform: %w", err) + } + + digest := subjectDescriptor.Digest.String() + ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest)) + if err != nil { + return nil, fmt.Errorf("failed to parse attestation reference: %w", err) + } + + attestationDigest, err := attestationDigestForImage(indexManifest, digest, "attestation-manifest") + if err != nil { + return nil, fmt.Errorf("failed to obtain attestation for image: %w", err) + } + ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest)) + if err != nil { + return nil, fmt.Errorf("failed to parse attestation reference: %w", err) + } + remoteDescriptor, err := remote.Get(ref, options...) + if err != nil { + return nil, fmt.Errorf("failed to get attestation: %w", err) + } + attestationImage, err := remoteDescriptor.Image() + if err != nil { + return nil, fmt.Errorf("failed to get attestation image: %w", err) + } + + layers, err := layersFromImage(attestationImage) + if err != nil { + return nil, fmt.Errorf("failed to get attestations from image: %w", err) + } + attest := &Manifest{ + OriginalLayers: layers, + OriginalDescriptor: &remoteDescriptor.Descriptor, + SubjectName: image, + SubjectDescriptor: subjectDescriptor, + } + return attest, nil +} diff --git a/pkg/oci/registry_test.go b/pkg/attestation/registry_test.go similarity index 94% rename from pkg/oci/registry_test.go rename to pkg/attestation/registry_test.go index 4de65bc..9347faa 100644 --- a/pkg/oci/registry_test.go +++ b/pkg/attestation/registry_test.go @@ -1,4 +1,4 @@ -package oci_test +package attestation_test import ( "fmt" @@ -25,7 +25,7 @@ func TestRegistry(t *testing.T) { require.NoError(t, err) opts := &attestation.SigningOptions{} - attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) require.NoError(t, err) diff --git a/pkg/attestation/resolver.go b/pkg/attestation/resolver.go new file mode 100644 index 0000000..d349719 --- /dev/null +++ b/pkg/attestation/resolver.go @@ -0,0 +1,12 @@ +package attestation + +import ( + "context" + + "github.com/docker/attest/pkg/oci" +) + +type Resolver interface { + oci.ImageDetailsResolver + Attestations(ctx context.Context, mediaType string) ([]*Envelope, error) +} diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index 3305b24..eeb118c 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -64,6 +64,12 @@ type Extension struct { Ext *DockerDSSEExtension `json:"ext"` } +type AnnotatedStatement struct { + OCIDescriptor *v1.Descriptor + InTotoStatement *intoto.Statement + Annotations map[string]string +} + type DockerDSSEExtension struct { TL *DockerTLExtension `json:"tl"` } @@ -83,6 +89,12 @@ type SigningOptions struct { SkipTL bool } +type Options struct { + NoReferrers bool + Attach bool + ReferrersRepo string +} + func DSSEMediaType(predicateType string) (string, error) { var predicateName string switch predicateType { diff --git a/pkg/mirror/README.md b/pkg/mirror/README.md new file mode 100644 index 0000000..a6a94b4 --- /dev/null +++ b/pkg/mirror/README.md @@ -0,0 +1,2 @@ +## mirror +This package contains components to mirror TUF metadata and targets to OCI. \ No newline at end of file diff --git a/pkg/oci/README.md b/pkg/oci/README.md new file mode 100644 index 0000000..aa8c500 --- /dev/null +++ b/pkg/oci/README.md @@ -0,0 +1,2 @@ +## oci +This package is for generic OCI components. For attestation specific components see the `attestation` package. \ No newline at end of file diff --git a/pkg/mirror/authn_test.go b/pkg/oci/authn_test.go similarity index 61% rename from pkg/mirror/authn_test.go rename to pkg/oci/authn_test.go index 03e6c30..e40a488 100644 --- a/pkg/mirror/authn_test.go +++ b/pkg/oci/authn_test.go @@ -1,19 +1,16 @@ //go:build e2e -package mirror_test +package oci import ( - "path/filepath" "testing" - "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/internal/test" "github.com/stretchr/testify/require" ) func TestRegistryAuth(t *testing.T) { - UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") - - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) // test cases for ecr, gcr and dockerhub testCases := []struct { @@ -24,9 +21,9 @@ func TestRegistryAuth(t *testing.T) { } for _, tc := range testCases { t.Run(tc.Image, func(t *testing.T) { - err := oci.PushIndexToRegistry(attIdx.Index, tc.Image) + err := PushIndexToRegistry(attIdx.Index, tc.Image) require.NoError(t, err) - _, err = oci.IndexFromRemote(tc.Image) + _, err = IndexFromRemote(tc.Image) require.NoError(t, err) }) } diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index f3a5961..f5cc573 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -2,14 +2,11 @@ package oci import ( "context" - "encoding/json" "fmt" "strings" "github.com/containerd/platforms" "github.com/distribution/reference" - "github.com/docker/attest/pkg/attestation" - att "github.com/docker/attest/pkg/attestation" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" @@ -45,36 +42,7 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option { return options } -func ExtractEnvelopes(manifest *attestation.Manifest, predicateType string) ([]*att.Envelope, error) { - var envs []*att.Envelope - dsseMediaType, err := attestation.DSSEMediaType(predicateType) - if err != nil { - return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err) - } - for _, attestationLayer := range manifest.OriginalLayers { - mt, err := attestationLayer.Layer.MediaType() - if err != nil { - return nil, fmt.Errorf("failed to get layer media type: %w", err) - } - if string(mt) == dsseMediaType { - reader, err := attestationLayer.Layer.Uncompressed() - if err != nil { - return nil, fmt.Errorf("failed to get layer contents: %w", err) - } - defer reader.Close() - env := new(att.Envelope) - err = json.NewDecoder(reader).Decode(&env) - if err != nil { - return nil, fmt.Errorf("failed to decode envelope: %w", err) - } - envs = append(envs, env) - } - } - - return envs, nil -} - -func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) { +func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) { for i := range ix.Manifests { m := &ix.Manifests[i] if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) { @@ -84,18 +52,6 @@ func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descripto return nil, fmt.Errorf("no image found for platform %v", platform) } -func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) { - for i := range ix.Manifests { - m := &ix.Manifests[i] - if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType { - if d, ok := m.Annotations[att.DockerReferenceDigest]; ok && d == imageDigest { - return m.Digest.String(), nil - } - } - } - return "", fmt.Errorf("no attestation found for image %s", imageDigest) -} - func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) { var isCanonical bool named, err := reference.ParseNormalizedNamed(ref) @@ -150,7 +106,7 @@ func SplitDigest(digest string) (common.DigestSet, error) { } func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) { - newName, err := replaceTag(src.Identifier, digest) + newName, err := ReplaceTag(src.Identifier, digest) if err != nil { return nil, fmt.Errorf("failed to parse repo name: %w", err) } @@ -162,7 +118,7 @@ func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) { } // so that the index tag is replaced with a tag unique to the image digest and doesn't overwrite it. -func replaceTag(image string, digest v1.Hash) (string, error) { +func ReplaceTag(image string, digest v1.Hash) (string, error) { if strings.HasPrefix(image, LocalPrefix) { return image, nil } diff --git a/pkg/oci/oci_test.go b/pkg/oci/oci_test.go index d28574a..03937d0 100644 --- a/pkg/oci/oci_test.go +++ b/pkg/oci/oci_test.go @@ -1,56 +1,55 @@ -package oci +package oci_test import ( - "path/filepath" "testing" + "github.com/docker/attest/internal/test" + "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") - func TestRefToPurl(t *testing.T) { - arm, err := ParsePlatform("arm64/linux") + arm, err := oci.ParsePlatform("arm64/linux") require.NoError(t, err) - purl, canonical, err := RefToPURL("alpine", arm) + purl, canonical, err := oci.RefToPURL("alpine", arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl) assert.False(t, canonical) - purl, canonical, err = RefToPURL("alpine:123", arm) + purl, canonical, err = oci.RefToPURL("alpine:123", arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - purl, canonical, err = RefToPURL("google/alpine:123", arm) + purl, canonical, err = oci.RefToPURL("google/alpine:123", arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - purl, canonical, err = RefToPURL("library/alpine:123", arm) + purl, canonical, err = oci.RefToPURL("library/alpine:123", arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - purl, canonical, err = RefToPURL("docker.io/library/alpine:123", arm) + purl, canonical, err = oci.RefToPURL("docker.io/library/alpine:123", arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - purl, canonical, err = RefToPURL("localhost:5001/library/alpine:123", arm) + purl, canonical, err = oci.RefToPURL("localhost:5001/library/alpine:123", arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - purl, canonical, err = RefToPURL("localhost:5001/alpine:123", arm) + purl, canonical, err = oci.RefToPURL("localhost:5001/alpine:123", arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - purl, canonical, err = RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm) + purl, canonical, err = oci.RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl) assert.True(t, canonical) @@ -58,7 +57,7 @@ func TestRefToPurl(t *testing.T) { // Test fix for https://github.com/docker/secure-artifacts-team-issues/issues/202 func TestImageDigestForPlatform(t *testing.T) { - idx, err := layout.ImageIndexFromPath(UnsignedTestImage) + idx, err := layout.ImageIndexFromPath(test.UnsignedTestImage) assert.NoError(t, err) idxm, err := idx.IndexManifest() @@ -72,16 +71,16 @@ func TestImageDigestForPlatform(t *testing.T) { mfs2, err := mfs.IndexManifest() assert.NoError(t, err) - p, err := ParsePlatform("linux/amd64") + p, err := oci.ParsePlatform("linux/amd64") assert.NoError(t, err) - desc, err := imageDescriptor(mfs2, p) + desc, err := oci.ImageDescriptor(mfs2, p) assert.NoError(t, err) digest := desc.Digest.String() assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest) - p, err = ParsePlatform("linux/arm64") + p, err = oci.ParsePlatform("linux/arm64") assert.NoError(t, err) - desc, err = imageDescriptor(mfs2, p) + desc, err = oci.ImageDescriptor(mfs2, p) assert.NoError(t, err) digest = desc.Digest.String() assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest) @@ -95,14 +94,14 @@ func TestWithoutTag(t *testing.T) { {name: "image:tag", expected: "index.docker.io/library/image"}, {name: "image", expected: "index.docker.io/library/image"}, {name: "image:sha256-digest.att", expected: "index.docker.io/library/image"}, - {name: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image"}, + {name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image"}, {name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"}, - {name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image"}, - {name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo"}, + {name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image"}, + {name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo"}, } for _, c := range tc { t.Run(c.name, func(t *testing.T) { - notag, _ := WithoutTag(c.name) + notag, _ := oci.WithoutTag(c.name) assert.Equal(t, c.expected, notag) }) } @@ -116,11 +115,11 @@ func TestReplaceTag(t *testing.T) { {name: "image:tag", expected: "index.docker.io/library/image:sha256-digest.att"}, {name: "image", expected: "index.docker.io/library/image:sha256-digest.att"}, {name: "image:sha256-digest.att", expected: "index.docker.io/library/image:sha256-digest.att"}, - {name: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"}, + {name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"}, {name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"}, - {name: LocalPrefix + "foobar", expected: LocalPrefix + "foobar"}, - {name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"}, - {name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"}, + {name: oci.LocalPrefix + "foobar", expected: oci.LocalPrefix + "foobar"}, + {name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"}, + {name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"}, } digest := v1.Hash{ @@ -129,7 +128,7 @@ func TestReplaceTag(t *testing.T) { } for _, c := range tc { t.Run(c.name, func(t *testing.T) { - replaced, err := replaceTag(c.name, digest) + replaced, err := oci.ReplaceTag(c.name, digest) require.NoError(t, err) assert.Equal(t, c.expected, replaced) }) diff --git a/pkg/oci/output.go b/pkg/oci/output.go index af358c1..bf5c6de 100644 --- a/pkg/oci/output.go +++ b/pkg/oci/output.go @@ -4,7 +4,6 @@ import ( "fmt" "os" - "github.com/docker/attest/pkg/attestation" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" @@ -113,26 +112,22 @@ func SaveImage(output *ImageSpec, image v1.Image, imageName string) error { return nil } -func SaveReferrers(manifest *attestation.Manifest, outputs []*ImageSpec) error { +func SaveImagesNoTag(images []v1.Image, outputs []*ImageSpec) error { for _, output := range outputs { - // OCI layout output for referrers not supported + // OCI layout output not supported if output.Type == OCI { continue } - images, err := manifest.BuildReferringArtifacts() - if err != nil { - return fmt.Errorf("failed to build image: %w", err) - } for _, image := range images { digest, err := image.Digest() if err != nil { - return fmt.Errorf("failed to get attestation image digest: %w", err) + return fmt.Errorf("failed to get image digest: %w", err) } - attOut, err := ReplaceDigestInSpec(output, digest) + spec, err := ReplaceDigestInSpec(output, digest) if err != nil { - return fmt.Errorf("failed to create attestation image spec: %w", err) + return fmt.Errorf("failed to create image spec: %w", err) } - err = PushImageToRegistry(image, attOut.Identifier) + err = PushImageToRegistry(image, spec.Identifier) if err != nil { return fmt.Errorf("failed to push image: %w", err) } diff --git a/pkg/oci/output_test.go b/pkg/oci/output_test.go index fc30a65..53784c6 100644 --- a/pkg/oci/output_test.go +++ b/pkg/oci/output_test.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http/httptest" "net/url" - "path/filepath" "testing" "github.com/docker/attest/internal/test" @@ -19,9 +18,8 @@ import ( ) func TestSavingIndex(t *testing.T) { - UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") outputLayout := test.CreateTempDir(t, "", "mirror-test") - attIdx, err := oci.IndexFromPath(UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) server := httptest.NewServer(registry.New()) @@ -82,7 +80,7 @@ func TestSavingReferrers(t *testing.T) { } manifest, err := attest.NewAttestationManifest(subject) require.NoError(t, err) - err = manifest.AddAttestation(ctx, signer, statement, opts) + err = manifest.Add(ctx, signer, statement, opts) require.NoError(t, err) server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true))) defer server.Close() @@ -93,16 +91,18 @@ func TestSavingReferrers(t *testing.T) { indexName := fmt.Sprintf("%s/repo:root", u.Host) output, err := oci.ParseImageSpecs(indexName) require.NoError(t, err) - err = oci.SaveReferrers(manifest, output) + artifacts, err := manifest.BuildReferringArtifacts() + require.NoError(t, err) + err = oci.SaveImagesNoTag(artifacts, output) require.NoError(t, err) - reg := &oci.MockRegistryResolver{ + reg := &attestation.MockRegistryResolver{ Subject: subject, - MockResolver: &oci.MockResolver{}, + MockResolver: &attestation.MockResolver{}, ImageNameStr: indexName, } require.NoError(t, err) - refResolver, err := oci.NewReferrersAttestationResolver(reg) + refResolver, err := attestation.NewReferrersResolver(reg) require.NoError(t, err) attestations, err := refResolver.Attestations(ctx, attestation.VSAPredicateType) require.NoError(t, err) diff --git a/pkg/oci/registry.go b/pkg/oci/registry.go index a36f4fd..c2d19aa 100644 --- a/pkg/oci/registry.go +++ b/pkg/oci/registry.go @@ -4,18 +4,11 @@ import ( "context" "fmt" - "github.com/docker/attest/pkg/attestation" - att "github.com/docker/attest/pkg/attestation" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" ) -type RegistryResolver struct { - *RegistryImageDetailsResolver - *attestation.Manifest -} - type RegistryImageDetailsResolver struct { *ImageSpec descriptor *v1.Descriptor @@ -27,12 +20,6 @@ func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResol }, nil } -func NewRegistryAttestationResolver(src *RegistryImageDetailsResolver) (*RegistryResolver, error) { - return &RegistryResolver{ - RegistryImageDetailsResolver: src, - }, nil -} - func (r *RegistryImageDetailsResolver) ImageName(_ context.Context) (string, error) { return r.Identifier, nil } @@ -72,70 +59,3 @@ func (r *RegistryImageDetailsResolver) ImageDescriptor(ctx context.Context) (*v1 } return r.descriptor, nil } - -func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) { - if r.Manifest == nil { - attest, err := FetchAttestationManifest(ctx, r.Identifier, r.ImageSpec.Platform) - if err != nil { - return nil, err - } - r.Manifest = attest - } - return ExtractEnvelopes(r.Manifest, predicateType) -} - -func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*attestation.Manifest, error) { - // we want to get to the image index, so ignoring platform for now - options := WithOptions(ctx, nil) - ref, err := name.ParseReference(image) - if err != nil { - return nil, fmt.Errorf("failed to parse reference: %w", err) - } - index, err := remote.Index(ref, options...) - if err != nil { - return nil, fmt.Errorf("failed to get index: %w", err) - } - indexManifest, err := index.IndexManifest() - if err != nil { - return nil, fmt.Errorf("failed to get index manifest: %w", err) - } - subjectDescriptor, err := imageDescriptor(indexManifest, platform) - if err != nil { - return nil, fmt.Errorf("failed to obtain image for platform: %w", err) - } - - digest := subjectDescriptor.Digest.String() - ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest)) - if err != nil { - return nil, fmt.Errorf("failed to parse attestation reference: %w", err) - } - - attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest") - if err != nil { - return nil, fmt.Errorf("failed to obtain attestation for image: %w", err) - } - ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest)) - if err != nil { - return nil, fmt.Errorf("failed to parse attestation reference: %w", err) - } - remoteDescriptor, err := remote.Get(ref, options...) - if err != nil { - return nil, fmt.Errorf("failed to get attestation: %w", err) - } - attestationImage, err := remoteDescriptor.Image() - if err != nil { - return nil, fmt.Errorf("failed to get attestation image: %w", err) - } - - layers, err := attestation.GetAttestationsFromImage(attestationImage) - if err != nil { - return nil, fmt.Errorf("failed to get attestations from image: %w", err) - } - attest := &attestation.Manifest{ - OriginalLayers: layers, - OriginalDescriptor: &remoteDescriptor.Descriptor, - SubjectName: image, - SubjectDescriptor: subjectDescriptor, - } - return attest, nil -} diff --git a/pkg/oci/resolver.go b/pkg/oci/resolver.go index 2c653f9..a5aaae5 100644 --- a/pkg/oci/resolver.go +++ b/pkg/oci/resolver.go @@ -3,15 +3,9 @@ package oci import ( "context" - att "github.com/docker/attest/pkg/attestation" v1 "github.com/google/go-containerregistry/pkg/v1" ) -type AttestationResolver interface { - ImageDetailsResolver - Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error) -} - type ImageDetailsResolver interface { ImageName(ctx context.Context) (string, error) ImagePlatform(ctx context.Context) (*v1.Platform, error) diff --git a/pkg/oci/types.go b/pkg/oci/types.go index 689bf35..edb5c3e 100644 --- a/pkg/oci/types.go +++ b/pkg/oci/types.go @@ -1,6 +1,8 @@ package oci import ( + "bytes" + "encoding/json" "fmt" "strings" @@ -26,12 +28,6 @@ type ( } ) -type AttestationOptions struct { - NoReferrers bool - Attach bool - ReferrersRepo string -} - type ImageSpecOption func(*ImageSpec) error type ImageSpec struct { @@ -180,3 +176,42 @@ func WithoutTag(image string) (string, error) { repo := ref.Context().Name() return prefix + repo, nil } + +type EmptyConfigImage struct { + v1.Image +} + +func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) { + return []byte("{}"), nil +} + +func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) { + mf, err := i.Image.Manifest() + if err != nil { + return nil, fmt.Errorf("failed to get manifest: %w", err) + } + mf.Config = v1.Descriptor{ + MediaType: "application/vnd.oci.empty.v1+json", + Size: 2, + Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"}, + Data: []byte("{}"), + } + return mf, nil +} + +func (i *EmptyConfigImage) RawManifest() ([]byte, error) { + mf, err := i.Manifest() + if err != nil { + return nil, fmt.Errorf("failed to get manifest: %w", err) + } + return json.Marshal(mf) +} + +func (i *EmptyConfigImage) Digest() (v1.Hash, error) { + mb, err := i.RawManifest() + if err != nil { + return v1.Hash{}, err + } + digest, _, err := v1.SHA256(bytes.NewReader(mb)) + return digest, err +} diff --git a/pkg/policy/README.md b/pkg/policy/README.md new file mode 100644 index 0000000..ffb914f --- /dev/null +++ b/pkg/policy/README.md @@ -0,0 +1,2 @@ +## policy +This package is for attestation policy mapping and evaluation. \ No newline at end of file diff --git a/pkg/policy/evaluator.go b/pkg/policy/evaluator.go index 87492da..efefefd 100644 --- a/pkg/policy/evaluator.go +++ b/pkg/policy/evaluator.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/attestation" ) type policyEvaluatorCtxKeyType struct{} @@ -26,5 +26,5 @@ func GetPolicyEvaluator(ctx context.Context) (Evaluator, error) { } type Evaluator interface { - Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) + Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) } diff --git a/pkg/policy/mock.go b/pkg/policy/mock.go index e6e724b..0f9f0b3 100644 --- a/pkg/policy/mock.go +++ b/pkg/policy/mock.go @@ -3,14 +3,14 @@ package policy import ( "context" - "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/attestation" ) type MockPolicyEvaluator struct { - EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) + EvaluateFunc func(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) } -func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) { +func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) { if pe.EvaluateFunc != nil { return pe.EvaluateFunc(ctx, resolver, pctx, input) } @@ -19,7 +19,7 @@ func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.Attest func GetMockPolicy() Evaluator { return &MockPolicyEvaluator{ - EvaluateFunc: func(_ context.Context, _ oci.AttestationResolver, _ *Policy, _ *Input) (*Result, error) { + EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *Policy, _ *Input) (*Result, error) { return AllowedResult(), nil }, } diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 0582192..cfab255 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/distribution/reference" + "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/config" "github.com/docker/attest/pkg/oci" ) @@ -211,28 +212,28 @@ func normalizeImageName(imageName string) (string, error) { func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) { switch imageSource.Type { case oci.OCI: - return oci.NewOCILayoutAttestationResolver(imageSource) + return attestation.NewOCILayoutResolver(imageSource) case oci.Docker: return oci.NewRegistryImageDetailsResolver(imageSource) } return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type) } -func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) { +func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (attestation.Resolver, error) { if mapping.Attestations != nil { if mapping.Attestations.Style == config.AttestationStyleAttached { switch resolver := resolver.(type) { case *oci.RegistryImageDetailsResolver: - return oci.NewRegistryAttestationResolver(resolver) - case *oci.LayoutResolver: + return attestation.NewRegistryResolver(resolver) + case *attestation.LayoutResolver: return resolver, nil default: return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver) } } if mapping.Attestations.Repo != "" { - return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) + return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(mapping.Attestations.Repo)) } } - return oci.NewReferrersAttestationResolver(resolver) + return attestation.NewReferrersResolver(resolver) } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 65f8a60..f185f7e 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -38,7 +38,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { re := policy.NewRegoEvaluator(true) - defaultResolver := oci.MockResolver{ + defaultResolver := attestation.MockResolver{ Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)}, } @@ -46,7 +46,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { repo string expectSuccess bool isCanonical bool - resolver oci.AttestationResolver + resolver attestation.Resolver policy *policy.Options policyID string errorStr string @@ -117,10 +117,10 @@ func TestLoadingMappings(t *testing.T) { } func TestCreateAttestationResolver(t *testing.T) { - mockResolver := oci.MockResolver{ + mockResolver := attestation.MockResolver{ Envs: []*attestation.Envelope{}, } - layoutResolver := &oci.LayoutResolver{} + layoutResolver := &attestation.LayoutResolver{} registryResolver := &oci.RegistryImageDetailsResolver{} nilRepoReferrers := &config.PolicyMapping{ @@ -166,11 +166,11 @@ func TestCreateAttestationResolver(t *testing.T) { return } switch resolver.(type) { - case *oci.ReferrersResolver: + case *attestation.ReferrersResolver: assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers) - case *oci.RegistryResolver: + case *attestation.RegistryResolver: assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached) - case *oci.LayoutResolver: + case *attestation.LayoutResolver: assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached) } }) diff --git a/pkg/policy/rego.go b/pkg/policy/rego.go index 5dec1c3..a99fe10 100644 --- a/pkg/policy/rego.go +++ b/pkg/policy/rego.go @@ -7,8 +7,7 @@ import ( "os" "path/filepath" - att "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/attestation" intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/rego" @@ -36,7 +35,7 @@ func NewRegoEvaluator(debug bool) Evaluator { } } -func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) { +func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) { var regoOpts []func(*rego.Rego) // Create a new in-memory store @@ -170,7 +169,7 @@ func handleErrors2(f func(rCtx *rego.BuiltinContext, a, b *ast.Term) (*ast.Term, } } -func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin { +func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin { return []*tester.Builtin{ { Decl: verifyDecl, @@ -197,7 +196,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin { } } -func fetchInTotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 { +func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 { return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) { predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String) if !ok { @@ -228,8 +227,8 @@ func fetchInTotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 { } func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) { - env := new(att.Envelope) - opts := new(att.VerifyOptions) + env := new(attestation.Envelope) + opts := new(attestation.VerifyOptions) err := ast.As(envTerm.Value, env) if err != nil { return nil, fmt.Errorf("failed to cast envelope: %w", err) @@ -239,7 +238,7 @@ func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term return nil, fmt.Errorf("failed to cast verifier options: %w", err) } - payload, err := att.VerifyDSSE(rCtx.Context, env, opts) + payload, err := attestation.VerifyDSSE(rCtx.Context, env, opts) if err != nil { return nil, err } diff --git a/pkg/signerverifier/README.md b/pkg/signerverifier/README.md new file mode 100644 index 0000000..0348ada --- /dev/null +++ b/pkg/signerverifier/README.md @@ -0,0 +1,2 @@ +## signerverifier +This package implements methods to sign and verify attestation envelopes. \ No newline at end of file diff --git a/pkg/tlog/README.md b/pkg/tlog/README.md new file mode 100644 index 0000000..b95c22e --- /dev/null +++ b/pkg/tlog/README.md @@ -0,0 +1,2 @@ +## tlog +This package implements transparency logging. \ No newline at end of file diff --git a/pkg/tuf/README.md b/pkg/tuf/README.md new file mode 100644 index 0000000..7b8d9a9 --- /dev/null +++ b/pkg/tuf/README.md @@ -0,0 +1,2 @@ +## tuf +This package implements TUF clients for http and oci data sources. \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..c928225 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,4 @@ +## scripts +This directory contains project scripts. + +`gen-testdata.sh` - used to generate static test data saved in `/test/testdata/` \ No newline at end of file diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..3dc7396 --- /dev/null +++ b/test/README.md @@ -0,0 +1,2 @@ +## test +This directory contains static `testdata` used to run go tests. \ No newline at end of file From 5a772633b0e22bdba72dadca0715c24d824f37a3 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 12 Aug 2024 16:43:42 -0500 Subject: [PATCH 3/7] feat: use EmptyConfigImage for mirror --- pkg/mirror/metadata.go | 7 ++++--- pkg/mirror/metadata_test.go | 15 ++++++++++----- pkg/mirror/targets.go | 6 ++++-- pkg/mirror/targets_test.go | 6 +++--- pkg/mirror/types.go | 3 ++- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/pkg/mirror/metadata.go b/pkg/mirror/metadata.go index 7f84b89..780212b 100644 --- a/pkg/mirror/metadata.go +++ b/pkg/mirror/metadata.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" + "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" @@ -17,7 +18,7 @@ import ( // ----------------- // GetMetadataManifest returns an image with TUF root metadata as layers. -func (m *TUFMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) { +func (m *TUFMirror) GetMetadataManifest(metadataURL string) (*oci.EmptyConfigImage, error) { metadata, err := m.getMetadataMirror(metadataURL) if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) @@ -26,7 +27,7 @@ func (m *TUFMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) { if err != nil { return nil, fmt.Errorf("failed to build metadata manifest: %w", err) } - return manifest, nil + return &oci.EmptyConfigImage{Image: manifest}, nil } // getMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes. @@ -183,7 +184,7 @@ func (m *TUFMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetM if err != nil { return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err) } - manifests = append(manifests, &Image{Image: img, Tag: role.Name}) + manifests = append(manifests, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: role.Name}) } return manifests, nil } diff --git a/pkg/mirror/metadata_test.go b/pkg/mirror/metadata_test.go index 98f5983..ee5417e 100644 --- a/pkg/mirror/metadata_test.go +++ b/pkg/mirror/metadata_test.go @@ -16,15 +16,20 @@ import ( "github.com/theupdateframework/go-tuf/v2/metadata" ) +const ( + metadataPath = "/metadata" + targetsPath = "/targets" +) + func TestGetTufMetadataMirror(t *testing.T) { server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo")))) defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) - tufMetadata, err := m.getMetadataMirror(server.URL + "/metadata") + tufMetadata, err := m.getMetadataMirror(server.URL + metadataPath) assert.NoError(t, err) // check that all roles are not empty @@ -39,10 +44,10 @@ func TestGetMetadataManifest(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) - img, err := m.GetMetadataManifest(server.URL + "/metadata") + img, err := m.GetMetadataManifest(server.URL + metadataPath) assert.NoError(t, err) assert.NotNil(t, img) @@ -78,7 +83,7 @@ func TestGetDelegatedMetadataMirrors(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) delegations, err := m.GetDelegatedMetadataMirrors() diff --git a/pkg/mirror/targets.go b/pkg/mirror/targets.go index 3a73411..df09e4c 100644 --- a/pkg/mirror/targets.go +++ b/pkg/mirror/targets.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" + "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" @@ -42,7 +43,7 @@ func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) { if err != nil { return nil, fmt.Errorf("failed to append role layer to image: %w", err) } - targetMirrors = append(targetMirrors, &Image{Image: img, Tag: name}) + targetMirrors = append(targetMirrors, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: name}) } return targetMirrors, nil } @@ -93,9 +94,10 @@ func (m *TUFMirror) GetDelegatedTargetMirrors() ([]*Index, error) { if err != nil { return nil, fmt.Errorf("failed to append role layer to image: %w", err) } + emptyConfigImage := &oci.EmptyConfigImage{Image: img} // append image to index with annotation index = mutate.AppendManifests(index, mutate.IndexAddendum{ - Add: img, + Add: emptyConfigImage, Descriptor: v1.Descriptor{ Annotations: map[string]string{ tufFileAnnotation: fmt.Sprintf("%s/%s", subdir, name), diff --git a/pkg/mirror/targets_test.go b/pkg/mirror/targets_test.go index aa785ef..c2b3ec5 100644 --- a/pkg/mirror/targets_test.go +++ b/pkg/mirror/targets_test.go @@ -27,7 +27,7 @@ func TestGetTufTargetsMirror(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) targets, err := m.GetTUFTargetMirrors() @@ -61,7 +61,7 @@ func TestTargetDelegationMetadata(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - tm, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) + tm, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) targets, err := tm.TUFClient.LoadDelegatedTargets("test-role", "targets") @@ -74,7 +74,7 @@ func TestGetDelegatedTargetMirrors(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) mirrors, err := m.GetDelegatedTargetMirrors() diff --git a/pkg/mirror/types.go b/pkg/mirror/types.go index b064c6c..41f933b 100644 --- a/pkg/mirror/types.go +++ b/pkg/mirror/types.go @@ -1,6 +1,7 @@ package mirror import ( + "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/tuf" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/theupdateframework/go-tuf/v2/metadata" @@ -32,7 +33,7 @@ type DelegatedTargetMetadata struct { } type Image struct { - Image v1.Image + Image *oci.EmptyConfigImage Tag string } From 57a61cc266f2977ccf8a6d982edddd2dd4ceb31b Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 12 Aug 2024 16:54:44 -0500 Subject: [PATCH 4/7] fix: e2e auth test --- pkg/oci/authn_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/oci/authn_test.go b/pkg/oci/authn_test.go index e40a488..119f19b 100644 --- a/pkg/oci/authn_test.go +++ b/pkg/oci/authn_test.go @@ -1,16 +1,17 @@ //go:build e2e -package oci +package oci_test import ( "testing" "github.com/docker/attest/internal/test" + "github.com/docker/attest/pkg/oci" "github.com/stretchr/testify/require" ) func TestRegistryAuth(t *testing.T) { - attIdx, err := IndexFromPath(test.UnsignedTestImage) + attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) require.NoError(t, err) // test cases for ecr, gcr and dockerhub testCases := []struct { @@ -21,9 +22,9 @@ func TestRegistryAuth(t *testing.T) { } for _, tc := range testCases { t.Run(tc.Image, func(t *testing.T) { - err := PushIndexToRegistry(attIdx.Index, tc.Image) + err := oci.PushIndexToRegistry(attIdx.Index, tc.Image) require.NoError(t, err) - _, err = IndexFromRemote(tc.Image) + _, err = oci.IndexFromRemote(tc.Image) require.NoError(t, err) }) } From 84cadeb97eacdcd43ad28a6d1479ec41357c09f9 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Tue, 13 Aug 2024 08:13:27 -0500 Subject: [PATCH 5/7] feat: output comments --- pkg/oci/output.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/oci/output.go b/pkg/oci/output.go index bf5c6de..059cadb 100644 --- a/pkg/oci/output.go +++ b/pkg/oci/output.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" ) +// PushImageToRegistry pushes an image to the registry with the specified name. func PushImageToRegistry(image v1.Image, imageName string) error { ref, err := name.ParseReference(imageName) if err != nil { @@ -22,6 +23,7 @@ func PushImageToRegistry(image v1.Image, imageName string) error { return remote.Write(ref, image, MultiKeychainOption()) } +// PushIndexToRegistry pushes an index to the registry with the specified name. func PushIndexToRegistry(index v1.ImageIndex, imageName string) error { // Parse the index name ref, err := name.ParseReference(imageName) @@ -33,6 +35,7 @@ func PushIndexToRegistry(index v1.ImageIndex, imageName string) error { return remote.WriteIndex(ref, index, MultiKeychainOption()) } +// SaveIndexAsOCILayout saves an image as an OCI layout to the specified path. func SaveImageAsOCILayout(image v1.Image, path string) error { // Save the image to the local filesystem err := os.MkdirAll(path, os.ModePerm) @@ -47,6 +50,7 @@ func SaveImageAsOCILayout(image v1.Image, path string) error { return l.AppendImage(image) } +// SaveIndexAsOCILayout saves an index as an OCI layout to the specified path. func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error { // Save the index to the local filesystem err := os.MkdirAll(path, os.ModePerm) @@ -61,6 +65,7 @@ func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error { return nil } +// SaveIndex saves an index to the specified outputs. func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) error { // split output by comma and write or push each one for _, output := range outputs { @@ -88,6 +93,7 @@ func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) erro return nil } +// SaveImage saves an image to the specified output. func SaveImage(output *ImageSpec, image v1.Image, imageName string) error { if output.Type == OCI { idx := v1.ImageIndex(empty.Index) @@ -112,6 +118,7 @@ func SaveImage(output *ImageSpec, image v1.Image, imageName string) error { return nil } +// SaveImagesNoTag saves a list of images by digest to the specified outputs. func SaveImagesNoTag(images []v1.Image, outputs []*ImageSpec) error { for _, output := range outputs { // OCI layout output not supported From 72f6517b2c0dfd171bbc4b3e8c53483b90684e5d Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Tue, 13 Aug 2024 08:26:36 -0500 Subject: [PATCH 6/7] refactor: move empty config image test --- pkg/attestation/referrers_test.go | 13 ------------- pkg/oci/types_test.go | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 pkg/oci/types_test.go diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index 28cb25b..9d7d272 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/docker/attest/internal/test" - "github.com/docker/attest/internal/util" "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/config" @@ -16,7 +15,6 @@ import ( "github.com/docker/attest/pkg/policy" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" - "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -328,14 +326,3 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) { } } } - -func TestEmptyConfigImageDigest(t *testing.T) { - empty := empty.Image - img := oci.EmptyConfigImage{Image: empty} - mf, err := img.RawManifest() - require.NoError(t, err) - hash := util.SHA256Hex(mf) - digest, err := img.Digest() - require.NoError(t, err) - assert.Equal(t, digest.Hex, hash) -} diff --git a/pkg/oci/types_test.go b/pkg/oci/types_test.go new file mode 100644 index 0000000..704fdd9 --- /dev/null +++ b/pkg/oci/types_test.go @@ -0,0 +1,21 @@ +package oci + +import ( + "testing" + + "github.com/docker/attest/internal/util" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEmptyConfigImageDigest(t *testing.T) { + empty := empty.Image + img := EmptyConfigImage{Image: empty} + mf, err := img.RawManifest() + require.NoError(t, err) + hash := util.SHA256Hex(mf) + digest, err := img.Digest() + require.NoError(t, err) + assert.Equal(t, digest.Hex, hash) +} From 5162cfa404aedd94f4765303b53276706a639ffc Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Tue, 13 Aug 2024 10:03:33 -0500 Subject: [PATCH 7/7] refactor: ensure tests are in correct pkg --- pkg/attest/sign_test.go | 151 ------------------ pkg/attest/verify.go | 11 -- pkg/attestation/attestation.go | 11 ++ .../example_attestation_manifest_test.go | 3 +- pkg/attestation/sign_test.go | 150 +++++++++++++++++ pkg/oci/output_test.go | 3 +- pkg/tuf/registry_test.go | 13 +- 7 files changed, 170 insertions(+), 172 deletions(-) diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index ac3f9df..3d6a602 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -1,10 +1,6 @@ package attest import ( - "encoding/json" - "fmt" - "net/http/httptest" - "net/url" "path/filepath" "testing" @@ -12,10 +8,6 @@ import ( "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" - "github.com/google/go-containerregistry/pkg/registry" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/static" - "github.com/google/go-containerregistry/pkg/v1/types" intoto "github.com/in-toto/in-toto-golang/in_toto" v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" "github.com/stretchr/testify/assert" @@ -88,146 +80,3 @@ func TestSignVerifyOCILayout(t *testing.T) { }) } } - -func TestAddSignedLayerAnnotations(t *testing.T) { - ctx, signer := test.Setup(t) - testCases := []struct { - name string - replace bool - }{ - {"replaced", true}, - {"not replaced", false}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - data := []byte("signed") - testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType)) - mediaType := types.OCIManifestSchema1 - opts := &attestation.SigningOptions{} - originalLayer := &attestation.Layer{ - Layer: testLayer, - Statement: &intoto.Statement{ - StatementHeader: intoto.StatementHeader{ - PredicateType: attestation.VSAPredicateType, - }, - }, - Annotations: map[string]string{"test": "test"}, - } - - manifest := &attestation.Manifest{ - OriginalDescriptor: &v1.Descriptor{ - MediaType: mediaType, - }, - OriginalLayers: []*attestation.Layer{ - originalLayer, - }, - SubjectDescriptor: &v1.Descriptor{}, - } - err := manifest.Add(ctx, signer, originalLayer.Statement, opts) - require.NoError(t, err) - - newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace)) - require.NoError(t, err) - mf, _ := newImg.RawManifest() - type Annotations struct { - Annotations map[string]string `json:"annotations"` - } - type Layers struct { - Layers []Annotations `json:"layers"` - } - l := &Layers{} - err = json.Unmarshal(mf, l) - require.NoError(t, err) - _, ok := l.Layers[0].Annotations["test"] - assert.Truef(t, ok, "missing annotations") - }) - } -} - -func TestSimpleStatementSigning(t *testing.T) { - ctx, signer := test.Setup(t) - empty := types.MediaType("application/vnd.oci.empty.v1+json") - testCases := []struct { - name string - replace bool - }{ - {"replaced", true}, - {"not replaced", false}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - opts := &attestation.SigningOptions{} - statement := &intoto.Statement{ - StatementHeader: intoto.StatementHeader{ - PredicateType: attestation.VSAPredicateType, - }, - } - statement2 := &intoto.Statement{ - StatementHeader: intoto.StatementHeader{ - PredicateType: attestation.VSAPredicateType, - }, - } - digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620") - require.NoError(t, err) - subject := &v1.Descriptor{ - MediaType: "application/vnd.oci.image.manifest.v1+json", - Digest: digest, - } - manifest, err := NewAttestationManifest(subject) - require.NoError(t, err) - err = manifest.Add(ctx, signer, statement, opts) - require.NoError(t, err) - - err = manifest.Add(ctx, signer, statement2, opts) - require.NoError(t, err) - - // fake that the manfifest was loaded from a real image - manifest.OriginalLayers = manifest.SignedLayers - envelopes, err := attestation.ExtractEnvelopes(manifest, attestation.VSAPredicateType) - require.NoError(t, err) - assert.Len(t, envelopes, 2) - - newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace)) - require.NoError(t, err) - layers, err := newImg.Layers() - require.NoError(t, err) - if tc.replace { - assert.Len(t, layers, 2) - } else { - assert.Len(t, layers, 4) - } - - newImgs, err := manifest.BuildReferringArtifacts() - require.NoError(t, err) - assert.Len(t, newImgs, 2) - for _, img := range newImgs { - mf, err := img.Manifest() - require.NoError(t, err) - assert.Contains(t, mf.ArtifactType, "application/vnd.in-toto") - assert.Contains(t, mf.ArtifactType, "+dsse") - assert.Equal(t, subject.MediaType, mf.MediaType) - assert.Equal(t, empty, mf.Config.MediaType) - assert.Equal(t, int64(2), mf.Config.Size) - assert.Equal(t, "{}", string(mf.Config.Data)) - layers, err := img.Layers() - require.NoError(t, err) - assert.Len(t, layers, 1) - } - server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true))) - defer server.Close() - - u, err := url.Parse(server.URL) - require.NoError(t, err) - - indexName := fmt.Sprintf("%s/repo:root", u.Host) - output, err := oci.ParseImageSpecs(indexName) - require.NoError(t, err) - artifacts, err := manifest.BuildReferringArtifacts() - require.NoError(t, err) - err = oci.SaveImagesNoTag(artifacts, output) - require.NoError(t, err) - }) - } -} diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index 963570c..c1d6540 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -11,7 +11,6 @@ import ( "github.com/docker/attest/pkg/config" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" - v1 "github.com/google/go-containerregistry/pkg/v1" intoto "github.com/in-toto/in-toto-golang/in_toto" ) @@ -164,13 +163,3 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx verificationResult.SubjectDescriptor = desc return verificationResult, nil } - -func NewAttestationManifest(subject *v1.Descriptor) (*attestation.Manifest, error) { - return &attestation.Manifest{ - OriginalDescriptor: &v1.Descriptor{ - MediaType: "application/vnd.oci.image.manifest.v1+json", - }, - OriginalLayers: []*attestation.Layer{}, - SubjectDescriptor: subject, - }, nil -} diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index 4a2dc3d..a3a1f0e 100644 --- a/pkg/attestation/attestation.go +++ b/pkg/attestation/attestation.go @@ -21,6 +21,17 @@ import ( "github.com/secure-systems-lab/go-securesystemslib/dsse" ) +// NewManifest creates a new attestation manifest from a descriptor. +func NewManifest(subject *v1.Descriptor) (*Manifest, error) { + return &Manifest{ + OriginalDescriptor: &v1.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + }, + OriginalLayers: []*Layer{}, + SubjectDescriptor: subject, + }, nil +} + // ManifestsFromIndex extracts all attestation manifests from an index. func ManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) { idx, err := index.IndexManifest() diff --git a/pkg/attestation/example_attestation_manifest_test.go b/pkg/attestation/example_attestation_manifest_test.go index 4b21229..79dffc3 100644 --- a/pkg/attestation/example_attestation_manifest_test.go +++ b/pkg/attestation/example_attestation_manifest_test.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/signerverifier" @@ -62,7 +61,7 @@ func ExampleManifest() { } // create a new manifest to hold the attestation - manifest, err := attest.NewAttestationManifest(desc) + manifest, err := attestation.NewManifest(desc) if err != nil { panic(err) } diff --git a/pkg/attestation/sign_test.go b/pkg/attestation/sign_test.go index d586c3e..2c18184 100644 --- a/pkg/attestation/sign_test.go +++ b/pkg/attestation/sign_test.go @@ -6,12 +6,19 @@ import ( "crypto/rand" "encoding/json" "fmt" + "net/http/httptest" + "net/url" "testing" "time" "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/signerverifier" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -149,3 +156,146 @@ func TestSignVerifyAttestation(t *testing.T) { }) } } + +func TestAddSignedLayerAnnotations(t *testing.T) { + ctx, signer := test.Setup(t) + testCases := []struct { + name string + replace bool + }{ + {"replaced", true}, + {"not replaced", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data := []byte("signed") + testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType)) + mediaType := types.OCIManifestSchema1 + opts := &attestation.SigningOptions{} + originalLayer := &attestation.Layer{ + Layer: testLayer, + Statement: &intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + PredicateType: attestation.VSAPredicateType, + }, + }, + Annotations: map[string]string{"test": "test"}, + } + + manifest := &attestation.Manifest{ + OriginalDescriptor: &v1.Descriptor{ + MediaType: mediaType, + }, + OriginalLayers: []*attestation.Layer{ + originalLayer, + }, + SubjectDescriptor: &v1.Descriptor{}, + } + err := manifest.Add(ctx, signer, originalLayer.Statement, opts) + require.NoError(t, err) + + newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace)) + require.NoError(t, err) + mf, _ := newImg.RawManifest() + type Annotations struct { + Annotations map[string]string `json:"annotations"` + } + type Layers struct { + Layers []Annotations `json:"layers"` + } + l := &Layers{} + err = json.Unmarshal(mf, l) + require.NoError(t, err) + _, ok := l.Layers[0].Annotations["test"] + assert.Truef(t, ok, "missing annotations") + }) + } +} + +func TestSimpleStatementSigning(t *testing.T) { + ctx, signer := test.Setup(t) + empty := types.MediaType("application/vnd.oci.empty.v1+json") + testCases := []struct { + name string + replace bool + }{ + {"replaced", true}, + {"not replaced", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + opts := &attestation.SigningOptions{} + statement := &intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + PredicateType: attestation.VSAPredicateType, + }, + } + statement2 := &intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + PredicateType: attestation.VSAPredicateType, + }, + } + digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620") + require.NoError(t, err) + subject := &v1.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: digest, + } + manifest, err := attestation.NewManifest(subject) + require.NoError(t, err) + err = manifest.Add(ctx, signer, statement, opts) + require.NoError(t, err) + + err = manifest.Add(ctx, signer, statement2, opts) + require.NoError(t, err) + + // fake that the manfifest was loaded from a real image + manifest.OriginalLayers = manifest.SignedLayers + envelopes, err := attestation.ExtractEnvelopes(manifest, attestation.VSAPredicateType) + require.NoError(t, err) + assert.Len(t, envelopes, 2) + + newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace)) + require.NoError(t, err) + layers, err := newImg.Layers() + require.NoError(t, err) + if tc.replace { + assert.Len(t, layers, 2) + } else { + assert.Len(t, layers, 4) + } + + newImgs, err := manifest.BuildReferringArtifacts() + require.NoError(t, err) + assert.Len(t, newImgs, 2) + for _, img := range newImgs { + mf, err := img.Manifest() + require.NoError(t, err) + assert.Contains(t, mf.ArtifactType, "application/vnd.in-toto") + assert.Contains(t, mf.ArtifactType, "+dsse") + assert.Equal(t, subject.MediaType, mf.MediaType) + assert.Equal(t, empty, mf.Config.MediaType) + assert.Equal(t, int64(2), mf.Config.Size) + assert.Equal(t, "{}", string(mf.Config.Data)) + layers, err := img.Layers() + require.NoError(t, err) + assert.Len(t, layers, 1) + } + server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true))) + defer server.Close() + + u, err := url.Parse(server.URL) + require.NoError(t, err) + + indexName := fmt.Sprintf("%s/repo:root", u.Host) + output, err := oci.ParseImageSpecs(indexName) + require.NoError(t, err) + artifacts, err := manifest.BuildReferringArtifacts() + require.NoError(t, err) + err = oci.SaveImagesNoTag(artifacts, output) + require.NoError(t, err) + }) + } +} diff --git a/pkg/oci/output_test.go b/pkg/oci/output_test.go index 53784c6..27fc546 100644 --- a/pkg/oci/output_test.go +++ b/pkg/oci/output_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/docker/attest/internal/test" - "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci" "github.com/google/go-containerregistry/pkg/registry" @@ -78,7 +77,7 @@ func TestSavingReferrers(t *testing.T) { MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest, } - manifest, err := attest.NewAttestationManifest(subject) + manifest, err := attestation.NewManifest(subject) require.NoError(t, err) err = manifest.Add(ctx, signer, statement, opts) require.NoError(t, err) diff --git a/pkg/tuf/registry_test.go b/pkg/tuf/registry_test.go index 16418c6..45ead27 100644 --- a/pkg/tuf/registry_test.go +++ b/pkg/tuf/registry_test.go @@ -32,6 +32,9 @@ const ( tufTargetMediaType = "application/vnd.tuf.target" testRole = "test-role" tufMetadataRepo = "tuf-metadata" + targetsPath = "/tuf-targets" + metadataPath = "/tuf-metadata" + targetsRepo = "test" + targetsPath ) func TestRegistryFetcher(t *testing.T) { @@ -44,9 +47,9 @@ func TestRegistryFetcher(t *testing.T) { }() LoadRegistryTestData(t, regAddr, OCITUFTestDataPath) - metadataRepo := regAddr.Host + "/tuf-metadata" + metadataRepo := regAddr.Host + metadataPath metadataImgTag := LatestTag - targetsRepo := regAddr.Host + "/tuf-targets" + targetsRepo := regAddr.Host + targetsPath targetFile := "test.txt" delegatedRole := testRole dir := CreateTempDir(t, "", "tuf_temp") @@ -122,7 +125,7 @@ func TestFindFileInManifest(t *testing.T) { // make test image manifest file := "test.json" data := []byte("test") - hash := v1.Hash{Algorithm: "sha256", Hex: util.SHA256Hex(data)} + hash := v1.Hash{Hex: util.SHA256Hex(data)} img := empty.Image img = mutate.MediaType(img, types.OCIManifestSchema1) img = mutate.ConfigMediaType(img, types.OCIConfigJSON) @@ -150,7 +153,6 @@ func TestFindFileInManifest(t *testing.T) { indexManifest, err := idx.RawManifest() assert.NoError(t, err) // cache image layer - targetsRepo := "test/tuf-targets" d := &RegistryFetcher{ cache: NewImageCache(), targetsRepo: targetsRepo, @@ -183,9 +185,8 @@ func TestFindFileInManifest(t *testing.T) { } func TestParseImgRef(t *testing.T) { - metadataRepo := "test/tuf-metadata" + metadataRepo := "test" + metadataPath metadataTag := LatestTag - targetsRepo := "test/tuf-targets" delegatedRole := testRole testCases := []struct { name string