diff --git a/pkg/oci/layout/index.go b/pkg/oci/layout/index.go index d71482548ae..94a0f2d565c 100644 --- a/pkg/oci/layout/index.go +++ b/pkg/oci/layout/index.go @@ -16,7 +16,6 @@ package layout import ( - "errors" "fmt" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -26,8 +25,10 @@ import ( ) const ( + kindAnnotation = "kind" imageAnnotation = "dev.cosignproject.cosign/image" sigsAnnotation = "dev.cosignproject.cosign/sigs" + attsAnnotation = "dev.cosignproject.cosign/atts" ) // SignedImageIndex provides access to a local index reference, and its signatures. @@ -57,16 +58,26 @@ var _ oci.SignedImageIndex = (*index)(nil) // Signatures implements oci.SignedImageIndex func (i *index) Signatures() (oci.Signatures, error) { - sigsImage, err := i.imageByAnnotation(sigsAnnotation) + img, err := i.imageByAnnotation(sigsAnnotation) if err != nil { return nil, err } - return &sigs{sigsImage}, nil + if img == nil { + return nil, nil + } + return &sigs{img}, nil } // Attestations implements oci.SignedImageIndex func (i *index) Attestations() (oci.Signatures, error) { - return nil, fmt.Errorf("not yet implemented") + img, err := i.imageByAnnotation(attsAnnotation) + if err != nil { + return nil, err + } + if img == nil { + return nil, nil + } + return &sigs{img}, nil } // Attestations implements oci.SignedImage @@ -98,11 +109,11 @@ func (i *index) imageByAnnotation(annotation string) (v1.Image, error) { return nil, err } for _, m := range manifest.Manifests { - if _, ok := m.Annotations[annotation]; ok { + if val, ok := m.Annotations[kindAnnotation]; ok && val == annotation { return i.Image(m.Digest) } } - return nil, errors.New("unable to find image") + return nil, nil } // SignedImageIndex implements oci.SignedImageIndex diff --git a/pkg/oci/layout/write.go b/pkg/oci/layout/write.go index c9d56308cc6..77518adc629 100644 --- a/pkg/oci/layout/write.go +++ b/pkg/oci/layout/write.go @@ -39,15 +39,34 @@ func WriteSignedImage(path string, si oci.SignedImage) error { if err != nil { return errors.Wrap(err, "getting signatures") } - if err := appendImage(layoutPath, sigs, sigsAnnotation); err != nil { - return errors.Wrap(err, "appending signatures") + if !isEmpty(sigs) { + if err := appendImage(layoutPath, sigs, sigsAnnotation); err != nil { + return errors.Wrap(err, "appending signatures") + } } - // TODO (priyawadhwa@) write attestations and attachments + + // write attestations + atts, err := si.Attestations() + if err != nil { + return errors.Wrap(err, "getting atts") + } + if !isEmpty(atts) { + if err := appendImage(layoutPath, atts, attsAnnotation); err != nil { + return errors.Wrap(err, "appending atts") + } + } + // TODO (priyawadhwa@) and attachments return nil } +// isEmpty returns true if the signatures or attesations are empty +func isEmpty(s oci.Signatures) bool { + ss, _ := s.Get() + return ss == nil +} + func appendImage(path layout.Path, img v1.Image, annotation string) error { return path.AppendImage(img, layout.WithAnnotations( - map[string]string{annotation: "true"}, + map[string]string{kindAnnotation: annotation}, )) } diff --git a/pkg/oci/layout/write_test.go b/pkg/oci/layout/write_test.go index e0e913148e7..dfdbd4996ac 100644 --- a/pkg/oci/layout/write_test.go +++ b/pkg/oci/layout/write_test.go @@ -48,6 +48,19 @@ func TestReadWrite(t *testing.T) { // compare the image we read with the one we wrote compareDigests(t, si, gotSignedImage) + // make sure we have 5 attestations + attImg, err := imageIndex.Attestations() + if err != nil { + t.Fatal(err) + } + atts, err := attImg.Get() + if err != nil { + t.Fatal(err) + } + if len(atts) != 5 { + t.Fatal("expected 5 attestations") + } + // make sure signatures are correct sigImage, err := imageIndex.Signatures() if err != nil { @@ -96,6 +109,19 @@ func randomSignedImage(t *testing.T) oci.SignedImage { t.Fatalf("SignEntity() = %v", err) } } + + want = 5 // Add 5 attestations + for i := 0; i < want; i++ { + sig, err := static.NewAttestation([]byte(fmt.Sprintf("%d", i))) + if err != nil { + t.Fatalf("static.NewSignature() = %v", err) + } + si, err = mutate.AttachAttestationToImage(si, sig) + if err != nil { + t.Fatalf("SignEntity() = %v", err) + } + } + return si } diff --git a/pkg/oci/remote/write.go b/pkg/oci/remote/write.go index f1f60a04a87..c07c237618f 100644 --- a/pkg/oci/remote/write.go +++ b/pkg/oci/remote/write.go @@ -44,11 +44,29 @@ func WriteSignedImageIndexImages(ref name.Reference, sii oci.SignedImageIndex, o if err != nil { return err } - sigsTag, err := SignatureTag(ref, opts...) + if sigs != nil { // will be nil if there are no associated signatures + sigsTag, err := SignatureTag(ref, opts...) + if err != nil { + return errors.Wrap(err, "sigs tag") + } + if err := remoteWrite(sigsTag, sigs, o.ROpt...); err != nil { + return err + } + } + + // write the attestations + atts, err := sii.Attestations() if err != nil { - return errors.Wrap(err, "sigs tag") + return err + } + if atts != nil { // will be nil if there are no associated attestations + attsTag, err := AttestationTag(ref, opts...) + if err != nil { + return errors.Wrap(err, "sigs tag") + } + return remoteWrite(attsTag, atts, o.ROpt...) } - return remoteWrite(sigsTag, sigs, o.ROpt...) + return nil } // WriteSignature publishes the signatures attached to the given entity diff --git a/test/e2e_test.go b/test/e2e_test.go index 174201b4733..da3db133ee1 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -639,6 +639,59 @@ func TestSaveLoad(t *testing.T) { must(verify(pubKeyPath, imgName2, true, nil, ""), t) } +func TestSaveLoadAttestation(t *testing.T) { + repo, stop := reg(t) + defer stop() + td := t.TempDir() + + imgName := path.Join(repo, "save-load") + + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + _, privKeyPath, pubKeyPath := keypair(t, td) + + ctx := context.Background() + // Now sign the image and verify it + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) + + // now, append an attestation to the image + slsaAttestation := `{ "builder": { "id": "2" }, "recipe": {} }` + slsaAttestationPath := filepath.Join(td, "attestation.slsa.json") + if err := os.WriteFile(slsaAttestationPath, []byte(slsaAttestation), 0600); err != nil { + t.Fatal(err) + } + + // Now attest the image + ko = sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + must(attest.AttestCmd(ctx, ko, options.RegistryOptions{}, imgName, "", false, slsaAttestationPath, false, + "custom", false, ftime.Duration(30*time.Second)), t) + + // save the image to a temp dir + imageDir := t.TempDir() + must(cli.SaveCmd(ctx, options.SaveOptions{Directory: imageDir}, imgName), t) + + // load the image from the temp dir into a new image and verify the new image + imgName2 := path.Join(repo, "save-load-2") + must(cli.LoadCmd(ctx, options.LoadOptions{Directory: imageDir}, imgName2), t) + must(verify(pubKeyPath, imgName2, true, nil, ""), t) + // Use cue to verify attestation on the new image + policyPath := filepath.Join(td, "policy.cue") + verifyAttestation := cliverify.VerifyAttestationCommand{ + KeyRef: pubKeyPath, + } + verifyAttestation.PredicateType = "slsaprovenance" + verifyAttestation.Policies = []string{policyPath} + // Success case + cuePolicy := `builder: id: "2"` + if err := os.WriteFile(policyPath, []byte(cuePolicy), 0600); err != nil { + t.Fatal(err) + } + must(verifyAttestation.Exec(ctx, []string{imgName2}), t) +} + func TestAttachSBOM(t *testing.T) { repo, stop := reg(t) defer stop()