From efb73f4cae4d8bc6d183fca6bb49e9e9f266d07e Mon Sep 17 00:00:00 2001 From: James Carnegie Date: Mon, 22 Jul 2024 18:17:12 +0100 Subject: [PATCH] Use DSSE artifactType in referrers (#95) * bug: Use DSSE media types for artifactType * Don't serialize DSSE extension if not present * Update pkg/attestation/types.go Co-authored-by: Joel Kamp * Don't error on no referrers --------- Co-authored-by: Joel Kamp --- go.mod | 2 +- go.sum | 4 +- pkg/attest/sign_test.go | 3 +- pkg/attestation/attestation.go | 11 ++- pkg/attestation/referrers_test.go | 82 ++++++++--------- pkg/attestation/types.go | 2 +- pkg/oci/layout.go | 11 ++- pkg/oci/oci.go | 8 +- pkg/oci/referrers.go | 140 +++++++++++++++--------------- 9 files changed, 131 insertions(+), 132 deletions(-) diff --git a/go.mod b/go.mod index 9368916..99df0d6 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( ) // fork of a fork (in case it goes away) with changes to support ArtifactType (https://github.com/google/go-containerregistry/pull/1931) -replace github.com/google/go-containerregistry v0.20.0 => github.com/kipz/go-containerregistry v0.0.0-20240719153227-9edd0a0441c8 +replace github.com/google/go-containerregistry v0.20.0 => github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d require ( cloud.google.com/go v0.115.0 // indirect diff --git a/go.sum b/go.sum index 277164e..90d5499 100644 --- a/go.sum +++ b/go.sum @@ -415,8 +415,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kipz/go-containerregistry v0.0.0-20240719153227-9edd0a0441c8 h1:jxznpXHtDmo7x90Fc26H1FEmcdQ0K6PF13OgXcrkcSc= -github.com/kipz/go-containerregistry v0.0.0-20240719153227-9edd0a0441c8/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d h1:5QaWAwKhslfqxEyMZY0ofvsbMJkMLcx5E30JFufMVj8= +github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index 89526a0..7332d56 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -220,7 +220,8 @@ func TestSimpleStatementSigning(t *testing.T) { for _, img := range newImgs { mf, err := img.Manifest() require.NoError(t, err) - assert.Equal(t, "application/vnd.in-toto+json", mf.ArtifactType) + 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) diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index aa3d9a2..ef48ae2 100644 --- a/pkg/attestation/attestation.go +++ b/pkg/attestation/attestation.go @@ -250,7 +250,9 @@ func (manifest *AttestationManifest) BuildReferringArtifacts() ([]v1.Image, erro func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *AttestationManifestImageOptions) (v1.Image, error) { newImg := empty.Image var err error - + if len(layers) == 0 { + return nil, fmt.Errorf("no layers supplied to build image") + } // NB: if we add the subject before the layers, it does not end up being computed/serialised in the output for some reason //TODO - recreate this bug and push upstream for _, layer := range layers { @@ -268,7 +270,11 @@ func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1 if opts.laxReferrers { newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json") } else { - newImg = mutate.ArtifactType(newImg, intoto.PayloadType) + dsseMediatType, err := DSSEMediaType(layers[0].Statement.PredicateType) + if err != nil { + return nil, fmt.Errorf("failed to get DSSE media type: %w", err) + } + newImg = mutate.ArtifactType(newImg, dsseMediatType) newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.empty.v1+json") } // we need to set this even when we set the artifact type otherwise things break (even the go-container-registry client) @@ -277,6 +283,7 @@ func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1 // see note above - must be added after the layers! if !opts.skipSubject { + subject.Platform = nil newImg = mutate.Subject(newImg, *subject).(v1.Image) } if !opts.laxReferrers { diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index c77eaf8..0e05172 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -40,7 +40,6 @@ func TestAttestationReferenceTypes(t *testing.T) { name string server *httptest.Server referrersServer *httptest.Server - skipSubject bool useDigest bool referrersRepo string attestationSource config.AttestationStyle @@ -50,16 +49,6 @@ func TestAttestationReferenceTypes(t *testing.T) { name: "referrers support, defaults", server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), }, - { - name: "no referrers support", - server: httptest.NewServer(registry.New()), - }, - { - name: "attached attestations", - server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), - skipSubject: true, - attestationSource: config.AttestationStyleAttached, - }, { name: "use digest", server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), @@ -110,28 +99,27 @@ func TestAttestationReferenceTypes(t *testing.T) { indexName := fmt.Sprintf("%s/repo:root", u.Host) require.NoError(t, err) + outputRepo := indexName if tc.referrersServer != nil { ru, err := url.Parse(s.URL) require.NoError(t, err) - repo := fmt.Sprintf("%s/referrers", ru.Host) - tc.referrersRepo = repo - signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) - require.NoError(t, err) - err = mirror.PushIndexToRegistry(attIdx.Index, indexName) - require.NoError(t, err) - for _, signedManifest := range signedManifests { - image, err := signedManifest.BuildAttestationImage(attestation.WithoutSubject(tc.skipSubject), attestation.WithReplacedLayers(true)) - require.NoError(t, err) - err = mirror.PushImageToRegistry(image, fmt.Sprintf("%s:tag-does-not-matter", repo)) - require.NoError(t, err) - } - } else { - signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) - require.NoError(t, err) - signedIndex := attIdx.Index - signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(true), attestation.WithoutSubject(tc.skipSubject)) - require.NoError(t, err) - err = mirror.PushIndexToRegistry(signedIndex, indexName) + tc.referrersRepo = fmt.Sprintf("%s/referrers", ru.Host) + outputRepo = tc.referrersRepo + } + // sign all the statements in the index + signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) + require.NoError(t, err) + + // push subject image so that it can be resolved + require.NoError(t, err) + err = mirror.PushIndexToRegistry(attIdx.Index, indexName) + require.NoError(t, err) + + // upload referrers + output, err := oci.ParseImageSpec(outputRepo) + require.NoError(t, err) + for _, attIdx := range signedManifests { + err = mirror.SaveReferrers(attIdx, []*oci.ImageSpec{output}) require.NoError(t, err) } @@ -170,26 +158,23 @@ func TestAttestationReferenceTypes(t *testing.T) { require.NoError(t, err) assert.Equal(t, attest.OutcomeSuccess, results.Outcome) - if !tc.skipSubject { - // can evaluate policy using referrers - 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()) - } - src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform)) + if tc.useDigest { + p, err := oci.ParsePlatform(platform) require.NoError(t, err) - results, err = attest.Verify(ctx, src, policyOpts) + options := oci.WithOptions(ctx, p) + subjectRef, err := name.ParseReference(indexName) require.NoError(t, err) - assert.Equal(t, attest.OutcomeSuccess, results.Outcome) + 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()) } + src, err = oci.ParseImageSpec(ref, oci.WithPlatform(platform)) + require.NoError(t, err) + results, err = attest.Verify(ctx, src, policyOpts) + require.NoError(t, err) + assert.Equal(t, attest.OutcomeSuccess, results.Outcome) } }) } @@ -335,7 +320,8 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) { imf, err := idx.IndexManifest() require.NoError(t, err) for _, m := range imf.Manifests { - assert.Equal(t, "application/vnd.in-toto+json", m.ArtifactType) + assert.Contains(t, m.ArtifactType, "application/vnd.in-toto") + assert.Contains(t, m.ArtifactType, "+dsse") } } } diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index de55fe8..3aa6cea 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -57,7 +57,7 @@ type Envelope struct { type Signature struct { KeyID string `json:"keyid"` Sig string `json:"sig"` - Extension Extension `json:"extension"` + Extension Extension `json:"extension,omitempty"` } type Extension struct { Kind string `json:"kind"` diff --git a/pkg/oci/layout.go b/pkg/oci/layout.go index 4793ecb..eecc79e 100644 --- a/pkg/oci/layout.go +++ b/pkg/oci/layout.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "github.com/docker/attest/pkg/attestation" att "github.com/docker/attest/pkg/attestation" @@ -44,17 +43,17 @@ func (r *OCILayoutResolver) fetchAttestationManifest() (*attestation.Attestation func (r *OCILayoutResolver) Attestations(ctx context.Context, 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 r.AttestationManifest.OriginalLayers { - if attestationLayer.Annotations[attestation.InTotoPredicateType] != predicateType { - continue - } - mt, err := attestationLayer.Layer.MediaType() if err != nil { return nil, fmt.Errorf("failed to get layer media type: %w", err) } mts := string(mt) - if !strings.HasSuffix(mts, "+dsse") { + if mts != dsseMediaType { continue } var env = new(att.Envelope) diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index b53fd22..438348f 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -49,14 +49,16 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option { func ExtractEnvelopes(manifest *attestation.AttestationManifest, 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 (strings.HasPrefix(string(mt), "application/vnd.in-toto.")) && - strings.HasSuffix(string(mt), "+dsse") && - attestationLayer.Annotations[att.InTotoPredicateType] == predicateType { + if string(mt) == dsseMediaType { reader, err := attestationLayer.Layer.Uncompressed() if err != nil { return nil, fmt.Errorf("failed to get layer contents: %w", err) diff --git a/pkg/oci/referrers.go b/pkg/oci/referrers.go index 3149bcf..217f381 100644 --- a/pkg/oci/referrers.go +++ b/pkg/oci/referrers.go @@ -8,12 +8,10 @@ import ( att "github.com/docker/attest/pkg/attestation" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/pkg/errors" ) type ReferrersResolver struct { referrersRepo string - manifests []*attestation.AttestationManifest ImageDetailsResolver } @@ -37,80 +35,86 @@ func WithReferrersRepo(repo string) func(*ReferrersResolver) error { } } -func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error { - if r.manifests == nil { - imageName, err := r.ImageName(ctx) - if err != nil { - return fmt.Errorf("failed to get image name: %w", err) - } - subjectRef, err := name.ParseReference(imageName) - if err != nil { - return fmt.Errorf("failed to parse reference: %w", err) - } - desc, err := r.ImageDescriptor(ctx) - if err != nil { - return fmt.Errorf("failed to get descriptor: %w", err) - } - subjectDigest := desc.Digest.String() - if err != nil { - return fmt.Errorf("failed to get digest: %w", err) - } - var referrersSubjectRef name.Digest - if r.referrersRepo != "" { - 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) - } - // TODO - search for in-toto artifact type - 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([]*attestation.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) - } - layers, err := attestation.GetAttestationsFromImage(attestationImage) - if err != nil { - return fmt.Errorf("failed to get attestations from image: %w", err) - } - attest := &attestation.AttestationManifest{ - SubjectName: imageName, - OriginalLayers: layers, - OriginalDescriptor: &m, - SubjectDescriptor: desc, - } - aManifests = append(aManifests, attest) - } - - if len(aManifests) == 0 { - return errors.New("no attestation manifests found") - } - r.manifests = aManifests +func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*attestation.AttestationManifest, + error) { + dsseMediaType, err := attestation.DSSEMediaType(predicateType) + if err != nil { + return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err) } - return nil + imageName, err := r.ImageName(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get image name: %w", err) + } + subjectRef, err := name.ParseReference(imageName) + if err != nil { + return nil, fmt.Errorf("failed to parse reference: %w", err) + } + desc, err := r.ImageDescriptor(ctx) + if err != nil { + 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", r.referrersRepo, 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 = append(options, remote.WithFilter("artifactType", dsseMediaType)) + referrersIndex, err := remote.Referrers(referrersSubjectRef, options...) + if err != nil { + return nil, fmt.Errorf("failed to get referrers: %w", err) + } + referrersIndexManifest, err := referrersIndex.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to get index manifest: %w", err) + } + aManifests := make([]*attestation.AttestationManifest, 0) + for _, m := range referrersIndexManifest.Manifests { + remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String()) + attestationImage, err := remote.Image(remoteRef) + if err != nil { + return nil, fmt.Errorf("failed to get referred image: %w", err) + } + layers, err := attestation.GetAttestationsFromImage(attestationImage) + if err != nil { + return nil, fmt.Errorf("failed to get attestations from image: %w", err) + } + if len(layers) != 1 { + return nil, fmt.Errorf("expected exactly one layer, got %d", len(layers)) + } + mt, err := layers[0].Layer.MediaType() + if err != nil { + return nil, fmt.Errorf("failed to get layer media type: %w", err) + } + if string(mt) != dsseMediaType { + return nil, fmt.Errorf("expected layer media type %s, got %s", dsseMediaType, mt) + } + attest := &attestation.AttestationManifest{ + SubjectName: imageName, + OriginalLayers: layers, + OriginalDescriptor: &m, + SubjectDescriptor: desc, + } + aManifests = append(aManifests, attest) + } + return aManifests, nil } func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) { - err := r.resolveAttestations(ctx) + manifests, err := r.resolveAttestations(ctx, predicateType) if err != nil { return nil, fmt.Errorf("failed to resolve attestations: %w", err) } var envs []*att.Envelope - for _, attest := range r.manifests { + for _, attest := range manifests { es, err := ExtractEnvelopes(attest, predicateType) if err != nil { return nil, fmt.Errorf("failed to extract envelopes: %w", err)