diff --git a/go.mod b/go.mod index e55febf..f7b3ff6 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,8 @@ require ( sigs.k8s.io/yaml v1.4.0 ) +replace github.com/google/go-containerregistry v0.19.2 => github.com/kipz/go-containerregistry v0.0.0-20240423201245-bf57eace21f2 + require ( cloud.google.com/go v0.115.0 // indirect cloud.google.com/go/auth v0.7.0 // indirect diff --git a/pkg/attest/example_sign_test.go b/pkg/attest/example_sign_test.go index db39cc9..2d006b2 100644 --- a/pkg/attest/example_sign_test.go +++ b/pkg/attest/example_sign_test.go @@ -27,7 +27,7 @@ func ExampleSign_remote() { // configure signing options opts := &attestation.SigningOptions{ - Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place + SkipTL: true, // skip trust logging to a transparency log } // load image index with unsigned attestation-manifests diff --git a/pkg/attest/sign.go b/pkg/attest/sign.go index 5dcca0f..c671767 100644 --- a/pkg/attest/sign.go +++ b/pkg/attest/sign.go @@ -18,7 +18,7 @@ func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVe } // sign every attestation layer in each manifest for _, manifest := range attestationManifests { - for _, layer := range manifest.AttestationImage.Layers { + for _, layer := range manifest.AttestationImage.OriginalLayers { err = manifest.AddAttestation(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 2759efb..dfd4cb8 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -53,15 +53,13 @@ func TestSignVerifyOCILayout(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { outputLayout := test.CreateTempDir(t, "", TestTempDir) - opts := &attestation.SigningOptions{ - Replace: tc.replace, - } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(tc.TestImage) require.NoError(t, err) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) require.NoError(t, err) signedIndex := attIdx.Index - signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests) + signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests, attestation.WithReplacedLayers(tc.replace)) require.NoError(t, err) // output signed attestations idx := v1.ImageIndex(empty.Index) @@ -102,6 +100,7 @@ func TestSignVerifyOCILayout(t *testing.T) { } func TestAddSignedLayerAnnotations(t *testing.T) { + ctx, signer := test.Setup(t) testCases := []struct { name string replace bool @@ -115,12 +114,14 @@ func TestAddSignedLayerAnnotations(t *testing.T) { data := []byte("signed") testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType)) mediaType := types.OCIManifestSchema1 - opts := &attestation.SigningOptions{ - Replace: tc.replace, - } + opts := &attestation.SigningOptions{} originalLayer := &attestation.AttestationLayer{ - Layer: testLayer, - Statement: &intoto.Statement{}, + Layer: testLayer, + Statement: &intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + PredicateType: attestation.VSAPredicateType, + }, + }, Annotations: map[string]string{"test": "test"}, } @@ -129,15 +130,15 @@ func TestAddSignedLayerAnnotations(t *testing.T) { MediaType: mediaType, }, AttestationImage: &attestation.AttestationImage{ - Image: empty.Image, - Layers: []*attestation.AttestationLayer{ + OriginalLayers: []*attestation.AttestationLayer{ originalLayer, }, }, SubjectDescriptor: &v1.Descriptor{}, } - err := manifest.AddOrReplaceLayer(originalLayer, opts) - newImg := manifest.AttestationImage.Image + err := manifest.AddAttestation(ctx, signer, originalLayer.Statement, opts) + + newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace)) require.NoError(t, err) mf, _ := newImg.RawManifest() type Annotations struct { diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index f7e049a..42f3efc 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -176,7 +176,7 @@ func NewAttestationManifest(subject *v1.Descriptor) (*attestation.AttestationMan MediaType: "application/vnd.oci.image.manifest.v1+json", }, AttestationImage: &attestation.AttestationImage{ - Layers: []*attestation.AttestationLayer{}, + OriginalLayers: []*attestation.AttestationLayer{}, }, SubjectDescriptor: subject, }, nil diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 57c4305..dd93f84 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -77,9 +77,7 @@ func TestVSA(t *testing.T) { // setup an image with signed attestations outputLayout := test.CreateTempDir(t, "", TestTempDir) - opts := &attestation.SigningOptions{ - Replace: true, - } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(UnsignedTestImage) assert.NoError(t, err) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) @@ -136,15 +134,13 @@ func TestVerificationFailure(t *testing.T) { // setup an image with signed attestations outputLayout := test.CreateTempDir(t, "", TestTempDir) - opts := &attestation.SigningOptions{ - Replace: true, - } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(UnsignedTestImage) assert.NoError(t, err) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) require.NoError(t, err) signedIndex := attIdx.Index - signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests) + signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests, attestation.WithReplacedLayers(true)) require.NoError(t, err) // output signed attestations @@ -215,14 +211,13 @@ func TestSignVerify(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { opts := &attestation.SigningOptions{ - Replace: true, - SkipTL: tc.signTL, + SkipTL: tc.signTL, } signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) require.NoError(t, err) signedIndex := attIdx.Index - signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests) + signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests, attestation.WithReplacedLayers(true)) require.NoError(t, err) imageName := tc.imageName diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index b51426b..0636142 100644 --- a/pkg/attestation/attestation.go +++ b/pkg/attestation/attestation.go @@ -48,8 +48,7 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManife OriginalDescriptor: &desc, SubjectDescriptor: subject, AttestationImage: &AttestationImage{ - Layers: attestationLayers, - Descriptor: &desc}}) + OriginalLayers: attestationLayers}}) } } return attestationManifests, nil @@ -97,11 +96,11 @@ func (manifest *AttestationManifest) AddAttestation(ctx context.Context, signer if err != nil { return fmt.Errorf("failed to create signed layer: %w", err) } - return addLayerToImage(manifest, layer, opts) + manifest.AttestationImage.signedLayers = append(manifest.AttestationImage.signedLayers, layer) + return nil } func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*AttestationLayer, error) { - // sign the statement env, err := SignInTotoStatement(ctx, statement, signer, opts) if err != nil { @@ -138,21 +137,18 @@ func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signe return env, nil } -func addLayerToImage( +func AddImageToIndex( + idx v1.ImageIndex, manifest *AttestationManifest, - layer *AttestationLayer, - opts *SigningOptions) error { + options ...func(*AttestationManifestImageOptions) error) (v1.ImageIndex, error) { + image, err := manifest.BuildAttestationImage(options...) - err := manifest.AddOrReplaceLayer(layer, opts) if err != nil { - return fmt.Errorf("failed to add signed layers: %w", err) + return nil, fmt.Errorf("failed to build image: %w", err) } - if !opts.SkipSubject { - manifest.AttestationImage.Image = mutate.Subject(manifest.AttestationImage.Image, *manifest.SubjectDescriptor).(v1.Image) - } - newDesc, err := partial.Descriptor(manifest.AttestationImage.Image) + newDesc, err := partial.Descriptor(image) if err != nil { - return fmt.Errorf("failed to get descriptor: %w", err) + return nil, fmt.Errorf("failed to get descriptor: %w", err) } newDesc.Platform = &v1.Platform{ Architecture: "unknown", @@ -160,77 +156,165 @@ func addLayerToImage( } newDesc.MediaType = manifest.OriginalDescriptor.MediaType newDesc.Annotations = manifest.OriginalDescriptor.Annotations - manifest.AttestationImage.Descriptor = newDesc - return nil -} - -// AddOrReplaceLayer adds signed layers to a new or existing attestation image -// NOTE: the pointers attestation.AttestationLayer.Statement are compared when replacing, -// so make sure you are signing a layer extracted from the original attestation-manifest image! -func (manifest *AttestationManifest) AddOrReplaceLayer(signedLayer *AttestationLayer, opts *SigningOptions) error { - var err error - // always create a new image from all the layers - newImg := empty.Image - //TODO: maybe we don't need these anymore - newImg = mutate.Annotations(newImg, map[string]string{ - DockerReferenceType: AttestationManifestType, - DockerReferenceDigest: manifest.SubjectDescriptor.Digest.String(), - }).(v1.Image) - - newImg = mutate.MediaType(newImg, manifest.OriginalDescriptor.MediaType) - newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json") - add := mutate.Addendum{ - Layer: signedLayer.Layer, - Annotations: signedLayer.Annotations, - } - newImg, err = mutate.Append(newImg, add) - if err != nil { - return fmt.Errorf("failed to add signed layer to image: %w", err) - } - newLayers := make([]*AttestationLayer, 0) - for _, existingLayer := range manifest.AttestationImage.Layers { - // if we're replacing, then we don't add it back in - if existingLayer.Statement == signedLayer.Statement && opts.Replace { - continue - } - // add original layer back in - add := mutate.Addendum{ - Layer: existingLayer.Layer, - Annotations: existingLayer.Annotations, - } - newImg, err = mutate.Append(newImg, add) - newLayers = append(newLayers, existingLayer) - if err != nil { - return fmt.Errorf("failed to add layer to image: %w", err) - } - } - manifest.AttestationImage.Layers = append(newLayers, signedLayer) - manifest.AttestationImage.Image = newImg - return nil -} - -func AddImageToIndex( - idx v1.ImageIndex, - manifest *AttestationManifest, -) (v1.ImageIndex, error) { idx = mutate.RemoveManifests(idx, match.Digests(manifest.OriginalDescriptor.Digest)) idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ - Add: manifest.AttestationImage.Image, - Descriptor: *manifest.AttestationImage.Descriptor, + Add: image, + Descriptor: *newDesc, }) return idx, nil } -func AddImagesToIndex( - idx v1.ImageIndex, - manifests []*AttestationManifest, -) (v1.ImageIndex, error) { - for _, manifest := range manifests { - var err error - idx, err = AddImageToIndex(idx, manifest) +func AddImagesToIndex(idx v1.ImageIndex, manifest []*AttestationManifest, options ...func(*AttestationManifestImageOptions) error) (v1.ImageIndex, error) { + var err error + for _, m := range manifest { + idx, err = AddImageToIndex(idx, m, options...) if err != nil { return nil, fmt.Errorf("failed to add image to index: %w", err) } } return idx, nil } + +func newOptions(options ...func(*AttestationManifestImageOptions) error) (*AttestationManifestImageOptions, error) { + opts := &AttestationManifestImageOptions{} + for _, opt := range options { + err := opt(opts) + if err != nil { + return nil, err + } + } + return opts, nil +} + +func WithoutSubject(skipSubject bool) func(*AttestationManifestImageOptions) error { + return func(r *AttestationManifestImageOptions) error { + r.skipSubject = skipSubject + return nil + } +} + +func WithReplacedLayers(replaceLayers bool) func(*AttestationManifestImageOptions) error { + return func(r *AttestationManifestImageOptions) error { + r.replaceLayers = replaceLayers + return nil + } +} + +// build an image with signed attestations, optionally replacing existing layers with signed layers +func (manifest *AttestationManifest) BuildAttestationImage(options ...func(*AttestationManifestImageOptions) error) (v1.Image, error) { + // always create a new image from all the layers + opts, err := newOptions(options...) + if err != nil { + return nil, fmt.Errorf("failed to create options: %w", err) + } + resultLayers := manifest.AttestationImage.signedLayers + for _, existingLayer := range manifest.AttestationImage.OriginalLayers { + var found bool + for _, signedLayer := range manifest.AttestationImage.signedLayers { + if existingLayer.Statement == signedLayer.Statement { + found = true + // copy over original annotations + for k, v := range existingLayer.Annotations { + signedLayer.Annotations[k] = v + } + break + } + } + //add existing layers if they've not been signed or we're not replacing them + if !found || !opts.replaceLayers { + resultLayers = append(resultLayers, existingLayer) + } + } + + newImg, err := buildImage(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts) + if err != nil { + return nil, fmt.Errorf("failed to build image: %w", err) + } + return newImg, nil +} + +// build an image per attestation (layer) suitable for use as Referrers +func (manifest *AttestationManifest) BuildReferringArtifacts() ([]v1.Image, error) { + var images []v1.Image + for _, layer := range manifest.AttestationImage.signedLayers { + opts := &AttestationManifestImageOptions{ + strictReferrers: true, + } + newImg, err := buildImage([]*AttestationLayer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts) + if err != nil { + return nil, fmt.Errorf("failed to build image: %w", err) + } + images = append(images, newImg) + } + return images, nil +} + +// build and image containing only layers +func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *AttestationManifestImageOptions) (v1.Image, error) { + newImg := empty.Image + var err error + + // 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 { + add := mutate.Addendum{ + Layer: layer.Layer, + Annotations: layer.Annotations, + } + newImg, err = mutate.Append(newImg, add) + if err != nil { + return nil, fmt.Errorf("failed to add layer to image: %w", err) + } + } + + if opts.strictReferrers { + newImg = mutate.ArtifactType(newImg, intoto.PayloadType) + newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.empty.v1+json") + + } else { + newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json") + } + // we need to set this even when we set the artifact type otherwise things break (even the go-container-registry client) + // even though it's allowed to be empty by spec when setting artifact type + newImg = mutate.MediaType(newImg, manifest.MediaType) + + // see note above - must be added after the layers! + if !opts.skipSubject { + newImg = mutate.Subject(newImg, *subject).(v1.Image) + } + if opts.strictReferrers { + // as per https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor + newImg = &EmptyConfigImage{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() + 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("e30="), + } + 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) +} diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index e37cd94..61b8aef 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -102,9 +102,7 @@ func TestAttestationReferenceTypes(t *testing.T) { require.NoError(t, err) opts := &attestation.SigningOptions{ - Replace: true, - SkipSubject: tc.skipSubject, - SkipTL: true, + SkipTL: true, } attIdx, err := oci.IndexFromPath(UnsignedTestImage) require.NoError(t, err) @@ -121,15 +119,17 @@ func TestAttestationReferenceTypes(t *testing.T) { require.NoError(t, err) err = mirror.PushIndexToRegistry(attIdx.Index, indexName) require.NoError(t, err) - for _, img := range signedManifests { - err = mirror.PushImageToRegistry(img.AttestationImage.Image, fmt.Sprintf("%s:tag-does-not-matter", repo)) + 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.AddImagesToIndex(signedIndex, signedManifests) + signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests, attestation.WithReplacedLayers(true), attestation.WithoutSubject(tc.skipSubject)) require.NoError(t, err) err = mirror.PushIndexToRegistry(signedIndex, indexName) require.NoError(t, err) @@ -225,8 +225,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { require.NoError(t, err) opts := &attestation.SigningOptions{ - Replace: true, - SkipTL: true, + SkipTL: true, } attIdx, err := oci.IndexFromPath(UnsignedTestImage) require.NoError(t, err) @@ -239,9 +238,11 @@ func TestReferencesInDifferentRepo(t *testing.T) { require.NoError(t, err) // push signed attestation image to the ref server - for _, img := range signedManifests { + for _, signedManifest := range signedManifests { // push references using subject-digest.att convention - err = mirror.PushImageToRegistry(img.AttestationImage.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName)) + image, err := signedManifest.BuildAttestationImage() + require.NoError(t, err) + err = mirror.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName)) require.NoError(t, err) refServer := tc.refServer diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index bf428e7..760564f 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -31,9 +31,8 @@ type AttestationLayer struct { } type AttestationImage struct { - Descriptor *v1.Descriptor - Layers []*AttestationLayer - Image v1.Image + OriginalLayers []*AttestationLayer + signedLayers []*AttestationLayer } type AttestationManifest struct { @@ -47,6 +46,13 @@ type AttestationManifest struct { SubjectDescriptor *v1.Descriptor } +type AttestationManifestImageOptions struct { + // how to output the image + skipSubject bool + replaceLayers bool + strictReferrers bool +} + // the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged type Envelope struct { PayloadType string `json:"payloadType"` @@ -78,12 +84,8 @@ type VerifyOptions struct { } type SigningOptions struct { - // 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/mirror/mirror.go b/pkg/mirror/mirror.go index 6ba2423..3f53484 100644 --- a/pkg/mirror/mirror.go +++ b/pkg/mirror/mirror.go @@ -37,7 +37,7 @@ func PushImageToRegistry(image v1.Image, imageName string) error { return remote.Write(ref, image, oci.MultiKeychainOption()) } -func PushIndexToRegistry(image v1.ImageIndex, imageName string) error { +func PushIndexToRegistry(index v1.ImageIndex, imageName string) error { // Parse the index name ref, err := name.ParseReference(imageName) if err != nil { @@ -45,7 +45,7 @@ func PushIndexToRegistry(image v1.ImageIndex, imageName string) error { } // Push the index to the registry - return remote.WriteIndex(ref, image, oci.MultiKeychainOption()) + return remote.WriteIndex(ref, index, oci.MultiKeychainOption()) } func SaveImageAsOCILayout(image v1.Image, path string) error { @@ -132,20 +132,8 @@ func SaveReferrers(manifest *attestation.AttestationManifest, outputs []*oci.Ima if output.Type == oci.OCI { continue } - ociManifest, err := manifest.AttestationImage.Image.Manifest() - if err != nil { - return fmt.Errorf("failed to get manifest: %w", err) - } - refDigest := ociManifest.Annotations[attestation.DockerReferenceDigest] - if refDigest == "" { - return fmt.Errorf("no digest found in manifest") - } - hash, err := v1.NewHash(refDigest) - if err != nil { - return fmt.Errorf("failed to parse digest: %w", err) - } // so that we use the same tag each time to reduce number of tags (tags aren't needed for referrers but we must push one) - attOut, err := oci.ReplaceTagInSpec(output, hash) + attOut, err := oci.ReplaceTagInSpec(output, manifest.SubjectDescriptor.Digest) if err != nil { return err } @@ -154,8 +142,13 @@ func SaveReferrers(manifest *attestation.AttestationManifest, outputs []*oci.Ima OS: "unknown", Architecture: "unknown", } - - err = SaveImage(attOut, manifest.AttestationImage.Image, "") + images, err := manifest.BuildReferringArtifacts() + if err != nil { + return fmt.Errorf("failed to build image: %w", err) + } + for _, image := range images { + err = SaveImage(attOut, image, "") + } if err != nil { return fmt.Errorf("failed to push image: %w", err) } diff --git a/pkg/oci/layout.go b/pkg/oci/layout.go index d59dbdb..966f23a 100644 --- a/pkg/oci/layout.go +++ b/pkg/oci/layout.go @@ -43,22 +43,13 @@ func (r *OCILayoutResolver) fetchAttestationManifest() (*attestation.Attestation } func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) { - attestationImage := r.AttestationManifest.AttestationImage - layers, err := attestationImage.Image.Layers() - if err != nil { - return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err) - } var envs []*att.Envelope - manifest, err := r.AttestationManifest.AttestationImage.Image.Manifest() - if err != nil { - return nil, fmt.Errorf("failed to get manifest: %w", err) - } - for i, l := range manifest.Layers { - if l.Annotations[attestation.InTotoPredicateType] != predicateType { + for _, attestationLayer := range r.AttestationManifest.AttestationImage.OriginalLayers { + if attestationLayer.Annotations[attestation.InTotoPredicateType] != predicateType { continue } - layer := layers[i] - mt, err := layer.MediaType() + + mt, err := attestationLayer.Layer.MediaType() if err != nil { return nil, fmt.Errorf("failed to get layer media type: %w", err) } @@ -68,7 +59,7 @@ func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType stri } var env = new(att.Envelope) // parse layer blob as json - r, err := layer.Uncompressed() + r, err := attestationLayer.Layer.Uncompressed() if err != nil { return nil, fmt.Errorf("failed to get layer contents: %w", err) @@ -138,8 +129,12 @@ 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) + if err != nil { + return nil, fmt.Errorf("failed to get attestations from image: %w", err) + } attest := &attestation.AttestationManifest{ - AttestationImage: &att.AttestationImage{Image: attestationImage}, + AttestationImage: &att.AttestationImage{OriginalLayers: layers}, OriginalDescriptor: &mf, SubjectName: name, SubjectDescriptor: subjectDescriptor, diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index 361fbd8..54070c2 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -47,21 +47,17 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option { return options } -func ExtractEnvelopes(ia *attestation.AttestationManifest, predicateType string) ([]*att.Envelope, error) { - manifest, err := ia.AttestationImage.Image.Manifest() - if err != nil { - return nil, fmt.Errorf("failed to get manifest: %w", err) - } +func ExtractEnvelopes(manifest *attestation.AttestationManifest, predicateType string) ([]*att.Envelope, error) { var envs []*att.Envelope - layers, err := ia.AttestationImage.Image.Layers() - if err != nil { - return nil, fmt.Errorf("failed to get layers: %w", err) - } - for i, l := range manifest.Layers { - if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) && - strings.HasSuffix(string(l.MediaType), "+dsse") && - l.Annotations[att.InTotoPredicateType] == predicateType { - reader, err := layers[i].Uncompressed() + for _, attestationLayer := range manifest.AttestationImage.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 { + 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 7f9c11e..aa1dad0 100644 --- a/pkg/oci/referrers.go +++ b/pkg/oci/referrers.go @@ -80,9 +80,13 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error { 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: r.Identifier, - AttestationImage: &attestation.AttestationImage{Image: attestationImage}, + AttestationImage: &attestation.AttestationImage{OriginalLayers: layers}, OriginalDescriptor: &m, SubjectDescriptor: desc, } diff --git a/pkg/oci/registry.go b/pkg/oci/registry.go index 63b5cd8..afad7c8 100644 --- a/pkg/oci/registry.go +++ b/pkg/oci/registry.go @@ -127,8 +127,12 @@ func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Pl 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.AttestationManifest{ - AttestationImage: &att.AttestationImage{Image: attestationImage}, + AttestationImage: &att.AttestationImage{OriginalLayers: layers}, OriginalDescriptor: &remoteDescriptor.Descriptor, SubjectName: image, SubjectDescriptor: subjectDescriptor, diff --git a/pkg/oci/registry_test.go b/pkg/oci/registry_test.go index 57ef74c..cb56f76 100644 --- a/pkg/oci/registry_test.go +++ b/pkg/oci/registry_test.go @@ -25,10 +25,7 @@ func TestRegistry(t *testing.T) { u, err := url.Parse(server.URL) require.NoError(t, err) - opts := &attestation.SigningOptions{ - Replace: true, - SkipSubject: true, - } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage) require.NoError(t, err) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) diff --git a/pkg/signerverifier/gcp_test.go b/pkg/signerverifier/gcp_test.go index 9c12a82..fc9a5de 100644 --- a/pkg/signerverifier/gcp_test.go +++ b/pkg/signerverifier/gcp_test.go @@ -2,16 +2,6 @@ package signerverifier -import ( - "context" - "crypto/ecdsa" - "testing" - - "github.com/docker/attest/internal/util" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - const publicKeyPEM = `-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMswW3iu7PR/rWTQjlhVmUsPK7rF k2s4SO3XbQ2GG2alm289SUUpmBAuVxvT8muYQ8HC/QzixzyTACTXsBDjQg== @@ -20,11 +10,7 @@ k2s4SO3XbQ2GG2alm289SUUpmBAuVxvT8muYQ8HC/QzixzyTACTXsBDjQg== // to run locally, we need to impersonate the GCP service account // gcloud auth application-default login --impersonate-service-account attest-kms-test@attest-kms-test.iam.gserviceaccount.com -<<<<<<< HEAD func TestGCPKMS_Signer(t *testing.T) { -======= -func _TestGCPKMS_Signer(t *testing.T) { ->>>>>>> 56ef672 (Single attestation when creating VSA) // create a new signer ctx := context.Background() ref := "projects/attest-kms-test/locations/us-west1/keyRings/attest-kms-test/cryptoKeys/test-signing-key/cryptoKeyVersions/1"