Repository: sigstore/cosign
Pull request #1084 (2021-11-19 23:30:55)

Add `mutate.AttachFileTo*` for attaching SBOMs.
This adds `mutate` methods for attaching SBOMs to `oci.Signed*`, so that downstream tools can start to manipulate the attachments on `oci.Signed*`.

Signed-off-by: Matt Moore <mattmoor@chainguard.dev>

#### Ticket Link

Related: https://github.com/google/ko/pull/506

cc @imjasonh 

#### Release Note
```release-note
Introduce new helpers in mutate to attach named oci.Files to oci.Signed*
```



Patch:
diff --git a/pkg/oci/mutate/mutate.go b/pkg/oci/mutate/mutate.go
index 48ceb2c4..2c6a3f14 100644
--- a/pkg/oci/mutate/mutate.go
+++ b/pkg/oci/mutate/mutate.go
@@ -151,29 +151,55 @@ func AttachAttestationToEntity(se oci.SignedEntity, att oci.Signature, opts ...S
 	}
 }
 
+// AttachFileToEntity attaches the provided file to the provided entity.
+func AttachFileToEntity(se oci.SignedEntity, name string, f oci.File, opts ...SignOption) (oci.SignedEntity, error) {
+	switch obj := se.(type) {
+	case oci.SignedImage:
+		return AttachFileToImage(obj, name, f, opts...)
+	case oci.SignedImageIndex:
+		return AttachFileToImageIndex(obj, name, f, opts...)
+	default:
+		return nil, fmt.Errorf("unsupported type: %T", se)
+	}
+}
+
 // AttachSignatureToImage attaches the provided signature to the provided image.
 func AttachSignatureToImage(si oci.SignedImage, sig oci.Signature, opts ...SignOption) (oci.SignedImage, error) {
 	return &signedImage{
 		SignedImage: si,
 		sig:         sig,
+		attachments: make(map[string]oci.File),
 		so:          makeSignOpts(opts...),
 	}, nil
 }
 
-// AttachAttestationToImage attaches the provided signature to the provided image.
+// AttachAttestationToImage attaches the provided attestation to the provided image.
 func AttachAttestationToImage(si oci.SignedImage, att oci.Signature, opts ...SignOption) (oci.SignedImage, error) {
 	return &signedImage{
 		SignedImage: si,
 		att:         att,
+		attachments: make(map[string]oci.File),
 		so:          makeSignOpts(opts...),
 	}, nil
 }
 
+// AttachFileToImage attaches the provided file to the provided image.
+func AttachFileToImage(si oci.SignedImage, name string, f oci.File, opts ...SignOption) (oci.SignedImage, error) {
+	return &signedImage{
+		SignedImage: si,
+		attachments: map[string]oci.File{
+			name: f,
+		},
+		so: makeSignOpts(opts...),
+	}, nil
+}
+
 type signedImage struct {
 	oci.SignedImage
-	sig oci.Signature
-	att oci.Signature
-	so  *signOpts
+	sig         oci.Signature
+	att         oci.Signature
+	so          *signOpts
+	attachments map[string]oci.File
 }
 
 // Signatures implements oci.SignedImage
@@ -223,7 +249,10 @@ func (si *signedImage) Attestations() (oci.Signatures, error) {
 
 // Attachment implements oci.SignedImage
 func (si *signedImage) Attachment(attName string) (oci.File, error) {
-	return nil, errors.New("unimplemented")
+	if f, ok := si.attachments[attName]; ok {
+		return f, nil
+	}
+	return nil, fmt.Errorf("attachment %q not found", attName)
 }
 
 // AttachSignatureToImageIndex attaches the provided signature to the provided image index.
@@ -231,6 +260,7 @@ func AttachSignatureToImageIndex(sii oci.SignedImageIndex, sig oci.Signature, op
 	return &signedImageIndex{
 		ociSignedImageIndex: sii,
 		sig:                 sig,
+		attachments:         make(map[string]oci.File),
 		so:                  makeSignOpts(opts...),
 	}, nil
 }
@@ -240,17 +270,30 @@ func AttachAttestationToImageIndex(sii oci.SignedImageIndex, att oci.Signature,
 	return &signedImageIndex{
 		ociSignedImageIndex: sii,
 		att:                 att,
+		attachments:         make(map[string]oci.File),
 		so:                  makeSignOpts(opts...),
 	}, nil
 }
 
+// AttachFileToImageIndex attaches the provided file to the provided image index.
+func AttachFileToImageIndex(sii oci.SignedImageIndex, name string, f oci.File, opts ...SignOption) (oci.SignedImageIndex, error) {
+	return &signedImageIndex{
+		ociSignedImageIndex: sii,
+		attachments: map[string]oci.File{
+			name: f,
+		},
+		so: makeSignOpts(opts...),
+	}, nil
+}
+
 type ociSignedImageIndex oci.SignedImageIndex
 
 type signedImageIndex struct {
 	ociSignedImageIndex
-	sig oci.Signature
-	att oci.Signature
-	so  *signOpts
+	sig         oci.Signature
+	att         oci.Signature
+	so          *signOpts
+	attachments map[string]oci.File
 }
 
 // Signatures implements oci.SignedImageIndex
@@ -300,5 +343,8 @@ func (sii *signedImageIndex) Attestations() (oci.Signatures, error) {
 
 // Attachment implements oci.SignedImageIndex
 func (sii *signedImageIndex) Attachment(attName string) (oci.File, error) {
-	return nil, errors.New("unimplemented")
+	if f, ok := sii.attachments[attName]; ok {
+		return f, nil
+	}
+	return nil, fmt.Errorf("attachment %q not found", attName)
 }


Test patch:
diff --git a/pkg/oci/mutate/mutate_test.go b/pkg/oci/mutate/mutate_test.go
index 62337503..9e1eef4b 100644
--- a/pkg/oci/mutate/mutate_test.go
+++ b/pkg/oci/mutate/mutate_test.go
@@ -16,8 +16,10 @@
 package mutate
 
 import (
+	"crypto/rand"
 	"errors"
 	"fmt"
+	"reflect"
 	"testing"
 
 	v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -161,6 +163,39 @@ func TestSignEntity(t *testing.T) {
 	}
 	sii := signed.ImageIndex(ii)
 
+	t.Run("attach SBOMs", func(t *testing.T) {
+		for _, se := range []oci.SignedEntity{si, sii} {
+			want := make([]byte, 300)
+			rand.Read(want)
+
+			orig, err := static.NewFile(want)
+			if err != nil {
+				t.Fatalf("static.NewFile() = %v", err)
+			}
+			se, err = AttachFileToEntity(se, "sbom", orig)
+			if err != nil {
+				t.Fatalf("AttachFileToEntity() = %v", err)
+			}
+
+			f, err := se.Attachment("sbom")
+			if err != nil {
+				t.Fatalf("Attachment(sbom) = %v", err)
+			}
+			got, err := f.Payload()
+			if err != nil {
+				t.Fatalf("Payload() = %v", err)
+			}
+			if !reflect.DeepEqual(want, got) {
+				t.Errorf("Payload() = %v, wanted %v", got, want)
+			}
+
+			f, err = se.Attachment("gitbom")
+			if err == nil {
+				t.Errorf("Attachment(gitbom) = %T, wanted error", f)
+			}
+		}
+	})
+
 	t.Run("without duplicate detector (signature)", func(t *testing.T) {
 		for _, se := range []oci.SignedEntity{si, sii} {
 			orig, err := static.NewSignature(nil, "")