From 130e1f640b19ccd8ee0326bbcb915e929f9c80f1 Mon Sep 17 00:00:00 2001 From: James Carnegie Date: Mon, 17 Jun 2024 17:30:12 +0100 Subject: [PATCH] Support referrers using digest, not just tag (#55) * Support referrers using digest, not just tag * ParseRef and switch on type * Call DigestStr instead of String --- pkg/attestation/referrers_test.go | 37 +++++++++++++++-- pkg/oci/oci.go | 66 ++++++++++++++++++------------- 2 files changed, 72 insertions(+), 31 deletions(-) diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index 0b7173a..dc5a51a 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -13,7 +13,9 @@ import ( "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "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/remote" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -33,6 +35,7 @@ func TestAttestationReferenceTypes(t *testing.T) { for _, tc := range []struct { server *httptest.Server skipSubject bool + useDigest bool }{ { server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), @@ -44,6 +47,10 @@ func TestAttestationReferenceTypes(t *testing.T) { server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), skipSubject: true, }, + { + server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), + useDigest: true, + }, } { s := tc.server defer s.Close() @@ -65,7 +72,18 @@ func TestAttestationReferenceTypes(t *testing.T) { for _, platform := range platforms { // can eval policy in the normal way - resolver, err := oci.NewRegistryAttestationResolver(indexName, platform) + ref := indexName + if tc.useDigest { + options := oci.WithOptions(ctx, nil) + subjectRef, err := name.ParseReference(indexName) + require.NoError(t, err) + desc, err := remote.Index(subjectRef, options...) + require.NoError(t, err) + idxDigest, err := desc.Digest() + require.NoError(t, err) + ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String()) + } + resolver, err := oci.NewRegistryAttestationResolver(ref, platform) require.NoError(t, err) policyOpts := &policy.PolicyOptions{ @@ -74,9 +92,22 @@ func TestAttestationReferenceTypes(t *testing.T) { 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)) + if tc.useDigest { + p, err := oci.ParsePlatform(platform) + require.NoError(t, err) + options := oci.WithOptions(ctx, p) + subjectRef, err := name.ParseReference(indexName) + require.NoError(t, err) + desc, err := remote.Image(subjectRef, options...) + require.NoError(t, err) + subjectDigest, err := desc.Digest() + require.NoError(t, err) + ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String()) + } + referrersResolver, err := oci.NewReferrersAttestationResolver(ref, oci.WithPlatform(platform)) require.NoError(t, err) results, err = attest.Verify(ctx, policyOpts, referrersResolver) @@ -137,7 +168,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { require.NoError(t, err) for _, mf := range mfs2.Manifests { //skip signed/unsigned attestations - if mf.Annotations[attestation.DockerReferenceType] == "attestation-manifest" { + if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType { continue } // can evaluate policy using referrers in a different repo diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index cac2eef..54b663e 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -198,6 +198,7 @@ func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) { type ReferrersResolver struct { image string platform *v1.Platform + digest string referrersRepo string manifests []*AttestationManifest } @@ -236,27 +237,20 @@ func WithPlatform(platform string) func(*ReferrersResolver) error { 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) - } + subjectDigest, err := r.ImageDigest(ctx) + var referrersSubjectRef name.Digest if r.referrersRepo != "" { - referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest.String())) + referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest)) if err != nil { return fmt.Errorf("failed to create referrers reference: %w", err) } } else { - referrersSubjectRef = subjectRef.Context().Digest(subjectDigest.String()) + referrersSubjectRef = subjectRef.Context().Digest(subjectDigest) } referrersIndex, err := remote.Referrers(referrersSubjectRef) if err != nil { @@ -284,7 +278,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error { if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType { continue } - if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest.String() { + if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest { continue } attest := &AttestationManifest{ @@ -292,7 +286,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error { Image: attestationImage, Manifest: manifest, Descriptor: &m, - Digest: subjectDigest.String(), + Digest: subjectDigest, Platform: r.platform, } aManifests = append(aManifests, attest) @@ -331,14 +325,30 @@ func (r *ReferrersResolver) ImagePlatform() (*v1.Platform, error) { } 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 r.digest == "" { + subjectRef, err := name.ParseReference(r.image) + if err != nil { + return "", fmt.Errorf("failed to parse reference: %w", err) + } + switch t := subjectRef.(type) { + case name.Digest: + r.digest = t.DigestStr() + case name.Tag: + options := WithOptions(ctx, r.platform) + desc, err := remote.Image(t, 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) + } + r.digest = subjectDigest.String() + default: + return "", fmt.Errorf("unsupported reference type: %T", t) + } } - if len(r.manifests) == 0 { - return "", errors.New("no attestation manifests found") - } - return r.manifests[0].Digest, nil + return r.digest, nil } type RegistryResolver struct { @@ -390,20 +400,20 @@ func (r *RegistryResolver) Attestations(ctx context.Context, predicateType strin func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*AttestationManifest, error) { // we want to get to the image index, so ignoring platform for now - options := withOptions(ctx, nil) + options := WithOptions(ctx, nil) ref, err := name.ParseReference(image) if err != nil { return nil, fmt.Errorf("failed to parse reference: %w", err) } - desc, err := remote.Index(ref, options...) + index, err := remote.Index(ref, options...) if err != nil { - return nil, fmt.Errorf("failed to obtain index manifest: %w", err) + return nil, fmt.Errorf("failed to get index: %w", err) } - ix, err := desc.IndexManifest() + indexManifest, err := index.IndexManifest() if err != nil { - return nil, fmt.Errorf("failed to obtain index manifest: %w", err) + return nil, fmt.Errorf("failed to get index manifest: %w", err) } - digest, err := imageDigestForPlatform(ix, platform) + digest, err := imageDigestForPlatform(indexManifest, platform) if err != nil { return nil, fmt.Errorf("failed to obtain image for platform: %w", err) } @@ -412,7 +422,7 @@ func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Pl return nil, fmt.Errorf("failed to parse attestation reference: %w", err) } - attestationDigest, err := attestationDigestForDigest(ix, digest, "attestation-manifest") + attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest") if err != nil { return nil, fmt.Errorf("failed to obtain attestation for image: %w", err) } @@ -444,7 +454,7 @@ func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Pl return attest, nil } -func withOptions(ctx context.Context, platform *v1.Platform) []remote.Option { +func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option { // prepare options options := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}