Skip to content

Commit

Permalink
Add support for storing attestations in oci/layout
Browse files Browse the repository at this point in the history
Also rename the annotation used to differentiate images to "kind"

Signed-off-by: Priya Wadhwa <[email protected]>
  • Loading branch information
Priya Wadhwa committed Nov 23, 2021
1 parent 7ec91a4 commit 82fcff2
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 13 deletions.
22 changes: 16 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 @@ -28,6 +27,7 @@ import (
const (
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 +57,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 +108,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["kind"]; 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{"kind": 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

0 comments on commit 82fcff2

Please sign in to comment.