Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for storing attestations in oci/layout #1096

Merged
merged 1 commit into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions pkg/oci/layout/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package layout

import (
"errors"
"fmt"

v1 "github.com/google/go-containerregistry/pkg/v1"
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
27 changes: 23 additions & 4 deletions pkg/oci/layout/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
))
}
26 changes: 26 additions & 0 deletions pkg/oci/layout/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
24 changes: 21 additions & 3 deletions pkg/oci/remote/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down