From 8ba965664531d2cf50059aa48e93c9c530ec9f23 Mon Sep 17 00:00:00 2001 From: James Carnegie Date: Thu, 13 Jun 2024 16:10:41 +0100 Subject: [PATCH] Add support for OCI Referrers and fallback (#50) * Add support for OCI Referrers and fallback --- internal/test/test.go | 40 ++--- pkg/attest/example_sign_test.go | 2 +- pkg/attest/sign.go | 83 +++++++--- pkg/attest/sign_test.go | 7 +- pkg/attest/verify_test.go | 6 +- pkg/attestation/attestation.go | 13 +- pkg/attestation/referrers_test.go | 156 +++++++++++++++++++ pkg/attestation/sign.go | 5 +- pkg/attestation/types.go | 27 +++- pkg/oci/oci.go | 150 +++++++++++++++++- pkg/oci/resolver.go | 4 + pkg/oci/types.go | 11 +- pkg/policy/policy.go | 6 +- test/testdata/local-policy-pass/mapping.yaml | 5 + test/testdata/unsigned-test-image/index.json | 15 +- 15 files changed, 463 insertions(+), 67 deletions(-) create mode 100644 pkg/attestation/referrers_test.go diff --git a/internal/test/test.go b/internal/test/test.go index 2ff8ebf..2da3576 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -96,23 +96,8 @@ type AnnotatedStatement struct { Annotations map[string]string } -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) - } - mfs2, err := mfs.IndexManifest() +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) } @@ -124,7 +109,7 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat continue } - attestationImage, err := mfs.Image(mf.Digest) + 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) } @@ -189,3 +174,22 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat } 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/example_sign_test.go b/pkg/attest/example_sign_test.go index 78c2ba4..358bb2e 100644 --- a/pkg/attest/example_sign_test.go +++ b/pkg/attest/example_sign_test.go @@ -32,7 +32,7 @@ func ExampleSign_remote() { // load image index with unsigned attestation-manifests ref := "docker/image-signer-verifier:latest" - att, err := oci.AttestationIndexFromRemote(ref) + att, err := oci.SubjectIndexFromRemote(ref) if err != nil { panic(err) } diff --git a/pkg/attest/sign.go b/pkg/attest/sign.go index d0d69f2..6865f42 100644 --- a/pkg/attest/sign.go +++ b/pkg/attest/sign.go @@ -19,20 +19,42 @@ import ( ) func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (v1.ImageIndex, error) { + images, err := SignedAttestationImages(ctx, idx, signer, opts) + if err != nil { + return nil, fmt.Errorf("failed to sign attestation images: %w", err) + } + for _, image := range images { + idx, err = addImageToIndex(idx, image.Image, image.Descriptor, image.AttestationManifest) + if err != nil { + return nil, fmt.Errorf("failed to add signed layers to index: %w", err) + } + } + return idx, nil +} + +func SignedAttestationImages(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.SignedAttestationImage, error) { // extract attestation manifests from index attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx) if err != nil { return nil, fmt.Errorf("failed to get attestation manifests: %w", err) } - + if len(attestationManifests) == 0 { + return nil, fmt.Errorf("no attestation manifests found") + } + images := []*attestation.SignedAttestationImage{} // sign every attestation layer in each manifest for _, manifest := range attestationManifests { - idx, err = signLayersAndAddToIndex(ctx, idx, manifest.Attestation.Layers, manifest, signer, opts) + newImg, newDescriptor, err := SignLayersAndAddToImage(ctx, manifest.Attestation.Layers, manifest, signer, opts) if err != nil { - return nil, fmt.Errorf("failed to add signed layers: %w", err) + return nil, fmt.Errorf("failed to add signed layers to image: %w", err) } + images = append(images, &attestation.SignedAttestationImage{ + Image: newImg, + Descriptor: newDescriptor, + AttestationManifest: manifest, + }) } - return idx, nil + return images, nil } func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.Statement, signer dsse.SignerVerifier) (v1.ImageIndex, error) { @@ -52,7 +74,7 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St } updatedIndex := false for _, manifest := range attestationManifests { - if subjectDigests[manifest.Annotations[oci.DockerReferenceDigest]] { + if subjectDigests[manifest.Annotations[attestation.DockerReferenceDigest]] { attestationLayers := []attestation.AttestationLayer{ { Statement: statement, @@ -63,9 +85,13 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St }, } // hard-coding replace to false here, because if it's true we will remove any unsigned statements, even unrelated ones - idx, err = signLayersAndAddToIndex(ctx, idx, attestationLayers, manifest, signer, &attestation.SigningOptions{Replace: false}) + newImg, newDec, err := SignLayersAndAddToImage(ctx, attestationLayers, manifest, signer, &attestation.SigningOptions{Replace: false}) if err != nil { - return nil, fmt.Errorf("failed to add signed layers: %w", err) + return nil, fmt.Errorf("failed to add signed layers to image: %w", err) + } + idx, err = addImageToIndex(idx, newImg, newDec, manifest) + if err != nil { + return nil, fmt.Errorf("failed to add attestation image to index: %w", err) } updatedIndex = true } @@ -74,33 +100,34 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St return nil, fmt.Errorf("no attestation manifest found for statement") } return idx, nil - } -func signLayersAndAddToIndex( +func SignLayersAndAddToImage( ctx context.Context, - idx v1.ImageIndex, attestationLayers []attestation.AttestationLayer, manifest attestation.AttestationManifest, signer dsse.SignerVerifier, - opts *attestation.SigningOptions) (v1.ImageIndex, error) { + opts *attestation.SigningOptions) (v1.Image, *v1.Descriptor, error) { signedLayers, err := signLayers(ctx, attestationLayers, signer, opts) if err != nil { - return nil, fmt.Errorf("failed to sign attestations: %w", err) + return nil, nil, fmt.Errorf("failed to sign attestations: %w", err) } newImg, err := addSignedLayers(signedLayers, manifest, opts) if err != nil { - return nil, fmt.Errorf("failed to add signed layers: %w", err) + return nil, nil, fmt.Errorf("failed to add signed layers: %w", err) + } + if !opts.SkipSubject { + newImg = mutate.Subject(newImg, *manifest.SubjectDescriptor).(v1.Image) } newDesc, err := partial.Descriptor(newImg) if err != nil { - return nil, fmt.Errorf("failed to get descriptor: %w", err) + return nil, nil, fmt.Errorf("failed to get descriptor: %w", err) } cf, err := manifest.Attestation.Image.ConfigFile() if err != nil { - return nil, fmt.Errorf("failed to get config file: %w", err) + return nil, nil, fmt.Errorf("failed to get config file: %w", err) } newDesc.Platform = cf.Platform() if newDesc.Platform == nil { @@ -111,10 +138,21 @@ func signLayersAndAddToIndex( } newDesc.MediaType = manifest.MediaType newDesc.Annotations = manifest.Annotations + + return newImg, newDesc, nil +} + +func addImageToIndex( + idx v1.ImageIndex, + img v1.Image, + desc *v1.Descriptor, + manifest attestation.AttestationManifest, +) (v1.ImageIndex, error) { + idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest)) idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ - Add: newImg, - Descriptor: *newDesc, + Add: img, + Descriptor: *desc, }) return idx, nil } @@ -168,6 +206,13 @@ func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signe // addSignedLayers adds signed layers to a new or existing attestation image func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *attestation.SigningOptions) (v1.Image, error) { + withAnnotations := func(img v1.Image) v1.Image { + // this is handy when dealing with referrers + return mutate.Annotations(img, map[string]string{ + attestation.DockerReferenceType: attestation.AttestationManifestType, + attestation.DockerReferenceDigest: manifest.SubjectDescriptor.Digest.String(), + }).(v1.Image) + } var err error if opts.Replace { // create a new attestation image with only signed layers @@ -189,7 +234,7 @@ func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.Attest } } } - return newImg, nil + return withAnnotations(newImg), nil } // Add signed layers to the existing image for _, layer := range signedLayers { @@ -198,5 +243,5 @@ func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.Attest return nil, fmt.Errorf("failed to append layer: %w", err) } } - return manifest.Attestation.Image, nil + return withAnnotations(manifest.Attestation.Image), nil } diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index 15b0d8f..96c5eb6 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -42,7 +42,7 @@ func TestSignVerifyOCILayout(t *testing.T) { replace bool }{ - {"signed replaced (does nothing)", UnsignedTestImage, 0, 4, true}, + {"signed replaced", UnsignedTestImage, 0, 4, true}, {"without replace", UnsignedTestImage, 4, 4, false}, // image without provenance doesn't fail {"no provenance (replace)", NoProvenanceImage, 0, 2, true}, @@ -57,7 +57,7 @@ func TestSignVerifyOCILayout(t *testing.T) { opts := &attestation.SigningOptions{ Replace: tc.replace, } - attIdx, err := oci.AttestationIndexFromPath(tc.TestImage) + attIdx, err := oci.SubjectIndexFromPath(tc.TestImage) require.NoError(t, err) signedIndex, err := Sign(ctx, attIdx.Index, signer, opts) require.NoError(t, err) @@ -107,7 +107,7 @@ func TestAddAttestation(t *testing.T) { expectedStatements := 4 outputLayout := test.CreateTempDir(t, "", TestTempDir) - attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) + attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage) require.NoError(t, err) statementToAdd := &intoto.Statement{ @@ -200,6 +200,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) { }, }, }, + SubjectDescriptor: &v1.Descriptor{}, } newImg, err := addSignedLayers(signedLayers, manifest, opts) require.NoError(t, err) diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 50e5bff..6a33afb 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -76,7 +76,7 @@ func TestVSA(t *testing.T) { opts := &attestation.SigningOptions{ Replace: true, } - attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) + attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage) assert.NoError(t, err) signedIndex, err := Sign(ctx, attIdx.Index, signer, opts) assert.NoError(t, err) @@ -128,7 +128,7 @@ func TestVerificationFailure(t *testing.T) { opts := &attestation.SigningOptions{ Replace: true, } - attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) + attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage) assert.NoError(t, err) signedIndex, err := Sign(ctx, attIdx.Index, signer, opts) assert.NoError(t, err) @@ -194,7 +194,7 @@ func TestSignVerifyNoTL(t *testing.T) { {name: "no tl", signTL: false, policyDir: PassPolicyDir, success: false}, } - attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) + attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage) assert.NoError(t, err) for _, tc := range testCases { diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index ece2f48..fb53106 100644 --- a/pkg/attestation/attestation.go +++ b/pkg/attestation/attestation.go @@ -17,9 +17,19 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes if err != nil { return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err) } + subjects := make(map[string]*v1.Descriptor) + for _, subject := range idx.Manifests { + subjects[subject.Digest.String()] = &subject + } + var attestationManifests []AttestationManifest for _, manifest := range idx.Manifests { + if manifest.Annotations[DockerReferenceType] == AttestationManifestType { + subject := subjects[manifest.Annotations[DockerReferenceDigest]] + if subject == nil { + return nil, fmt.Errorf("failed to find subject for attestation manifest: %w", err) + } attestationImage, err := index.Image(manifest.Digest) if err != nil { return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err) @@ -30,7 +40,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes } attestationManifests = append(attestationManifests, AttestationManifest{ - Manifest: manifest, + Descriptor: manifest, + SubjectDescriptor: subject, Attestation: AttestationImage{ Layers: attestationLayers, Image: attestationImage}, diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go new file mode 100644 index 0000000..b0aa065 --- /dev/null +++ b/pkg/attestation/referrers_test.go @@ -0,0 +1,156 @@ +package attestation_test + +import ( + "fmt" + "net/http/httptest" + "net/url" + "path/filepath" + "testing" + + "github.com/docker/attest/internal/test" + "github.com/docker/attest/pkg/attest" + "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/mirror" + "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/policy" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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") + PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl") + FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") + TestTempDir = "attest-sign-test" +) + +func TestAttestationReferenceTypes(t *testing.T) { + ctx, signer := test.Setup(t) + platforms := []string{"linux/amd64", "linux/arm64"} + for _, tc := range []struct { + server *httptest.Server + skipSubject bool + }{ + { + server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), + }, + { + server: httptest.NewServer(registry.New()), + }, + { + server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), + skipSubject: true, + }, + } { + s := tc.server + defer s.Close() + u, err := url.Parse(s.URL) + require.NoError(t, err) + + opts := &attestation.SigningOptions{ + Replace: true, + SkipSubject: tc.skipSubject, + } + attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage) + require.NoError(t, err) + signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts) + require.NoError(t, err) + + indexName := fmt.Sprintf("%s/repo:root", u.Host) + require.NoError(t, err) + err = mirror.PushToRegistry(signedIndex, indexName) + + for _, platform := range platforms { + // can eval policy in the normal way + resolver, err := oci.NewRegistryAttestationResolver(indexName, platform) + require.NoError(t, err) + + policyOpts := &policy.PolicyOptions{ + LocalPolicyDir: PassPolicyDir, + } + results, err := attest.Verify(ctx, policyOpts, resolver) + require.NoError(t, err) + assert.Equal(t, attest.OutcomeSuccess, results.Outcome) + if !tc.skipSubject { + // can evaluate policy using referrers + referrersResolver, err := oci.NewReferrersAttestationResolver(indexName, oci.WithPlatform(platform)) + require.NoError(t, err) + + results, err = attest.Verify(ctx, policyOpts, referrersResolver) + require.NoError(t, err) + assert.Equal(t, attest.OutcomeSuccess, results.Outcome) + } + } + } +} + +func TestReferencesInDifferentRepo(t *testing.T) { + ctx, signer := test.Setup(t) + repoName := "repo" + for _, tc := range []struct { + server *httptest.Server + refServer *httptest.Server + }{ + { + server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), + refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), + }, + { + server: httptest.NewServer(registry.New()), + refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), + }, + } { + server := tc.server + defer server.Close() + serverUrl, err := url.Parse(server.URL) + require.NoError(t, err) + + refServer := tc.refServer + defer refServer.Close() + refServerUrl, err := url.Parse(refServer.URL) + require.NoError(t, err) + + opts := &attestation.SigningOptions{ + Replace: true, + SkipTL: true, + } + attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage) + require.NoError(t, err) + + indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName) + err = mirror.PushToRegistry(attIdx.Index, indexName) + require.NoError(t, err) + + signedImages, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts) + require.NoError(t, err) + + // push signed attestation image to the ref server + for _, img := range signedImages { + // push references using subject-digest.att convention + err = mirror.PushToRegistry(&img.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName)) + require.NoError(t, err) + } + mfs2, err := attIdx.Index.IndexManifest() + require.NoError(t, err) + for _, mf := range mfs2.Manifests { + //skip signed/unsigned attestations + if mf.Annotations[attestation.DockerReferenceType] == "attestation-manifest" { + continue + } + // can evaluate policy using referrers in a different repo + repo := fmt.Sprintf("%s/%s", refServerUrl.Host, repoName) + referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String()) + referrersResolver, err := oci.NewReferrersAttestationResolver(referencedImage, oci.WithReferrersRepo(repo)) + require.NoError(t, err) + policyOpts := &policy.PolicyOptions{ + LocalPolicyDir: PassPolicyDir, + } + results, err := attest.Verify(ctx, policyOpts, referrersResolver) + require.NoError(t, err) + assert.Equal(t, attest.OutcomeSuccess, results.Outcome) + } + } +} diff --git a/pkg/attestation/sign.go b/pkg/attestation/sign.go index 91cd640..7fd77c6 100644 --- a/pkg/attestation/sign.go +++ b/pkg/attestation/sign.go @@ -12,10 +12,7 @@ import ( // SignDSSE signs a payload with a given signer and uploads the signature to the transparency log func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) { - payloadType := opts.PayloadType - if payloadType == "" { - payloadType = intoto.PayloadType - } + payloadType := intoto.PayloadType env := new(Envelope) env.Payload = base64Encoding.EncodeToString(payload) env.PayloadType = payloadType diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index ae3ecce..ac40453 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -14,6 +14,7 @@ import ( const ( DockerReferenceType = "vnd.docker.reference.type" AttestationManifestType = "attestation-manifest" + DockerReferenceDigest = "vnd.docker.reference.digest" DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json" RekorTlExtKind = "Rekor" OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse" @@ -33,12 +34,19 @@ type AttestationImage struct { Image v1.Image } +type SignedAttestationImage struct { + Image v1.Image + Descriptor *v1.Descriptor + AttestationManifest AttestationManifest +} + type AttestationManifest struct { - Manifest v1.Descriptor - Attestation AttestationImage - MediaType types.MediaType - Annotations map[string]string - Digest v1.Hash + Descriptor v1.Descriptor + Attestation AttestationImage + MediaType types.MediaType + Annotations map[string]string + Digest v1.Hash + SubjectDescriptor *v1.Descriptor } // the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged @@ -72,9 +80,12 @@ type VerifyOptions struct { } type SigningOptions struct { - Replace bool - SkipTL bool - PayloadType string + // replace unsigned statements with signed attestations + Replace bool + // don't log to the configured transparency log + SkipTL bool + // don't add OCI subject field to attestation image + SkipSubject bool } func DSSEMediaType(predicateType string) (string, error) { diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index 1ee2645..9371a3d 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -72,7 +72,7 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*Atte continue } - if mf.Annotations[DockerReferenceDigest] != imageDigest { + if mf.Annotations[att.DockerReferenceDigest] != imageDigest { continue } @@ -195,6 +195,152 @@ func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) { return r.Digest, nil } +type ReferrersResolver struct { + image string + platform *v1.Platform + referrersRepo string + manifests []*AttestationManifest +} + +func NewReferrersAttestationResolver(image string, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) { + res := &ReferrersResolver{ + image: image, + } + for _, opt := range options { + err := opt(res) + if err != nil { + return nil, err + } + } + return res, nil +} + +func WithReferrersRepo(repo string) func(*ReferrersResolver) error { + return func(r *ReferrersResolver) error { + r.referrersRepo = repo + return nil + } +} + +// WithPlatform sets the platform for the resolver (needed for platform specific tag resolution) +func WithPlatform(platform string) func(*ReferrersResolver) error { + return func(r *ReferrersResolver) error { + p, err := ParsePlatform(platform) + if err != nil { + return err + } + r.platform = p + return nil + } +} + +func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error { + if r.manifests == nil { + options := withOptions(ctx, r.platform) + subjectRef, err := name.ParseReference(r.image) + if err != nil { + return fmt.Errorf("failed to parse reference: %w", err) + } + desc, err := remote.Image(subjectRef, options...) + if err != nil { + return fmt.Errorf("failed to get image manifest: %w", err) + } + subjectDigest, err := desc.Digest() + if err != nil { + return fmt.Errorf("failed to get image digest: %w", err) + } + var referrersSubjectRef name.Digest + if r.referrersRepo != "" { + referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest.String())) + if err != nil { + return fmt.Errorf("failed to create referrers reference: %w", err) + } + } else { + referrersSubjectRef = subjectRef.Context().Digest(subjectDigest.String()) + } + referrersIndex, err := remote.Referrers(referrersSubjectRef) + if err != nil { + return fmt.Errorf("failed to get referrers: %w", err) + } + referrersIndexManifest, err := referrersIndex.IndexManifest() + if err != nil { + return fmt.Errorf("failed to get index manifest: %w", err) + } + if len(referrersIndexManifest.Manifests) == 0 { + return errors.New("no referrers found") + } + aManifests := make([]*AttestationManifest, 0) + for _, m := range referrersIndexManifest.Manifests { + + remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String()) + attestationImage, err := remote.Image(remoteRef) + if err != nil { + return fmt.Errorf("failed to get referred image: %w", err) + } + manifest, err := attestationImage.Manifest() + if err != nil { + return fmt.Errorf("failed to get manifest: %w", err) + } + if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType { + continue + } + if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest.String() { + continue + } + attest := &AttestationManifest{ + Name: r.image, + Image: attestationImage, + Manifest: manifest, + Descriptor: &m, + Digest: subjectDigest.String(), + Platform: r.platform, + } + aManifests = append(aManifests, attest) + } + + if len(aManifests) == 0 { + return errors.New("no attestation manifests found") + } + r.manifests = aManifests + } + return nil +} + +func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) { + err := r.resolveAttestations(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve attestations: %w", err) + } + var envs []*att.Envelope + for _, attest := range r.manifests { + es, err := ExtractEnvelopes(attest, predicateType) + if err != nil { + return nil, fmt.Errorf("failed to extract envelopes: %w", err) + } + envs = append(envs, es...) + } + return envs, nil +} + +func (r *ReferrersResolver) ImageName(ctx context.Context) (string, error) { + return r.image, nil +} + +func (r *ReferrersResolver) ImagePlatform() (*v1.Platform, error) { + return r.platform, nil +} + +func (r *ReferrersResolver) ImageDigest(ctx context.Context) (string, error) { + err := r.resolveAttestations(ctx) + if err != nil { + return "", fmt.Errorf("failed to resolve attestations: %w", err) + } + if len(r.manifests) == 0 { + return "", errors.New("no attestation manifests found") + } + return r.manifests[0].Digest, nil +} + type RegistryResolver struct { image string platform *v1.Platform @@ -352,7 +498,7 @@ func imageDigestForPlatform(ix *v1.IndexManifest, platform *v1.Platform) (string func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) { for _, m := range ix.Manifests { if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType { - if d, ok := m.Annotations[DockerReferenceDigest]; ok && d == imageDigest { + if d, ok := m.Annotations[att.DockerReferenceDigest]; ok && d == imageDigest { return m.Digest.String(), nil } } diff --git a/pkg/oci/resolver.go b/pkg/oci/resolver.go index 02d1f71..754c769 100644 --- a/pkg/oci/resolver.go +++ b/pkg/oci/resolver.go @@ -7,6 +7,10 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" ) +type AttestationManifests struct { + Manifests []*AttestationManifest +} + type AttestationManifest struct { // attestation image details Image v1.Image diff --git a/pkg/oci/types.go b/pkg/oci/types.go index 73d6e4b..91a4880 100644 --- a/pkg/oci/types.go +++ b/pkg/oci/types.go @@ -12,18 +12,17 @@ import ( ) const ( - DockerReferenceDigest = "vnd.docker.reference.digest" AttestationManifestType = "attestation-manifest" InTotoPredicateType = "in-toto.io/predicate-type" OciReferenceTarget = "org.opencontainers.image.ref.name" ) -type AttestationIndex struct { +type SubjectIndex struct { Index v1.ImageIndex Name string } -func AttestationIndexFromPath(path string) (*AttestationIndex, error) { +func SubjectIndexFromPath(path string) (*SubjectIndex, error) { wrapperIdx, err := layout.ImageIndexFromPath(path) if err != nil { return nil, fmt.Errorf("failed to load image index: %w", err) @@ -40,13 +39,13 @@ func AttestationIndexFromPath(path string) (*AttestationIndex, error) { if err != nil { return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err) } - return &AttestationIndex{ + return &SubjectIndex{ Index: idx, Name: imageName, }, nil } -func AttestationIndexFromRemote(image string) (*AttestationIndex, error) { +func SubjectIndexFromRemote(image string) (*SubjectIndex, error) { ref, err := name.ParseReference(image) if err != nil { log.Fatalf("Failed to parse image name: %v", err) @@ -61,7 +60,7 @@ func AttestationIndexFromRemote(image string) (*AttestationIndex, error) { if err != nil { return nil, fmt.Errorf("failed to pull image %s: %w", image, err) } - return &AttestationIndex{ + return &SubjectIndex{ Index: idx, Name: image, }, nil diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 3c52306..fa7ae91 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -158,6 +158,9 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, err } func loadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) { + if tufClient == nil { + return nil, fmt.Errorf("tuf client not set") + } filename := PolicyMappingFileName _, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename)) if err != nil { @@ -182,7 +185,8 @@ func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMa } // now search mirrors for _, mirror := range mappings.Mirrors { - if slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) && + if (slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) || + slices.Contains(mirror.Mirror.Domains, "*")) && strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) { for _, mapping := range mappings.Policies { if mapping.Id == mirror.PolicyId { diff --git a/test/testdata/local-policy-pass/mapping.yaml b/test/testdata/local-policy-pass/mapping.yaml index f729528..1aeb316 100644 --- a/test/testdata/local-policy-pass/mapping.yaml +++ b/test/testdata/local-policy-pass/mapping.yaml @@ -9,3 +9,8 @@ policies: description: Local test images files: - path: doi/policy.rego +mirrors: + - policy-id: test-images + mirror: + domains: ["*"] + prefix: "" diff --git a/test/testdata/unsigned-test-image/index.json b/test/testdata/unsigned-test-image/index.json index 026629c..8420714 100644 --- a/test/testdata/unsigned-test-image/index.json +++ b/test/testdata/unsigned-test-image/index.json @@ -1 +1,14 @@ -{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898","size":1607,"annotations":{"org.opencontainers.image.created":"2024-04-29T10:23:46Z","org.opencontainers.image.ref.name":"docker.io/library/test-image:test"}}]} \ No newline at end of file +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898", + "size": 1607, + "annotations": { + "org.opencontainers.image.created": "2024-04-29T10:23:46Z", + "org.opencontainers.image.ref.name": "docker.io/library/test-image:test" + } + } + ] +}