Repository: sigstore/cosign
Pull request #1071 (2021-11-18 11:17:47)

Add flag for manually specifying a hash algo when verifying
#### Summary

While, at the time of writing, it's not possible to manually specify the signature digest algorithm which should be used when signing an image, some KMS providers have key types which force a digest algorithm which isn't sha256; e.g. GCP KMS with RSA4096 / SHA512 keys.

Cosign will happily use these keys for signing and will infer the digest algorithm based on what the KMS provider mandates, leading to a situation where cosign generates signatures which it can't later verify.

This commit adds a new `--signature-digest-algorithm`  flag which allows the user to manually specify one of 4 commonly-used secure hashes when using `cosign verify`.

#### Notes

1. Normally I'd (of course) add an e2e test for this kind of thing, but I can't see an obvious way of generating, say, a SHA512 signature without using a cloud KMS currently, and unless I'm missing something it doesn't look trivial to add that. I think adding an e2e test would require adding `--signature-digest-algorithm` to `cosign sign`, which I think would be desirable but is a much bigger piece of work and maybe needs more discussion.
2. I'm not totally sold on  `--signature-digest-algorithm` as a flag name - it's a bit wordy. Happy to rename if someone has a better suggestion!

#### Testing

I've tested that this works correctly using a test image which I think _should_ be publicly available. Using this commit, I can successfully run:

```console
curl -sSL https://raw.githubusercontent.com/SgtCoDFish/ace/main/PUBKEY > /tmp/PUBKEY
$ ./cosign verify --key "/tmp/PUBKEY" --signature-digest-algorithm SHA512 ghcr.io/sgtcodfish/ace:0.5
```

#### Ticket Link

Related to #946, specifically [this comment](https://github.com/sigstore/cosign/issues/946#issuecomment-966519321). That issue is more broad, though, and there's probably more work to be done in the future for that issue to be completed.

#### Release Note

```release-note
- Added the `--signature-digest-algorithm` flag to `cosign verify`, allowing verification of container image signatures which were generated with a non-SHA256 signature algorithm
```



Patch:
diff --git a/cmd/cosign/cli/options/signature_digest.go b/cmd/cosign/cli/options/signature_digest.go
new file mode 100644
index 00000000..764a3dfe
--- /dev/null
+++ b/cmd/cosign/cli/options/signature_digest.go
@@ -0,0 +1,85 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	"crypto"
+	"fmt"
+	"sort"
+	"strings"
+
+	_ "crypto/sha256" // for sha224 + sha256
+	_ "crypto/sha512" // for sha384 + sha512
+
+	"github.com/spf13/cobra"
+)
+
+var supportedSignatureAlgorithms = map[string]crypto.Hash{
+	"sha224": crypto.SHA224,
+	"sha256": crypto.SHA256,
+	"sha384": crypto.SHA384,
+	"sha512": crypto.SHA512,
+}
+
+func supportedSignatureAlgorithmNames() []string {
+	names := make([]string, 0, len(supportedSignatureAlgorithms))
+
+	for name := range supportedSignatureAlgorithms {
+		names = append(names, name)
+	}
+
+	sort.Strings(names)
+
+	return names
+}
+
+// SignatureDigestOptions holds options for specifying which digest algorithm should
+// be used when processing a signature.
+type SignatureDigestOptions struct {
+	AlgorithmName string
+}
+
+var _ Interface = (*SignatureDigestOptions)(nil)
+
+// AddFlags implements Interface
+func (o *SignatureDigestOptions) AddFlags(cmd *cobra.Command) {
+	validSignatureDigestAlgorithms := strings.Join(supportedSignatureAlgorithmNames(), "|")
+
+	cmd.Flags().StringVar(&o.AlgorithmName, "signature-digest-algorithm", "sha256",
+		fmt.Sprintf("digest algorithm to use when processing a signature (%s)", validSignatureDigestAlgorithms))
+}
+
+// HashAlgorithm converts the algorithm's name - provided as a string - into a crypto.Hash algorithm.
+// Returns an error if the algorithm name doesn't match a supported algorithm, and defaults to SHA256
+// in the event that the given algorithm is invalid.
+func (o *SignatureDigestOptions) HashAlgorithm() (crypto.Hash, error) {
+	normalizedAlgo := strings.ToLower(strings.TrimSpace(o.AlgorithmName))
+
+	if normalizedAlgo == "" {
+		return crypto.SHA256, nil
+	}
+
+	algo, exists := supportedSignatureAlgorithms[normalizedAlgo]
+	if !exists {
+		return crypto.SHA256, fmt.Errorf("unknown digest algorithm: %s", o.AlgorithmName)
+	}
+
+	if !algo.Available() {
+		return crypto.SHA256, fmt.Errorf("hash %q is not available on this platform", o.AlgorithmName)
+	}
+
+	return algo, nil
+}
diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go
index 32acf4bb..6d8f2586 100644
--- a/cmd/cosign/cli/options/verify.go
+++ b/cmd/cosign/cli/options/verify.go
@@ -30,7 +30,8 @@ type VerifyOptions struct {
 	SecurityKey SecurityKeyOptions
 	Rekor       RekorOptions
 	// TODO: this seems like it should have the Fulcio options.
-	Registry RegistryOptions
+	Registry        RegistryOptions
+	SignatureDigest SignatureDigestOptions
 	AnnotationOptions
 }
 
@@ -41,6 +42,7 @@ func (o *VerifyOptions) AddFlags(cmd *cobra.Command) {
 	o.SecurityKey.AddFlags(cmd)
 	o.Rekor.AddFlags(cmd)
 	o.Registry.AddFlags(cmd)
+	o.SignatureDigest.AddFlags(cmd)
 	o.AnnotationOptions.AddFlags(cmd)
 
 	cmd.Flags().StringVar(&o.Key, "key", "",
diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go
index 34b03b48..bec8bb80 100644
--- a/cmd/cosign/cli/verify.go
+++ b/cmd/cosign/cli/verify.go
@@ -46,9 +46,13 @@ against the transparency log.`,
   # (experimental) additionally, verify with the transparency log
   COSIGN_EXPERIMENTAL=1 cosign verify <IMAGE>
 
-  # verify image with public key
+  # verify image with an on-disk public key
   cosign verify --key cosign.pub <IMAGE>
 
+  # verify image with an on-disk public key, manually specifying the
+  # signature digest algorithm
+  cosign verify --key cosign.pub --signature-digest-algorithm sha512 <IMAGE>
+
   # verify image with public key provided by URL
   cosign verify --key https://host.for/[FILE] <IMAGE>
 
@@ -73,6 +77,12 @@ against the transparency log.`,
 			if err != nil {
 				return err
 			}
+
+			hashAlgorithm, err := o.SignatureDigest.HashAlgorithm()
+			if err != nil {
+				return err
+			}
+
 			v := verify.VerifyCommand{
 				RegistryOptions: o.Registry,
 				CheckClaims:     o.CheckClaims,
@@ -84,7 +94,10 @@ against the transparency log.`,
 				RekorURL:        o.Rekor.URL,
 				Attachment:      o.Attachment,
 				Annotations:     annotations,
+
+				HashAlgorithm: hashAlgorithm,
 			}
+
 			return v.Exec(cmd.Context(), args)
 		},
 	}
diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go
index 16076746..88be5478 100644
--- a/cmd/cosign/cli/verify/verify.go
+++ b/cmd/cosign/cli/verify/verify.go
@@ -17,6 +17,7 @@ package verify
 
 import (
 	"context"
+	"crypto"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -50,6 +51,8 @@ type VerifyCommand struct {
 	RekorURL    string
 	Attachment  string
 	Annotations sigs.AnnotationsMap
+
+	HashAlgorithm crypto.Hash
 }
 
 // Exec runs the verification command
@@ -65,6 +68,11 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
 		return flag.ErrHelp
 	}
 
+	// always default to sha256 if the algorithm hasn't been explicitly set
+	if c.HashAlgorithm == 0 {
+		c.HashAlgorithm = crypto.SHA256
+	}
+
 	if !options.OneOf(c.KeyRef, c.Sk) && !options.EnableExperimental() {
 		return &options.KeyParseError{}
 	}
@@ -89,7 +97,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
 	// Keys are optional!
 	var pubKey signature.Verifier
 	if keyRef != "" {
-		pubKey, err = sigs.PublicKeyFromKeyRef(ctx, keyRef)
+		pubKey, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, c.HashAlgorithm)
 		if err != nil {
 			return errors.Wrap(err, "loading public key")
 		}
diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md
index 77b70b22..f3d4be77 100644
--- a/doc/cosign_dockerfile_verify.md
+++ b/doc/cosign_dockerfile_verify.md
@@ -64,6 +64,7 @@ cosign dockerfile verify [flags]
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
   -o, --output string                                                                            output format for the signing image information (json|text) (default "json")
       --rekor-url string                                                                         [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev")
+      --signature-digest-algorithm string                                                        digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256")
       --sk                                                                                       whether to use a hardware security key
       --slot string                                                                              security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)
 ```
diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md
index aad65429..a15d0b76 100644
--- a/doc/cosign_manifest_verify.md
+++ b/doc/cosign_manifest_verify.md
@@ -58,6 +58,7 @@ cosign manifest verify [flags]
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
   -o, --output string                                                                            output format for the signing image information (json|text) (default "json")
       --rekor-url string                                                                         [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev")
+      --signature-digest-algorithm string                                                        digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256")
       --sk                                                                                       whether to use a hardware security key
       --slot string                                                                              security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)
 ```
diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md
index 40e7e705..b24748a7 100644
--- a/doc/cosign_verify.md
+++ b/doc/cosign_verify.md
@@ -28,9 +28,13 @@ cosign verify [flags]
   # (experimental) additionally, verify with the transparency log
   COSIGN_EXPERIMENTAL=1 cosign verify <IMAGE>
 
-  # verify image with public key
+  # verify image with an on-disk public key
   cosign verify --key cosign.pub <IMAGE>
 
+  # verify image with an on-disk public key, manually specifying the
+  # signature digest algorithm
+  cosign verify --key cosign.pub --signature-digest-algorithm sha512 <IMAGE>
+
   # verify image with public key provided by URL
   cosign verify --key https://host.for/[FILE] <IMAGE>
 
@@ -64,6 +68,7 @@ cosign verify [flags]
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
   -o, --output string                                                                            output format for the signing image information (json|text) (default "json")
       --rekor-url string                                                                         [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev")
+      --signature-digest-algorithm string                                                        digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256")
       --sk                                                                                       whether to use a hardware security key
       --slot string                                                                              security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)
 ```
diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go
index 23428b6b..408331dd 100644
--- a/pkg/cosign/verify.go
+++ b/pkg/cosign/verify.go
@@ -403,7 +403,7 @@ func VerifyBundle(sig oci.Signature) (bool, error) {
 		return false, errors.Wrap(err, "pem to ecdsa")
 	}
 
-	if err := VerifySET(bundle.Payload, []byte(bundle.SignedEntryTimestamp), rekorPubKey); err != nil {
+	if err := VerifySET(bundle.Payload, bundle.SignedEntryTimestamp, rekorPubKey); err != nil {
 		return false, err
 	}
 
diff --git a/pkg/oci/remote/layer.go b/pkg/oci/internal/signature/layer.go
similarity index 89%
rename from pkg/oci/remote/layer.go
rename to pkg/oci/internal/signature/layer.go
index 02614a48..9091cc57 100644
--- a/pkg/oci/remote/layer.go
+++ b/pkg/oci/internal/signature/layer.go
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package remote
+package signature
 
 import (
 	"crypto/x509"
@@ -28,11 +28,25 @@ import (
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 )
 
+const (
+	sigkey    = "dev.cosignproject.cosign/signature"
+	certkey   = "dev.sigstore.cosign/certificate"
+	chainkey  = "dev.sigstore.cosign/chain"
+	BundleKey = "dev.sigstore.cosign/bundle"
+)
+
 type sigLayer struct {
 	v1.Layer
 	desc v1.Descriptor
 }
 
+func New(l v1.Layer, desc v1.Descriptor) oci.Signature {
+	return &sigLayer{
+		Layer: l,
+		desc:  desc,
+	}
+}
+
 var _ oci.Signature = (*sigLayer)(nil)
 
 // Annotations implements oci.Signature
diff --git a/pkg/oci/layout/index.go b/pkg/oci/layout/index.go
new file mode 100644
index 00000000..d7148254
--- /dev/null
+++ b/pkg/oci/layout/index.go
@@ -0,0 +1,117 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+	"errors"
+	"fmt"
+
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/layout"
+	"github.com/sigstore/cosign/pkg/oci"
+	"github.com/sigstore/cosign/pkg/oci/signed"
+)
+
+const (
+	imageAnnotation = "dev.cosignproject.cosign/image"
+	sigsAnnotation  = "dev.cosignproject.cosign/sigs"
+)
+
+// SignedImageIndex provides access to a local index reference, and its signatures.
+func SignedImageIndex(path string) (oci.SignedImageIndex, error) {
+	p, err := layout.FromPath(path)
+	if err != nil {
+		return nil, err
+	}
+	ii, err := p.ImageIndex()
+	if err != nil {
+		return nil, err
+	}
+	return &index{
+		v1Index: ii,
+	}, nil
+}
+
+// We alias ImageIndex so that we can inline it without the type
+// name colliding with the name of a method it had to implement.
+type v1Index v1.ImageIndex
+
+type index struct {
+	v1Index
+}
+
+var _ oci.SignedImageIndex = (*index)(nil)
+
+// Signatures implements oci.SignedImageIndex
+func (i *index) Signatures() (oci.Signatures, error) {
+	sigsImage, err := i.imageByAnnotation(sigsAnnotation)
+	if err != nil {
+		return nil, err
+	}
+	return &sigs{sigsImage}, nil
+}
+
+// Attestations implements oci.SignedImageIndex
+func (i *index) Attestations() (oci.Signatures, error) {
+	return nil, fmt.Errorf("not yet implemented")
+}
+
+// Attestations implements oci.SignedImage
+func (i *index) Attachment(name string) (oci.File, error) {
+	return nil, fmt.Errorf("not yet implemented")
+}
+
+// SignedImage implements oci.SignedImageIndex
+// if an empty hash is passed in, return the original image that was signed
+func (i *index) SignedImage(h v1.Hash) (oci.SignedImage, error) {
+	var img v1.Image
+	var err error
+	if h.String() == ":" {
+		img, err = i.imageByAnnotation(imageAnnotation)
+	} else {
+		img, err = i.Image(h)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return signed.Image(img), nil
+}
+
+// imageByAnnotation searches through all manifests in the index.json
+// and returns the image that has the matching annotation
+func (i *index) imageByAnnotation(annotation string) (v1.Image, error) {
+	manifest, err := i.IndexManifest()
+	if err != nil {
+		return nil, err
+	}
+	for _, m := range manifest.Manifests {
+		if _, ok := m.Annotations[annotation]; ok {
+			return i.Image(m.Digest)
+		}
+	}
+	return nil, errors.New("unable to find image")
+}
+
+// SignedImageIndex implements oci.SignedImageIndex
+func (i *index) SignedImageIndex(h v1.Hash) (oci.SignedImageIndex, error) {
+	ii, err := i.ImageIndex(h)
+	if err != nil {
+		return nil, err
+	}
+	return &index{
+		v1Index: ii,
+	}, nil
+}
diff --git a/pkg/oci/layout/signatures.go b/pkg/oci/layout/signatures.go
new file mode 100644
index 00000000..c9538808
--- /dev/null
+++ b/pkg/oci/layout/signatures.go
@@ -0,0 +1,45 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/sigstore/cosign/pkg/oci"
+	"github.com/sigstore/cosign/pkg/oci/internal/signature"
+)
+
+type sigs struct {
+	v1.Image
+}
+
+var _ oci.Signatures = (*sigs)(nil)
+
+// Get implements oci.Signatures
+func (s *sigs) Get() ([]oci.Signature, error) {
+	manifest, err := s.Image.Manifest()
+	if err != nil {
+		return nil, err
+	}
+	signatures := make([]oci.Signature, 0, len(manifest.Layers))
+	for _, desc := range manifest.Layers {
+		l, err := s.Image.LayerByDigest(desc.Digest)
+		if err != nil {
+			return nil, err
+		}
+		signatures = append(signatures, signature.New(l, desc))
+	}
+	return signatures, nil
+}
diff --git a/pkg/oci/layout/write.go b/pkg/oci/layout/write.go
new file mode 100644
index 00000000..c9d56308
--- /dev/null
+++ b/pkg/oci/layout/write.go
@@ -0,0 +1,53 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/empty"
+	"github.com/google/go-containerregistry/pkg/v1/layout"
+	"github.com/pkg/errors"
+	"github.com/sigstore/cosign/pkg/oci"
+)
+
+// WriteSignedImage writes the image and all related signatures, attestations and attachments
+func WriteSignedImage(path string, si oci.SignedImage) error {
+	// First, write an empty index
+	layoutPath, err := layout.Write(path, empty.Index)
+	if err != nil {
+		return err
+	}
+	// write the image
+	if err := appendImage(layoutPath, si, imageAnnotation); err != nil {
+		return errors.Wrap(err, "appending signed image")
+	}
+	// write the signatures
+	sigs, err := si.Signatures()
+	if err != nil {
+		return errors.Wrap(err, "getting signatures")
+	}
+	if err := appendImage(layoutPath, sigs, sigsAnnotation); err != nil {
+		return errors.Wrap(err, "appending signatures")
+	}
+	// TODO (priyawadhwa@) write attestations and attachments
+	return nil
+}
+
+func appendImage(path layout.Path, img v1.Image, annotation string) error {
+	return path.AppendImage(img, layout.WithAnnotations(
+		map[string]string{annotation: "true"},
+	))
+}
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)
 }
diff --git a/pkg/oci/remote/remote.go b/pkg/oci/remote/remote.go
index ab4c63b7..9644ecf0 100644
--- a/pkg/oci/remote/remote.go
+++ b/pkg/oci/remote/remote.go
@@ -26,13 +26,6 @@ import (
 	"github.com/sigstore/cosign/pkg/oci"
 )
 
-const (
-	sigkey    = "dev.cosignproject.cosign/signature"
-	certkey   = "dev.sigstore.cosign/certificate"
-	chainkey  = "dev.sigstore.cosign/chain"
-	BundleKey = "dev.sigstore.cosign/bundle"
-)
-
 // These enable mocking for unit testing without faking an entire registry.
 var (
 	remoteImage = remote.Image
diff --git a/pkg/oci/remote/signatures.go b/pkg/oci/remote/signatures.go
index a17064ad..9f535a8d 100644
--- a/pkg/oci/remote/signatures.go
+++ b/pkg/oci/remote/signatures.go
@@ -24,6 +24,7 @@ import (
 	"github.com/pkg/errors"
 	"github.com/sigstore/cosign/pkg/oci"
 	"github.com/sigstore/cosign/pkg/oci/empty"
+	"github.com/sigstore/cosign/pkg/oci/internal/signature"
 )
 
 // Signatures fetches the signatures image represented by the named reference.
@@ -63,10 +64,7 @@ func (s *sigs) Get() ([]oci.Signature, error) {
 		if err != nil {
 			return nil, err
 		}
-		signatures = append(signatures, &sigLayer{
-			Layer: layer,
-			desc:  desc,
-		})
+		signatures = append(signatures, signature.New(layer, desc))
 	}
 	return signatures, nil
 }
diff --git a/pkg/oci/signatures.go b/pkg/oci/signatures.go
index 2d645276..82e473d5 100644
--- a/pkg/oci/signatures.go
+++ b/pkg/oci/signatures.go
@@ -18,7 +18,6 @@ package oci
 import (
 	"crypto/x509"
 
-	"github.com/go-openapi/strfmt"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 )
 
@@ -64,7 +63,7 @@ type Signature interface {
 // Bundle holds metadata about recording a Signature's ephemeral key to
 // a Rekor transparency log.
 type Bundle struct {
-	SignedEntryTimestamp strfmt.Base64
+	SignedEntryTimestamp []byte
 	Payload              BundlePayload
 }
 
diff --git a/pkg/oci/static/options.go b/pkg/oci/static/options.go
index 9220c06b..4e067173 100644
--- a/pkg/oci/static/options.go
+++ b/pkg/oci/static/options.go
@@ -16,7 +16,8 @@
 package static
 
 import (
-	"github.com/go-openapi/swag"
+	"encoding/json"
+
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/oci"
 	ctypes "github.com/sigstore/cosign/pkg/types"
@@ -51,7 +52,7 @@ func makeOptions(opts ...Option) (*options, error) {
 	}
 
 	if o.Bundle != nil {
-		b, err := swag.WriteJSON(o.Bundle)
+		b, err := json.Marshal(o.Bundle)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/oci/static/signature.go b/pkg/oci/static/signature.go
index 3fd0602a..b9878697 100644
--- a/pkg/oci/static/signature.go
+++ b/pkg/oci/static/signature.go
@@ -24,7 +24,6 @@ import (
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/oci"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
-	"knative.dev/pkg/kmeta"
 )
 
 const (
@@ -66,9 +65,12 @@ var _ oci.Signature = (*staticLayer)(nil)
 
 // Annotations implements oci.Signature
 func (l *staticLayer) Annotations() (map[string]string, error) {
-	return kmeta.UnionMaps(l.opts.Annotations, map[string]string{
-		SignatureAnnotationKey: l.b64sig,
-	}), nil
+	m := make(map[string]string, len(l.opts.Annotations)+1)
+	for k, v := range l.opts.Annotations {
+		m[k] = v
+	}
+	m[SignatureAnnotationKey] = l.b64sig
+	return m, nil
 }
 
 // Payload implements oci.Signature
diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go
index 99a12136..3d8017b7 100644
--- a/pkg/signature/keys.go
+++ b/pkg/signature/keys.go
@@ -35,9 +35,16 @@ import (
 	"github.com/sigstore/sigstore/pkg/signature/kms"
 )
 
+// LoadPublicKey is a wrapper for VerifierForKeyRef, hardcoding SHA256 as the hash algorithm
 func LoadPublicKey(ctx context.Context, keyRef string) (verifier signature.Verifier, err error) {
+	return VerifierForKeyRef(ctx, keyRef, crypto.SHA256)
+}
+
+// VerifierForKeyRef parses the given keyRef, loads the key and returns an appropriate
+// verifier using the provided hash algorithm
+func VerifierForKeyRef(ctx context.Context, keyRef string, hashAlgorithm crypto.Hash) (verifier signature.Verifier, err error) {
 	// The key could be plaintext, in a file, at a URL, or in KMS.
-	if kmsKey, err := kms.Get(ctx, keyRef, crypto.SHA256); err == nil {
+	if kmsKey, err := kms.Get(ctx, keyRef, hashAlgorithm); err == nil {
 		// KMS specified
 		return kmsKey, nil
 	}
@@ -53,7 +60,8 @@ func LoadPublicKey(ctx context.Context, keyRef string) (verifier signature.Verif
 	if err != nil {
 		return nil, errors.Wrap(err, "pem to public key")
 	}
-	return signature.LoadVerifier(pubKey, crypto.SHA256)
+
+	return signature.LoadVerifier(pubKey, hashAlgorithm)
 }
 
 func loadKey(keyPath string, pf cosign.PassFunc) (*signature.ECDSASignerVerifier, error) {
@@ -68,13 +76,13 @@ func loadKey(keyPath string, pf cosign.PassFunc) (*signature.ECDSASignerVerifier
 	return cosign.LoadECDSAPrivateKey(kb, pass)
 }
 
-func loadPublicKey(raw []byte) (signature.Verifier, error) {
+func loadPublicKey(raw []byte, hashAlgorithm crypto.Hash) (signature.Verifier, error) {
 	// PEM encoded file.
 	ed, err := cosign.PemToECDSAKey(raw)
 	if err != nil {
 		return nil, errors.Wrap(err, "pem to ecdsa")
 	}
-	return signature.LoadECDSAVerifier(ed, crypto.SHA256)
+	return signature.LoadECDSAVerifier(ed, hashAlgorithm)
 }
 
 func SignerFromKeyRef(ctx context.Context, keyRef string, pf cosign.PassFunc) (signature.Signer, error) {
@@ -145,6 +153,10 @@ func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.Pass
 }
 
 func PublicKeyFromKeyRef(ctx context.Context, keyRef string) (signature.Verifier, error) {
+	return PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, crypto.SHA256)
+}
+
+func PublicKeyFromKeyRefWithHashAlgo(ctx context.Context, keyRef string, hashAlgorithm crypto.Hash) (signature.Verifier, error) {
 	if strings.HasPrefix(keyRef, kubernetes.KeyReference) {
 		s, err := kubernetes.GetKeyPairSecret(ctx, keyRef)
 		if err != nil {
@@ -152,7 +164,7 @@ func PublicKeyFromKeyRef(ctx context.Context, keyRef string) (signature.Verifier
 		}
 
 		if len(s.Data) > 0 {
-			return loadPublicKey(s.Data["cosign.pub"])
+			return loadPublicKey(s.Data["cosign.pub"], hashAlgorithm)
 		}
 	}
 
@@ -191,11 +203,11 @@ func PublicKeyFromKeyRef(ctx context.Context, keyRef string) (signature.Verifier
 		}
 
 		if len(pubKey) > 0 {
-			return loadPublicKey([]byte(pubKey))
+			return loadPublicKey([]byte(pubKey), hashAlgorithm)
 		}
 	}
 
-	return LoadPublicKey(ctx, keyRef)
+	return VerifierForKeyRef(ctx, keyRef, hashAlgorithm)
 }
 
 func PublicKeyPem(key signature.PublicKeyProvider, pkOpts ...signature.PublicKeyOption) ([]byte, error) {


Test patch:
diff --git a/pkg/oci/remote/layer_test.go b/pkg/oci/internal/signature/layer_test.go
similarity index 98%
rename from pkg/oci/remote/layer_test.go
rename to pkg/oci/internal/signature/layer_test.go
index 5eca7971..43f4eef1 100644
--- a/pkg/oci/remote/layer_test.go
+++ b/pkg/oci/internal/signature/layer_test.go
@@ -13,10 +13,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package remote
+package signature
 
 import (
 	"bytes"
+	"encoding/base64"
 	"fmt"
 	"testing"
 
@@ -28,6 +29,14 @@ import (
 	"github.com/sigstore/cosign/pkg/oci"
 )
 
+func mustDecode(s string) []byte {
+	b, err := base64.StdEncoding.DecodeString(s)
+	if err != nil {
+		panic(err.Error())
+	}
+	return b
+}
+
 func TestSignature(t *testing.T) {
 	layer, err := random.Layer(300 /* byteSize */, types.DockerLayer)
 	if err != nil {
diff --git a/pkg/oci/layout/write_test.go b/pkg/oci/layout/write_test.go
new file mode 100644
index 00000000..e0e91314
--- /dev/null
+++ b/pkg/oci/layout/write_test.go
@@ -0,0 +1,114 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/random"
+	"github.com/sigstore/cosign/pkg/oci"
+	"github.com/sigstore/cosign/pkg/oci/mutate"
+	"github.com/sigstore/cosign/pkg/oci/signed"
+	"github.com/sigstore/cosign/pkg/oci/static"
+)
+
+func TestReadWrite(t *testing.T) {
+	// write random signed image to disk
+	si := randomSignedImage(t)
+	tmp := t.TempDir()
+	if err := WriteSignedImage(tmp, si); err != nil {
+		t.Fatal(err)
+	}
+
+	// read the image and make sure the signatures exist
+	imageIndex, err := SignedImageIndex(tmp)
+	if err != nil {
+		t.Fatal(err)
+	}
+	gotSignedImage, err := imageIndex.SignedImage(v1.Hash{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	// compare the image we read with the one we wrote
+	compareDigests(t, si, gotSignedImage)
+
+	// make sure signatures are correct
+	sigImage, err := imageIndex.Signatures()
+	if err != nil {
+		t.Fatal(err)
+	}
+	sigs, err := sigImage.Get()
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := 6
+	if len(sigs) != want {
+		t.Fatal("didn't get the expected number of signatures")
+	}
+	// make sure the annotation is correct
+	for i, sig := range sigs {
+		annotations, err := sig.Annotations()
+		if err != nil {
+			t.Fatal(err)
+		}
+		val, ok := annotations["layer"]
+		if !ok {
+			t.Fatal("expected annotation doesn't exist on signature")
+		}
+		if val != fmt.Sprintf("%d", i) {
+			t.Fatal("expected annotation isn't correct")
+		}
+	}
+}
+
+func randomSignedImage(t *testing.T) oci.SignedImage {
+	i, err := random.Image(300 /* byteSize */, 7 /* layers */)
+	if err != nil {
+		t.Fatalf("random.Image() = %v", err)
+	}
+	si := signed.Image(i)
+
+	want := 6 // Add 6 signatures
+	for i := 0; i < want; i++ {
+		annotationOption := static.WithAnnotations(map[string]string{"layer": fmt.Sprintf("%d", i)})
+		sig, err := static.NewSignature(nil, fmt.Sprintf("%d", i), annotationOption)
+		if err != nil {
+			t.Fatalf("static.NewSignature() = %v", err)
+		}
+		si, err = mutate.AttachSignatureToImage(si, sig)
+		if err != nil {
+			t.Fatalf("SignEntity() = %v", err)
+		}
+	}
+	return si
+}
+
+func compareDigests(t *testing.T, img1 oci.SignedImage, img2 oci.SignedImage) {
+	d1, err := img1.Digest()
+	if err != nil {
+		t.Fatal(err)
+	}
+	d2, err := img2.Digest()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if d := cmp.Diff(d1, d2); d != "" {
+		t.Fatalf("digests are different: %s", d)
+	}
+}
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, "")
diff --git a/pkg/oci/remote/remote_test.go b/pkg/oci/remote/remote_test.go
index 17e4fb1c..885a5e1b 100644
--- a/pkg/oci/remote/remote_test.go
+++ b/pkg/oci/remote/remote_test.go
@@ -16,7 +16,6 @@
 package remote
 
 import (
-	"encoding/base64"
 	"testing"
 
 	"github.com/google/go-containerregistry/pkg/name"
@@ -25,14 +24,6 @@ import (
 	"github.com/pkg/errors"
 )
 
-func mustDecode(s string) []byte {
-	b, err := base64.StdEncoding.DecodeString(s)
-	if err != nil {
-		panic(err.Error())
-	}
-	return b
-}
-
 func TestTagMethods(t *testing.T) {
 	rg := remoteGet
 	defer func() {
diff --git a/pkg/oci/static/options_test.go b/pkg/oci/static/options_test.go
index 5994c980..5a9ca66b 100644
--- a/pkg/oci/static/options_test.go
+++ b/pkg/oci/static/options_test.go
@@ -19,6 +19,7 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/oci"
 	ctypes "github.com/sigstore/cosign/pkg/types"
@@ -86,7 +87,7 @@ func TestOptions(t *testing.T) {
 			LayerMediaType:  ctypes.SimpleSigningMediaType,
 			ConfigMediaType: types.OCIConfigJSON,
 			Annotations: map[string]string{
-				BundleAnnotationKey: "{\"SignedEntryTimestamp\":\"\",\"Payload\":{\"body\":null,\"integratedTime\":0,\"logIndex\":0,\"logID\":\"\"}}",
+				BundleAnnotationKey: "{\"SignedEntryTimestamp\":null,\"Payload\":{\"body\":null,\"integratedTime\":0,\"logIndex\":0,\"logID\":\"\"}}",
 			},
 			Bundle: bundle,
 		},
@@ -100,7 +101,7 @@ func TestOptions(t *testing.T) {
 			}
 
 			if !reflect.DeepEqual(got, test.want) {
-				t.Errorf("makeOptions() = %#v, wanted %#v", got, test.want)
+				t.Errorf("makeOptions() = %s", cmp.Diff(got, test.want))
 			}
 		})
 	}
diff --git a/test/e2e_test.go b/test/e2e_test.go
index accf84ad..171f84b5 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -21,6 +21,7 @@ package test
 import (
 	"bytes"
 	"context"
+	"crypto"
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
@@ -74,11 +75,12 @@ var passFunc = func(_ bool) ([]byte, error) {
 
 var verify = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string) error {
 	cmd := cliverify.VerifyCommand{
-		KeyRef:      keyRef,
-		RekorURL:    rekorURL,
-		CheckClaims: checkClaims,
-		Annotations: sigs.AnnotationsMap{Annotations: annotations},
-		Attachment:  attachment,
+		KeyRef:        keyRef,
+		RekorURL:      rekorURL,
+		CheckClaims:   checkClaims,
+		Annotations:   sigs.AnnotationsMap{Annotations: annotations},
+		Attachment:    attachment,
+		HashAlgorithm: crypto.SHA256,
 	}
 
 	args := []string{imageRef}