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 <joel.kamp@docker.com>

* Don't error on no referrers

---------

Co-authored-by: Joel Kamp <joel.kamp@docker.com>
This commit is contained in:
James Carnegie
2024-07-22 18:17:12 +01:00
committed by GitHub
parent 5e68d94ad4
commit efb73f4cae
9 changed files with 131 additions and 132 deletions

2
go.mod
View File

@@ -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) // 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 ( require (
cloud.google.com/go v0.115.0 // indirect cloud.google.com/go v0.115.0 // indirect

4
go.sum
View File

@@ -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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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-20240722163910-ebe90246535d h1:5QaWAwKhslfqxEyMZY0ofvsbMJkMLcx5E30JFufMVj8=
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/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=

View File

@@ -220,7 +220,8 @@ func TestSimpleStatementSigning(t *testing.T) {
for _, img := range newImgs { for _, img := range newImgs {
mf, err := img.Manifest() mf, err := img.Manifest()
require.NoError(t, err) 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, subject.MediaType, mf.MediaType)
assert.Equal(t, empty, mf.Config.MediaType) assert.Equal(t, empty, mf.Config.MediaType)
assert.Equal(t, int64(2), mf.Config.Size) assert.Equal(t, int64(2), mf.Config.Size)

View File

@@ -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) { func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *AttestationManifestImageOptions) (v1.Image, error) {
newImg := empty.Image newImg := empty.Image
var err error 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 // 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 //TODO - recreate this bug and push upstream
for _, layer := range layers { for _, layer := range layers {
@@ -268,7 +270,11 @@ func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1
if opts.laxReferrers { if opts.laxReferrers {
newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json") newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json")
} else { } 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") 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) // 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! // see note above - must be added after the layers!
if !opts.skipSubject { if !opts.skipSubject {
subject.Platform = nil
newImg = mutate.Subject(newImg, *subject).(v1.Image) newImg = mutate.Subject(newImg, *subject).(v1.Image)
} }
if !opts.laxReferrers { if !opts.laxReferrers {

View File

@@ -40,7 +40,6 @@ func TestAttestationReferenceTypes(t *testing.T) {
name string name string
server *httptest.Server server *httptest.Server
referrersServer *httptest.Server referrersServer *httptest.Server
skipSubject bool
useDigest bool useDigest bool
referrersRepo string referrersRepo string
attestationSource config.AttestationStyle attestationSource config.AttestationStyle
@@ -50,16 +49,6 @@ func TestAttestationReferenceTypes(t *testing.T) {
name: "referrers support, defaults", name: "referrers support, defaults",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), 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", name: "use digest",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), 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) indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err) require.NoError(t, err)
outputRepo := indexName
if tc.referrersServer != nil { if tc.referrersServer != nil {
ru, err := url.Parse(s.URL) ru, err := url.Parse(s.URL)
require.NoError(t, err) require.NoError(t, err)
repo := fmt.Sprintf("%s/referrers", ru.Host) tc.referrersRepo = fmt.Sprintf("%s/referrers", ru.Host)
tc.referrersRepo = repo outputRepo = tc.referrersRepo
}
// sign all the statements in the index
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err) require.NoError(t, err)
// push subject image so that it can be resolved
require.NoError(t, err)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName) err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err) require.NoError(t, err)
for _, signedManifest := range signedManifests {
image, err := signedManifest.BuildAttestationImage(attestation.WithoutSubject(tc.skipSubject), attestation.WithReplacedLayers(true)) // upload referrers
output, err := oci.ParseImageSpec(outputRepo)
require.NoError(t, err) require.NoError(t, err)
err = mirror.PushImageToRegistry(image, fmt.Sprintf("%s:tag-does-not-matter", repo)) for _, attIdx := range signedManifests {
require.NoError(t, err) err = mirror.SaveReferrers(attIdx, []*oci.ImageSpec{output})
}
} 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)
require.NoError(t, err) require.NoError(t, err)
} }
@@ -170,8 +158,6 @@ func TestAttestationReferenceTypes(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome) assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
if !tc.skipSubject {
// can evaluate policy using referrers
if tc.useDigest { if tc.useDigest {
p, err := oci.ParsePlatform(platform) p, err := oci.ParsePlatform(platform)
require.NoError(t, err) require.NoError(t, err)
@@ -184,13 +170,12 @@ func TestAttestationReferenceTypes(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String()) ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String())
} }
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform)) src, err = oci.ParseImageSpec(ref, oci.WithPlatform(platform))
require.NoError(t, err) require.NoError(t, err)
results, err = attest.Verify(ctx, src, policyOpts) results, err = attest.Verify(ctx, src, policyOpts)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome) assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
} }
}
}) })
} }
} }
@@ -335,7 +320,8 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
imf, err := idx.IndexManifest() imf, err := idx.IndexManifest()
require.NoError(t, err) require.NoError(t, err)
for _, m := range imf.Manifests { 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")
} }
} }
} }

View File

@@ -57,7 +57,7 @@ type Envelope struct {
type Signature struct { type Signature struct {
KeyID string `json:"keyid"` KeyID string `json:"keyid"`
Sig string `json:"sig"` Sig string `json:"sig"`
Extension Extension `json:"extension"` Extension Extension `json:"extension,omitempty"`
} }
type Extension struct { type Extension struct {
Kind string `json:"kind"` Kind string `json:"kind"`

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/attestation"
att "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) { func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
var envs []*att.Envelope var envs []*att.Envelope
for _, attestationLayer := range r.AttestationManifest.OriginalLayers { dsseMediaType, err := attestation.DSSEMediaType(predicateType)
if attestationLayer.Annotations[attestation.InTotoPredicateType] != predicateType { if err != nil {
continue return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
} }
for _, attestationLayer := range r.AttestationManifest.OriginalLayers {
mt, err := attestationLayer.Layer.MediaType() mt, err := attestationLayer.Layer.MediaType()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err) return nil, fmt.Errorf("failed to get layer media type: %w", err)
} }
mts := string(mt) mts := string(mt)
if !strings.HasSuffix(mts, "+dsse") { if mts != dsseMediaType {
continue continue
} }
var env = new(att.Envelope) var env = new(att.Envelope)

View File

@@ -49,14 +49,16 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
func ExtractEnvelopes(manifest *attestation.AttestationManifest, predicateType string) ([]*att.Envelope, error) { func ExtractEnvelopes(manifest *attestation.AttestationManifest, predicateType string) ([]*att.Envelope, error) {
var envs []*att.Envelope 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 { for _, attestationLayer := range manifest.OriginalLayers {
mt, err := attestationLayer.Layer.MediaType() mt, err := attestationLayer.Layer.MediaType()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err) return nil, fmt.Errorf("failed to get layer media type: %w", err)
} }
if (strings.HasPrefix(string(mt), "application/vnd.in-toto.")) && if string(mt) == dsseMediaType {
strings.HasSuffix(string(mt), "+dsse") &&
attestationLayer.Annotations[att.InTotoPredicateType] == predicateType {
reader, err := attestationLayer.Layer.Uncompressed() reader, err := attestationLayer.Layer.Uncompressed()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err) return nil, fmt.Errorf("failed to get layer contents: %w", err)

View File

@@ -8,12 +8,10 @@ import (
att "github.com/docker/attest/pkg/attestation" att "github.com/docker/attest/pkg/attestation"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/pkg/errors"
) )
type ReferrersResolver struct { type ReferrersResolver struct {
referrersRepo string referrersRepo string
manifests []*attestation.AttestationManifest
ImageDetailsResolver ImageDetailsResolver
} }
@@ -37,55 +35,67 @@ func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
} }
} }
func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error { func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*attestation.AttestationManifest,
if r.manifests == nil { 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)
}
imageName, err := r.ImageName(ctx) imageName, err := r.ImageName(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get image name: %w", err) return nil, fmt.Errorf("failed to get image name: %w", err)
} }
subjectRef, err := name.ParseReference(imageName) subjectRef, err := name.ParseReference(imageName)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse reference: %w", err) return nil, fmt.Errorf("failed to parse reference: %w", err)
} }
desc, err := r.ImageDescriptor(ctx) desc, err := r.ImageDescriptor(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get descriptor: %w", err) return nil, fmt.Errorf("failed to get descriptor: %w", err)
} }
subjectDigest := desc.Digest.String() subjectDigest := desc.Digest.String()
if err != nil { if err != nil {
return fmt.Errorf("failed to get digest: %w", err) return nil, fmt.Errorf("failed to get digest: %w", err)
} }
var referrersSubjectRef name.Digest var referrersSubjectRef name.Digest
if r.referrersRepo != "" { if r.referrersRepo != "" {
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest)) referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest))
if err != nil { if err != nil {
return fmt.Errorf("failed to create referrers reference: %w", err) return nil, fmt.Errorf("failed to create referrers reference: %w", err)
} }
} else { } else {
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest) referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
} }
// TODO - search for in-toto artifact type options := WithOptions(ctx, nil)
referrersIndex, err := remote.Referrers(referrersSubjectRef) options = append(options, remote.WithFilter("artifactType", dsseMediaType))
referrersIndex, err := remote.Referrers(referrersSubjectRef, options...)
if err != nil { if err != nil {
return fmt.Errorf("failed to get referrers: %w", err) return nil, fmt.Errorf("failed to get referrers: %w", err)
} }
referrersIndexManifest, err := referrersIndex.IndexManifest() referrersIndexManifest, err := referrersIndex.IndexManifest()
if err != nil { if err != nil {
return fmt.Errorf("failed to get index manifest: %w", err) return nil, 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) aManifests := make([]*attestation.AttestationManifest, 0)
for _, m := range referrersIndexManifest.Manifests { for _, m := range referrersIndexManifest.Manifests {
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String()) remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
attestationImage, err := remote.Image(remoteRef) attestationImage, err := remote.Image(remoteRef)
if err != nil { if err != nil {
return fmt.Errorf("failed to get referred image: %w", err) return nil, fmt.Errorf("failed to get referred image: %w", err)
} }
layers, err := attestation.GetAttestationsFromImage(attestationImage) layers, err := attestation.GetAttestationsFromImage(attestationImage)
if err != nil { if err != nil {
return fmt.Errorf("failed to get attestations from image: %w", err) 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{ attest := &attestation.AttestationManifest{
SubjectName: imageName, SubjectName: imageName,
@@ -95,22 +105,16 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
} }
aManifests = append(aManifests, attest) aManifests = append(aManifests, attest)
} }
return aManifests, nil
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) { 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 { if err != nil {
return nil, fmt.Errorf("failed to resolve attestations: %w", err) return nil, fmt.Errorf("failed to resolve attestations: %w", err)
} }
var envs []*att.Envelope var envs []*att.Envelope
for _, attest := range r.manifests { for _, attest := range manifests {
es, err := ExtractEnvelopes(attest, predicateType) es, err := ExtractEnvelopes(attest, predicateType)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to extract envelopes: %w", err) return nil, fmt.Errorf("failed to extract envelopes: %w", err)