Repository: sigstore/cosign
Pull request #3234 (2023-09-10 07:42:07)

feat: add platform flag to cosign copy command
Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>
<!--
Thanks for opening a pull request! Please do not just delete this text.  The three fields below are mandatory.

Please remember to:
- This PR requires an issue. If it is a new feature, the issue should proceed the PR and will have allowed sufficent time for discussions to take place. Please use
issue tags such as "Closes #XYZ" or "Resolves sigstore/repo-name#XYZ".
  [documentation](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
- ensure your commits are signed-off, as sigstore uses the [DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits
- lastly, ensure there are no merge commits!

Thank you :)
-->

#### Summary

Fixes: #2748

This PR adds a `--platform` flag to `cosign copy`. This allows copying images and signatures for specific platform/architecture.

/cc @imjasonh @znewman01 @cpanato

<!--
 Explain the **motivation** for making this change. What existing problem does the pull request solve? How can reviewers test this PR?
-->

Before:
```bash
cosign copy alpine ttl.sh/test-cosign-copy:5m
Copying index.docker.io/library/alpine@sha256:5febc00b4d2a84af2a077bc34ea90659b6570110a54253f19c5dca8164b1dbf6 to ttl.sh/test-cosign-copy:sha256:5febc00b4d2a84af2a077bc34ea90659b6570110a54253f19c5dca8164b1dbf6...
Copying index.docker.io/library/alpine@sha256:b312e4b0e2c665d634602411fcb7c2699ba748c36f59324457bc17de485f36f6 to ttl.sh/test-cosign-copy:sha256:b312e4b0e2c665d634602411fcb7c2699ba748c36f59324457bc17de485f36f6...
Copying index.docker.io/library/alpine@sha256:f748290eb66ad6f938e25dd348acfb3527a422e280b7547b1cdfaf38d4492c4b to ttl.sh/test-cosign-copy:sha256:f748290eb66ad6f938e25dd348acfb3527a422e280b7547b1cdfaf38d4492c4b...
Copying index.docker.io/library/alpine@sha256:16e86b2388774982fbdf230101a72201691b1f97cb0066c2099abf30dd7e6d59 to ttl.sh/test-cosign-copy:sha256:16e86b2388774982fbdf230101a72201691b1f97cb0066c2099abf30dd7e6d59...
Copying index.docker.io/library/alpine@sha256:c75ede79e457d6454bca6fc51967a247a4b9daff9f31197cfbef69b1a651cada to ttl.sh/test-cosign-copy:sha256:c75ede79e457d6454bca6fc51967a247a4b9daff9f31197cfbef69b1a651cada...
Copying index.docker.io/library/alpine@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a to ttl.sh/test-cosign-copy:sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a...
Copying index.docker.io/library/alpine@sha256:c5c5fda71656f28e49ac9c5416b3643eaa6a108a8093151d6d1afc9463be8e33 to ttl.sh/test-cosign-copy:sha256:c5c5fda71656f28e49ac9c5416b3643eaa6a108a8093151d6d1afc9463be8e33...
Copying index.docker.io/library/alpine@sha256:1fd62556954250bac80d601a196bb7fd480ceba7c10e94dd8fd4c6d1c08783d5 to ttl.sh/test-cosign-copy:sha256:1fd62556954250bac80d601a196bb7fd480ceba7c10e94dd8fd4c6d1c08783d5...
Copying index.docker.io/library/alpine@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a to ttl.sh/test-cosign-copy:5m...
```

After:
```bash
go run ./cmd/cosign copy --platform=linux/amd64 alpine ttl.sh/test-cosign-copy-local:5m
Copying index.docker.io/library/alpine@sha256:c5c5fda71656f28e49ac9c5416b3643eaa6a108a8093151d6d1afc9463be8e33 to ttl.sh/test-cosign-copy-local:sha256-c5c5fda71656f28e49ac9c5416b3643eaa6a108a8093151d6d1afc9463be8e33...
Copying index.docker.io/library/alpine@sha256:c5c5fda71656f28e49ac9c5416b3643eaa6a108a8093151d6d1afc9463be8e33 to ttl.sh/test-cosign-copy-local:5m...
```
#### Release Note
Added `--platform` flag to `cosign copy` command
<!--
Add a release note for each of the following conditions:

* Config changes (additions, deletions, updates)
* API additions—new endpoint, new response fields, or newly accepted request parameters
* Database changes (any)
* Websocket additions or changes
* Anything noteworthy to an administrator running private sigstore instances (err on the side of over-communicating)
* New features and improvements, including behavioural changes, UI changes and CLI changes
* Bug fixes and fixes of previous known issues
* Deprecation warnings, breaking changes, or compatibility notes

If no release notes are required write NONE. Use past-tense.

-->



Patch:
diff --git a/.ko.yaml b/.ko.yaml
index d64dcd6c..a74906d9 100644
--- a/.ko.yaml
+++ b/.ko.yaml
@@ -14,7 +14,7 @@
 # limitations under the License.
 
 # We need a shell for a lot of redirection/piping to work
-defaultBaseImage: gcr.io/distroless/base:debug-nonroot
+defaultBaseImage: gcr.io/distroless/base-debian12:debug-nonroot
 
 builds:
 - id: cosign
diff --git a/cmd/cosign/cli/copy.go b/cmd/cosign/cli/copy.go
index 7c23f7e3..5af36483 100644
--- a/cmd/cosign/cli/copy.go
+++ b/cmd/cosign/cli/copy.go
@@ -37,12 +37,15 @@ func Copy() *cobra.Command {
   cosign copy --sig-only example.com/src example.com/dest
 
   # overwrite destination image and signatures
-  cosign copy -f example.com/src example.com/dest`,
+  cosign copy -f example.com/src example.com/dest
+
+	# copy a container image and its signatures for a specific platform
+  cosign copy --platform=linux/amd64 example.com/src:latest example.com/dest:latest`,
 
 		Args:             cobra.ExactArgs(2),
 		PersistentPreRun: options.BindViper,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return copy.CopyCmd(cmd.Context(), o.Registry, args[0], args[1], o.SignatureOnly, o.Force)
+			return copy.CopyCmd(cmd.Context(), o.Registry, args[0], args[1], o.SignatureOnly, o.Force, o.Platform)
 		},
 	}
 
diff --git a/cmd/cosign/cli/copy/copy.go b/cmd/cosign/cli/copy/copy.go
index 0c0d6d2b..462c57d2 100644
--- a/cmd/cosign/cli/copy/copy.go
+++ b/cmd/cosign/cli/copy/copy.go
@@ -28,6 +28,7 @@ import (
 	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
 	"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
 	"github.com/sigstore/cosign/v2/pkg/oci"
+	ociplatform "github.com/sigstore/cosign/v2/pkg/oci/platform"
 	ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
 	"github.com/sigstore/cosign/v2/pkg/oci/walk"
 	"golang.org/x/sync/errgroup"
@@ -35,7 +36,7 @@ import (
 
 // CopyCmd implements the logic to copy the supplied container image and signatures.
 // nolint
-func CopyCmd(ctx context.Context, regOpts options.RegistryOptions, srcImg, dstImg string, sigOnly, force bool) error {
+func CopyCmd(ctx context.Context, regOpts options.RegistryOptions, srcImg, dstImg string, sigOnly, force bool, platform string) error {
 	no := regOpts.NameOptions()
 	srcRef, err := name.ParseReference(srcImg, no...)
 	if err != nil {
@@ -71,6 +72,11 @@ func CopyCmd(ctx context.Context, regOpts options.RegistryOptions, srcImg, dstIm
 		return err
 	}
 
+	root, err = ociplatform.SignedEntityForPlatform(root, platform)
+	if err != nil {
+		return err
+	}
+
 	if err := walk.SignedEntity(gctx, root, func(ctx context.Context, se oci.SignedEntity) error {
 		// Both of the SignedEntity types implement Digest()
 		h, err := se.Digest()
diff --git a/cmd/cosign/cli/download/sbom.go b/cmd/cosign/cli/download/sbom.go
index 0d4816e4..2e3c41a1 100644
--- a/cmd/cosign/cli/download/sbom.go
+++ b/cmd/cosign/cli/download/sbom.go
@@ -21,28 +21,14 @@ import (
 	"fmt"
 	"io"
 	"os"
-	"strings"
 
 	"github.com/google/go-containerregistry/pkg/name"
-	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
 	"github.com/sigstore/cosign/v2/pkg/oci"
+	"github.com/sigstore/cosign/v2/pkg/oci/platform"
 	ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
 )
 
-type platformList []struct {
-	hash     v1.Hash
-	platform *v1.Platform
-}
-
-func (pl *platformList) String() string {
-	r := []string{}
-	for _, p := range *pl {
-		r = append(r, p.platform.String())
-	}
-	return strings.Join(r, ", ")
-}
-
 func SBOMCmd(
 	ctx context.Context, regOpts options.RegistryOptions,
 	dnOpts options.SBOMDownloadOptions, imageRef string, out io.Writer,
@@ -62,43 +48,12 @@ func SBOMCmd(
 		return nil, err
 	}
 
-	idx, isIndex := se.(oci.SignedImageIndex)
-
-	// We only allow --platform on multiarch indexes
-	if dnOpts.Platform != "" && !isIndex {
-		return nil, fmt.Errorf("specified reference is not a multiarch image")
+	se, err = platform.SignedEntityForPlatform(se, dnOpts.Platform)
+	if err != nil {
+		return nil, err
 	}
 
-	if dnOpts.Platform != "" && isIndex {
-		targetPlatform, err := v1.ParsePlatform(dnOpts.Platform)
-		if err != nil {
-			return nil, fmt.Errorf("parsing platform: %w", err)
-		}
-		platforms, err := getIndexPlatforms(idx)
-		if err != nil {
-			return nil, fmt.Errorf("getting available platforms: %w", err)
-		}
-
-		platforms = matchPlatform(targetPlatform, platforms)
-		if len(platforms) == 0 {
-			return nil, fmt.Errorf("unable to find an SBOM for %s", targetPlatform.String())
-		}
-		if len(platforms) > 1 {
-			return nil, fmt.Errorf(
-				"platform spec matches more than one image architecture: %s",
-				platforms.String(),
-			)
-		}
-
-		nse, err := idx.SignedImage(platforms[0].hash)
-		if err != nil {
-			return nil, fmt.Errorf("searching for %s image: %w", platforms[0].hash.String(), err)
-		}
-		if nse == nil {
-			return nil, fmt.Errorf("unable to find image %s", platforms[0].hash.String())
-		}
-		se = nse
-	}
+	idx, isIndex := se.(oci.SignedImageIndex)
 
 	file, err := se.Attachment("sbom")
 	if errors.Is(err, ociremote.ErrImageNotFound) {
@@ -106,7 +61,7 @@ func SBOMCmd(
 			return nil, errors.New("no sbom attached to reference")
 		}
 		// Help the user with the available architectures
-		pl, err := getIndexPlatforms(idx)
+		pl, err := platform.GetIndexPlatforms(idx)
 		if len(pl) > 0 && err == nil {
 			fmt.Fprintf(
 				os.Stderr,
@@ -139,57 +94,3 @@ func SBOMCmd(
 
 	return sboms, nil
 }
-
-func getIndexPlatforms(idx oci.SignedImageIndex) (platformList, error) {
-	im, err := idx.IndexManifest()
-	if err != nil {
-		return nil, fmt.Errorf("fetching index manifest: %w", err)
-	}
-
-	platforms := platformList{}
-	for _, m := range im.Manifests {
-		if m.Platform == nil {
-			continue
-		}
-		platforms = append(platforms, struct {
-			hash     v1.Hash
-			platform *v1.Platform
-		}{m.Digest, m.Platform})
-	}
-	return platforms, nil
-}
-
-// matchPlatform filters a list of platforms returning only those matching
-// a base. "Based" on ko's internal equivalent while it moves to GGCR.
-// https://github.com/google/ko/blob/e6a7a37e26d82a8b2bb6df991c5a6cf6b2728794/pkg/build/gobuild.go#L1020
-func matchPlatform(base *v1.Platform, list platformList) platformList {
-	ret := platformList{}
-	for _, p := range list {
-		if base.OS != "" && base.OS != p.platform.OS {
-			continue
-		}
-		if base.Architecture != "" && base.Architecture != p.platform.Architecture {
-			continue
-		}
-		if base.Variant != "" && base.Variant != p.platform.Variant {
-			continue
-		}
-
-		if base.OSVersion != "" && p.platform.OSVersion != base.OSVersion {
-			if base.OS != "windows" {
-				continue
-			} else { //nolint: revive
-				if pcount, bcount := strings.Count(base.OSVersion, "."), strings.Count(p.platform.OSVersion, "."); pcount == 2 && bcount == 3 {
-					if base.OSVersion != p.platform.OSVersion[:strings.LastIndex(p.platform.OSVersion, ".")] {
-						continue
-					}
-				} else {
-					continue
-				}
-			}
-		}
-		ret = append(ret, p)
-	}
-
-	return ret
-}
diff --git a/cmd/cosign/cli/options/copy.go b/cmd/cosign/cli/options/copy.go
index e9c5c701..4e2a9912 100644
--- a/cmd/cosign/cli/options/copy.go
+++ b/cmd/cosign/cli/options/copy.go
@@ -23,6 +23,7 @@ import (
 type CopyOptions struct {
 	SignatureOnly bool
 	Force         bool
+	Platform      string
 	Registry      RegistryOptions
 }
 
@@ -37,4 +38,7 @@ func (o *CopyOptions) AddFlags(cmd *cobra.Command) {
 
 	cmd.Flags().BoolVarP(&o.Force, "force", "f", false,
 		"overwrite destination image(s), if necessary")
+
+	cmd.Flags().StringVar(&o.Platform, "platform", "",
+		"only copy container image and its signatures for a specific platform image")
 }
diff --git a/cmd/cosign/cli/options/predicate.go b/cmd/cosign/cli/options/predicate.go
index 707a38ca..0c68e279 100644
--- a/cmd/cosign/cli/options/predicate.go
+++ b/cmd/cosign/cli/options/predicate.go
@@ -19,7 +19,8 @@ import (
 	"fmt"
 	"net/url"
 
-	slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
+	slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
+	slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
 
 	"github.com/in-toto/in-toto-golang/in_toto"
 	"github.com/spf13/cobra"
@@ -30,6 +31,8 @@ import (
 const (
 	PredicateCustom    = "custom"
 	PredicateSLSA      = "slsaprovenance"
+	PredicateSLSA02    = "slsaprovenance02"
+	PredicateSLSA1     = "slsaprovenance1"
 	PredicateSPDX      = "spdx"
 	PredicateSPDXJSON  = "spdxjson"
 	PredicateCycloneDX = "cyclonedx"
@@ -40,7 +43,9 @@ const (
 // PredicateTypeMap is the mapping between the predicate `type` option to predicate URI.
 var PredicateTypeMap = map[string]string{
 	PredicateCustom:    attestation.CosignCustomProvenanceV01,
-	PredicateSLSA:      slsa.PredicateSLSAProvenance,
+	PredicateSLSA:      slsa02.PredicateSLSAProvenance,
+	PredicateSLSA02:    slsa02.PredicateSLSAProvenance,
+	PredicateSLSA1:     slsa1.PredicateSLSAProvenance,
 	PredicateSPDX:      in_toto.PredicateSPDX,
 	PredicateSPDXJSON:  in_toto.PredicateSPDX,
 	PredicateCycloneDX: in_toto.PredicateCycloneDX,
@@ -58,7 +63,7 @@ var _ Interface = (*PredicateOptions)(nil)
 // AddFlags implements Interface
 func (o *PredicateOptions) AddFlags(cmd *cobra.Command) {
 	cmd.Flags().StringVar(&o.Type, "type", "custom",
-		"specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI")
+		"specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI")
 }
 
 // ParsePredicateType parses the predicate `type` flag passed into a predicate URI, or validates `type` is a valid URI.
diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go
index ba286981..53c0fd3c 100644
--- a/cmd/cosign/cli/sign/sign.go
+++ b/cmd/cosign/cli/sign/sign.go
@@ -179,7 +179,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO
 
 		if digest, ok := ref.(name.Digest); ok && !signOpts.Recursive {
 			se, err := ociremote.SignedEntity(ref, opts...)
-			if err == ociremote.ErrEntityNotFound {
+			if _, isEntityNotFoundErr := err.(*ociremote.EntityNotFoundError); isEntityNotFoundErr {
 				se = ociremote.SignedUnknown(digest)
 			} else if err != nil {
 				return fmt.Errorf("accessing image: %w", err)
diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go
index c8d0f15d..3629fff8 100644
--- a/cmd/cosign/cli/verify/verify.go
+++ b/cmd/cosign/cli/verify/verify.go
@@ -202,7 +202,8 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
 	keyRef := c.KeyRef
 	certRef := c.CertRef
 
-	if !c.IgnoreSCT {
+	// Ignore Signed Certificate Timestamp if the flag is set or a key is provided
+	if !c.IgnoreSCT || keyRef != "" {
 		co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
 		if err != nil {
 			return fmt.Errorf("getting ctlog public keys: %w", err)
diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go
index b2f0561f..ccbec350 100644
--- a/cmd/cosign/cli/verify/verify_blob.go
+++ b/cmd/cosign/cli/verify/verify_blob.go
@@ -285,7 +285,8 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
 		opts = append(opts, static.WithCertChain(certPEM, chainPEM))
 	}
 
-	if !c.IgnoreSCT {
+	// Ignore Signed Certificate Timestamp if the flag is set or a key is provided
+	if !c.IgnoreSCT || c.KeyRef != "" {
 		co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
 		if err != nil {
 			return fmt.Errorf("getting ctlog public keys: %w", err)
diff --git a/doc/cosign_copy.md b/doc/cosign_copy.md
index 37a4fb0c..331d2925 100644
--- a/doc/cosign_copy.md
+++ b/doc/cosign_copy.md
@@ -19,6 +19,9 @@ cosign copy [flags]
 
   # overwrite destination image and signatures
   cosign copy -f example.com/src example.com/dest
+
+	# copy a container image and its signatures for a specific platform
+  cosign copy --platform=linux/amd64 example.com/src:latest example.com/dest:latest
 ```
 
 ### Options
@@ -30,6 +33,7 @@ cosign copy [flags]
   -f, --force                                                                                    overwrite destination image(s), if necessary
   -h, --help                                                                                     help for copy
       --k8s-keychain                                                                             whether to use the kubernetes keychain instead of the default keychain (supports workload identity).
+      --platform string                                                                          only copy container image and its signatures for a specific platform image
       --sig-only                                                                                 only copy the image signature
 ```
 
diff --git a/go.mod b/go.mod
index 1d544674..2de60085 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	cuelang.org/go v0.6.0
 	github.com/ThalesIgnite/crypto11 v1.2.5
 	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795
-	github.com/buildkite/agent/v3 v3.54.0
+	github.com/buildkite/agent/v3 v3.55.0
 	github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21
 	github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7
 	github.com/depcheck-test/depcheck-test v0.0.0-20220607135614-199033aaa936
@@ -51,7 +51,7 @@ require (
 	golang.org/x/oauth2 v0.12.0
 	golang.org/x/sync v0.3.0
 	golang.org/x/term v0.12.0
-	google.golang.org/api v0.139.0
+	google.golang.org/api v0.141.0
 	k8s.io/api v0.27.3
 	k8s.io/apimachinery v0.27.3
 	k8s.io/client-go v0.27.3
@@ -105,7 +105,7 @@ require (
 	github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
 	github.com/aliyun/credentials-go v1.2.3 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
-	github.com/aws/aws-sdk-go v1.45.4 // indirect
+	github.com/aws/aws-sdk-go v1.45.6 // indirect
 	github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect
 	github.com/aws/aws-sdk-go-v2/config v1.18.37 // indirect
 	github.com/aws/aws-sdk-go-v2/credentials v1.13.35 // indirect
@@ -177,7 +177,7 @@ require (
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/google/tink/go v1.7.0 // indirect
-	github.com/google/uuid v1.3.0 // indirect
+	github.com/google/uuid v1.3.1 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
@@ -284,10 +284,10 @@ require (
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230911183012-2d3300fd4832 // indirect
 	google.golang.org/grpc v1.57.0 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
-	gopkg.in/DataDog/dd-trace-go.v1 v1.54.0 // indirect
+	gopkg.in/DataDog/dd-trace-go.v1 v1.54.1 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
diff --git a/go.sum b/go.sum
index 9620b941..20219030 100644
--- a/go.sum
+++ b/go.sum
@@ -182,8 +182,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
 github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
-github.com/aws/aws-sdk-go v1.45.4 h1:6B8oTYNEncxga8EV1C6Q4iJNnpDIqLEigy0v0oh2qYw=
-github.com/aws/aws-sdk-go v1.45.4/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.45.6 h1:Y2isQQBZsnO15dzUQo9YQRThtHgrV200XCH05BRHVJI=
+github.com/aws/aws-sdk-go v1.45.6/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/aws/aws-sdk-go-v2 v1.7.1/go.mod h1:L5LuPC1ZgDr2xQS7AmIec/Jlc7O/Y1u2KxJyNVab250=
 github.com/aws/aws-sdk-go-v2 v1.14.0/go.mod h1:ZA3Y8V0LrlWj63MQAnRHgKf/5QB//LSZCPNWlWrNGLU=
 github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc=
@@ -240,8 +240,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/buildkite/agent/v3 v3.54.0 h1:pJy1XdrUpOlmbS16Yk//Kf0bUjLHYzeeFPOtGavpxMA=
-github.com/buildkite/agent/v3 v3.54.0/go.mod h1:qFKpH/yvwuk/ewxkktg45q/29+WOj9aEohnGhJw3mxo=
+github.com/buildkite/agent/v3 v3.55.0 h1:4NCquMim55OPo0MTzzuxCKBV4mUYukzftbf2RQULBG8=
+github.com/buildkite/agent/v3 v3.55.0/go.mod h1:wsj54vFFUGVlWgecpgsm3FA+tAnKmh0bmi93kE4Xn6A=
 github.com/buildkite/interpolate v0.0.0-20200526001904-07f35b4ae251 h1:k6UDF1uPYOs0iy1HPeotNa155qXRWrzKnqAaGXHLZCE=
 github.com/buildkite/interpolate v0.0.0-20200526001904-07f35b4ae251/go.mod h1:gbPR1gPu9dB96mucYIR7T3B7p/78hRVSOuzIWLHK2Y4=
 github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
@@ -571,8 +571,8 @@ github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcg
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
 github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -1405,8 +1405,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
 google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
 google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.139.0 h1:A1TrCPgMmOiYu0AiNkvQIpIx+D8blHTDcJ5EogkP7LI=
-google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk=
+google.golang.org/api v0.141.0 h1:Df6vfMgDoIM6ss0m7H4MPwFwY87WNXHfBIda/Bmfl4E=
+google.golang.org/api v0.141.0/go.mod h1:iZqLkdPlXKyG0b90eu6KxVSE4D/ccRF2e/doKD2CnQQ=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1456,8 +1456,8 @@ google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWof
 google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=
 google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 h1:nIgk/EEq3/YlnmVVXVnm14rC2oxgs1o0ong4sD/rd44=
 google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230911183012-2d3300fd4832 h1:o4LtQxebKIJ4vkzyhtD2rfUNZ20Zf0ik5YVP5E7G7VE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230911183012-2d3300fd4832/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1492,8 +1492,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
 google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/DataDog/dd-trace-go.v1 v1.54.0 h1:LAFmtVYLnqhsFAsKv3799SYalXD9Hl3K0/pR+3eV/Qc=
-gopkg.in/DataDog/dd-trace-go.v1 v1.54.0/go.mod h1:1JqaWiPl1+vHNYuVNmHOG4HDyHbF84z98BW/hwq8FeU=
+gopkg.in/DataDog/dd-trace-go.v1 v1.54.1 h1:FJyqbLawXX5CgIkEAV/gUliRiYFa4iQDqvT0lfmobrI=
+gopkg.in/DataDog/dd-trace-go.v1 v1.54.1/go.mod h1:1JqaWiPl1+vHNYuVNmHOG4HDyHbF84z98BW/hwq8FeU=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/oci/platform/platform.go b/pkg/oci/platform/platform.go
new file mode 100644
index 00000000..a2939d73
--- /dev/null
+++ b/pkg/oci/platform/platform.go
@@ -0,0 +1,133 @@
+// Copyright 2023 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 platform
+
+import (
+	"fmt"
+	"strings"
+
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/sigstore/cosign/v2/pkg/oci"
+)
+
+type List []struct {
+	Hash     v1.Hash
+	Platform *v1.Platform
+}
+
+func (pl *List) String() string {
+	r := []string{}
+	for _, p := range *pl {
+		r = append(r, p.Platform.String())
+	}
+	return strings.Join(r, ", ")
+}
+
+func GetIndexPlatforms(idx oci.SignedImageIndex) (List, error) {
+	im, err := idx.IndexManifest()
+	if err != nil {
+		return nil, fmt.Errorf("fetching index manifest: %w", err)
+	}
+
+	platforms := List{}
+	for _, m := range im.Manifests {
+		if m.Platform == nil {
+			continue
+		}
+		platforms = append(platforms, struct {
+			Hash     v1.Hash
+			Platform *v1.Platform
+		}{m.Digest, m.Platform})
+	}
+	return platforms, nil
+}
+
+// matchPlatform filters a list of platforms returning only those matching
+// a base. "Based" on ko's internal equivalent while it moves to GGCR.
+// https://github.com/google/ko/blob/e6a7a37e26d82a8b2bb6df991c5a6cf6b2728794/pkg/build/gobuild.go#L1020
+func matchPlatform(base *v1.Platform, list List) List {
+	ret := List{}
+	for _, p := range list {
+		if base.OS != "" && base.OS != p.Platform.OS {
+			continue
+		}
+		if base.Architecture != "" && base.Architecture != p.Platform.Architecture {
+			continue
+		}
+		if base.Variant != "" && base.Variant != p.Platform.Variant {
+			continue
+		}
+
+		if base.OSVersion != "" && p.Platform.OSVersion != base.OSVersion {
+			if base.OS != "windows" {
+				continue
+			} else { //nolint: revive
+				if pcount, bcount := strings.Count(base.OSVersion, "."), strings.Count(p.Platform.OSVersion, "."); pcount == 2 && bcount == 3 {
+					if base.OSVersion != p.Platform.OSVersion[:strings.LastIndex(p.Platform.OSVersion, ".")] {
+						continue
+					}
+				} else {
+					continue
+				}
+			}
+		}
+		ret = append(ret, p)
+	}
+
+	return ret
+}
+
+func SignedEntityForPlatform(se oci.SignedEntity, platform string) (oci.SignedEntity, error) {
+	if platform == "" {
+		// Copy all platforms
+		return se, nil
+	}
+	idx, isIndex := se.(oci.SignedImageIndex)
+
+	// We only allow --platform on multiarch indexes
+	if !isIndex {
+		return nil, fmt.Errorf("specified reference is not a multiarch image")
+	}
+
+	targetPlatform, err := v1.ParsePlatform(platform)
+	if err != nil {
+		return nil, fmt.Errorf("parsing platform: %w", err)
+	}
+	platforms, err := GetIndexPlatforms(idx)
+	if err != nil {
+		return nil, fmt.Errorf("getting available platforms: %w", err)
+	}
+
+	platforms = matchPlatform(targetPlatform, platforms)
+	if len(platforms) == 0 {
+		return nil, fmt.Errorf("unable to find an entity for %s", targetPlatform.String())
+	}
+	if len(platforms) > 1 {
+		return nil, fmt.Errorf(
+			"platform spec matches more than one image architecture: %s",
+			platforms.String(),
+		)
+	}
+
+	nse, err := idx.SignedImage(platforms[0].Hash)
+	if err != nil {
+		return nil, fmt.Errorf("searching for %s image: %w", platforms[0].Hash.String(), err)
+	}
+	if nse == nil {
+		return nil, fmt.Errorf("unable to find image %s", platforms[0].Hash.String())
+	}
+
+	return nse, nil
+}
diff --git a/pkg/oci/remote/remote.go b/pkg/oci/remote/remote.go
index fab0a765..d279d38d 100644
--- a/pkg/oci/remote/remote.go
+++ b/pkg/oci/remote/remote.go
@@ -36,12 +36,24 @@ var (
 	remoteIndex = remote.Index
 	remoteGet   = remote.Get
 	remoteWrite = remote.Write
-
-	// ErrEntityNotFound is the error that SignedEntity returns when the
-	// provided ref does not exist.
-	ErrEntityNotFound = errors.New("entity not found in registry")
 )
 
+// EntityNotFoundError is the error that SignedEntity returns when the
+// provided ref does not exist.
+type EntityNotFoundError struct {
+	baseErr error
+}
+
+func (e *EntityNotFoundError) Error() string {
+	return fmt.Sprintf("entity not found in registry, error: %v", e.baseErr)
+}
+
+func NewEntityNotFoundError(err error) error {
+	return &EntityNotFoundError{
+		baseErr: err,
+	}
+}
+
 // SignedEntity provides access to a remote reference, and its signatures.
 // The SignedEntity will be one of SignedImage or SignedImageIndex.
 func SignedEntity(ref name.Reference, options ...Option) (oci.SignedEntity, error) {
@@ -50,7 +62,7 @@ func SignedEntity(ref name.Reference, options ...Option) (oci.SignedEntity, erro
 	got, err := remoteGet(ref, o.ROpt...)
 	var te *transport.Error
 	if errors.As(err, &te) && te.StatusCode == http.StatusNotFound {
-		return nil, ErrEntityNotFound
+		return nil, NewEntityNotFoundError(err)
 	} else if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/cmd/cosign/cli/attest/attest_blob_test.go b/cmd/cosign/cli/attest/attest_blob_test.go
index 1f40a9e5..a62b0eae 100644
--- a/cmd/cosign/cli/attest/attest_blob_test.go
+++ b/cmd/cosign/cli/attest/attest_blob_test.go
@@ -71,6 +71,16 @@ func writeFile(t *testing.T, td string, blob string, name string) string {
 	return blobPath
 }
 
+func makeSLSA02PredicateFile(t *testing.T, td string) string {
+	predicate := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`
+	return writeFile(t, td, predicate, "predicate02.json")
+}
+
+func makeSLSA1PredicateFile(t *testing.T, td string) string {
+	predicate := `{ "buildDefinition": {}, "runDetails": {} }`
+	return writeFile(t, td, predicate, "predicate1.json")
+}
+
 // TestAttestBlobCmdWithCert verifies the AttestBlobCmd checks
 // that the cmd correctly matches the signing key with the cert
 // provided.
@@ -99,73 +109,78 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) {
 	chainRef := writeFile(t, td, string(pemChain), "chain.pem")
 
 	blob := writeFile(t, td, "foo", "foo.txt")
-	predicate := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`
-	predicateType := "slsaprovenance"
-	predicatePath := writeFile(t, td, predicate, "predicate.json")
 
-	ctx := context.Background()
-	for _, tc := range []struct {
-		name         string
-		keyref       string
-		certref      string
-		certchainref string
-		errString    string
-	}{
-		{
-			name:   "no cert",
-			keyref: keyRef,
-		},
-		{
-			name:    "cert matches key",
-			keyref:  keyRef,
-			certref: certRef,
-		},
-		{
-			name:      "fail: cert no match key",
-			keyref:    keyRef,
-			certref:   subCertPem,
-			errString: "public key in certificate does not match the provided public key",
-		},
-		{
-			name:         "cert chain matches key",
-			keyref:       keyRef,
-			certref:      certRef,
-			certchainref: chainRef,
-		},
-		{
-			name:         "cert chain partial",
-			keyref:       keyRef,
-			certref:      certRef,
-			certchainref: subCertPem,
-		},
-		{
-			name:         "fail: cert chain bad",
-			keyref:       keyRef,
-			certref:      certRef,
-			certchainref: otherRootPem,
-			errString:    "unable to validate certificate chain",
-		},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			at := AttestBlobCommand{
-				KeyOpts:       options.KeyOpts{KeyRef: tc.keyref},
-				CertPath:      tc.certref,
-				CertChainPath: tc.certchainref,
-				PredicatePath: predicatePath,
-				PredicateType: predicateType,
-			}
-			err := at.Exec(ctx, blob)
-			if err != nil {
-				if tc.errString == "" {
-					t.Fatalf("unexpected error %v", err)
-				}
-				if !strings.Contains(err.Error(), tc.errString) {
-					t.Fatalf("expected error %v got %v", tc.errString, err)
-				}
-				return
-			}
-			if tc.errString != "" {
-				t.Fatalf("expected error %v", tc.errString)
+	predicates := map[string]string{}
+	predicates["slsaprovenance"] = makeSLSA02PredicateFile(t, td)
+	predicates["slsaprovenance1"] = makeSLSA1PredicateFile(t, td)
+
+	for predicateType, predicatePath := range predicates {
+		t.Run(predicateType, func(t *testing.T) {
+			ctx := context.Background()
+			for _, tc := range []struct {
+				name         string
+				keyref       string
+				certref      string
+				certchainref string
+				errString    string
+			}{
+				{
+					name:   "no cert",
+					keyref: keyRef,
+				},
+				{
+					name:    "cert matches key",
+					keyref:  keyRef,
+					certref: certRef,
+				},
+				{
+					name:      "fail: cert no match key",
+					keyref:    keyRef,
+					certref:   subCertPem,
+					errString: "public key in certificate does not match the provided public key",
+				},
+				{
+					name:         "cert chain matches key",
+					keyref:       keyRef,
+					certref:      certRef,
+					certchainref: chainRef,
+				},
+				{
+					name:         "cert chain partial",
+					keyref:       keyRef,
+					certref:      certRef,
+					certchainref: subCertPem,
+				},
+				{
+					name:         "fail: cert chain bad",
+					keyref:       keyRef,
+					certref:      certRef,
+					certchainref: otherRootPem,
+					errString:    "unable to validate certificate chain",
+				},
+			} {
+				t.Run(tc.name, func(t *testing.T) {
+					at := AttestBlobCommand{
+						KeyOpts:       options.KeyOpts{KeyRef: tc.keyref},
+						CertPath:      tc.certref,
+						CertChainPath: tc.certchainref,
+						PredicatePath: predicatePath,
+						PredicateType: predicateType,
+					}
+					err := at.Exec(ctx, blob)
+					if err != nil {
+						if tc.errString == "" {
+							t.Fatalf("unexpected error %v", err)
+						}
+						if !strings.Contains(err.Error(), tc.errString) {
+							t.Fatalf("expected error %v got %v", tc.errString, err)
+						}
+						return
+					}
+					if tc.errString != "" {
+						t.Fatalf("expected error %v", tc.errString)
+					}
+				})
 			}
 		})
 	}
@@ -185,59 +200,64 @@ func TestAttestBlob(t *testing.T) {
 	blobPath := writeFile(t, td, string(blob), "foo.txt")
 	digest, _, _ := signature.ComputeDigestForSigning(bytes.NewReader(blob), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384})
 	blobDigest := strings.ToLower(hex.EncodeToString(digest))
-	predicate := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`
-	predicateType := "slsaprovenance"
-	predicatePath := writeFile(t, td, predicate, "predicate.json")
-
-	dssePath := filepath.Join(td, "dsse.intoto.jsonl")
-	at := AttestBlobCommand{
-		KeyOpts:         options.KeyOpts{KeyRef: keyRef},
-		PredicatePath:   predicatePath,
-		PredicateType:   predicateType,
-		OutputSignature: dssePath,
-	}
-	err := at.Exec(ctx, blobPath)
-	if err != nil {
-		t.Fatal(err)
-	}
 
-	// Load the attestation.
-	dsseBytes, _ := os.ReadFile(dssePath)
-	env := &ssldsse.Envelope{}
-	if err := json.Unmarshal(dsseBytes, env); err != nil {
-		t.Fatal(err)
-	}
+	predicates := map[string]string{}
+	predicates["slsaprovenance"] = makeSLSA02PredicateFile(t, td)
+	predicates["slsaprovenance1"] = makeSLSA1PredicateFile(t, td)
 
-	if len(env.Signatures) != 1 {
-		t.Fatalf("expected 1 signature, got %d", len(env.Signatures))
-	}
+	for predicateType, predicatePath := range predicates {
+		t.Run(predicateType, func(t *testing.T) {
+			dssePath := filepath.Join(td, "dsse.intoto.jsonl")
+			at := AttestBlobCommand{
+				KeyOpts:         options.KeyOpts{KeyRef: keyRef},
+				PredicatePath:   predicatePath,
+				PredicateType:   predicateType,
+				OutputSignature: dssePath,
+			}
+			err := at.Exec(ctx, blobPath)
+			if err != nil {
+				t.Fatal(err)
+			}
 
-	// Verify the subject digest
-	decodedPredicate, err := base64.StdEncoding.DecodeString(env.Payload)
-	if err != nil {
-		t.Fatalf("decoding dsse payload: %v", err)
-	}
-	var statement in_toto.Statement
-	if err := json.Unmarshal(decodedPredicate, &statement); err != nil {
-		t.Fatalf("decoding predicate: %v", err)
-	}
-	if statement.Subject == nil || len(statement.Subject) != 1 {
-		t.Fatalf("expected one subject in intoto statement")
-	}
-	if statement.Subject[0].Digest["sha256"] != blobDigest {
-		t.Fatalf("expected matching digest")
-	}
-	if statement.PredicateType != options.PredicateTypeMap[predicateType] {
-		t.Fatalf("expected matching predicate type")
-	}
+			// Load the attestation.
+			dsseBytes, _ := os.ReadFile(dssePath)
+			env := &ssldsse.Envelope{}
+			if err := json.Unmarshal(dsseBytes, env); err != nil {
+				t.Fatal(err)
+			}
 
-	// Load a verifier and DSSE verify
-	verifier, _ := signature.LoadVerifierFromPEMFile(pubKeyRef, crypto.SHA256)
-	dssev, err := ssldsse.NewEnvelopeVerifier(&dsse.VerifierAdapter{SignatureVerifier: verifier})
-	if err != nil {
-		t.Fatalf("new envelope verifier: %v", err)
-	}
-	if _, err := dssev.Verify(ctx, env); err != nil {
-		t.Fatalf("dsse verify: %v", err)
+			if len(env.Signatures) != 1 {
+				t.Fatalf("expected 1 signature, got %d", len(env.Signatures))
+			}
+
+			// Verify the subject digest
+			decodedPredicate, err := base64.StdEncoding.DecodeString(env.Payload)
+			if err != nil {
+				t.Fatalf("decoding dsse payload: %v", err)
+			}
+			var statement in_toto.Statement
+			if err := json.Unmarshal(decodedPredicate, &statement); err != nil {
+				t.Fatalf("decoding predicate: %v", err)
+			}
+			if statement.Subject == nil || len(statement.Subject) != 1 {
+				t.Fatalf("expected one subject in intoto statement")
+			}
+			if statement.Subject[0].Digest["sha256"] != blobDigest {
+				t.Fatalf("expected matching digest")
+			}
+			if statement.PredicateType != options.PredicateTypeMap[predicateType] {
+				t.Fatalf("expected matching predicate type")
+			}
+
+			// Load a verifier and DSSE verify
+			verifier, _ := signature.LoadVerifierFromPEMFile(pubKeyRef, crypto.SHA256)
+			dssev, err := ssldsse.NewEnvelopeVerifier(&dsse.VerifierAdapter{SignatureVerifier: verifier})
+			if err != nil {
+				t.Fatalf("new envelope verifier: %v", err)
+			}
+			if _, err := dssev.Verify(ctx, env); err != nil {
+				t.Fatalf("dsse verify: %v", err)
+			}
+		})
 	}
 }
diff --git a/cmd/cosign/cli/copy/copy_test.go b/cmd/cosign/cli/copy/copy_test.go
index e1b4671f..f89c993b 100644
--- a/cmd/cosign/cli/copy/copy_test.go
+++ b/cmd/cosign/cli/copy/copy_test.go
@@ -33,8 +33,20 @@ func TestCopyAttachmentTagPrefix(t *testing.T) {
 
 	err := CopyCmd(ctx, options.RegistryOptions{
 		RefOpts: refOpts,
-	}, srcImg, destImg, false, true)
+	}, srcImg, destImg, false, true, "")
 	if err == nil {
 		t.Fatal("failed to copy with attachment-tag-prefix")
 	}
 }
+
+func TestCopyPlatformOpt(t *testing.T) {
+	ctx := context.Background()
+
+	srcImg := "alpine"
+	destImg := "test-alpine"
+
+	err := CopyCmd(ctx, options.RegistryOptions{}, srcImg, destImg, false, true, "linux/amd64")
+	if err == nil {
+		t.Fatal("failed to copy with platform")
+	}
+}
diff --git a/cmd/cosign/cli/download/attestation.go b/cmd/cosign/cli/download/attestation.go
index 92a2f660..1861494a 100644
--- a/cmd/cosign/cli/download/attestation.go
+++ b/cmd/cosign/cli/download/attestation.go
@@ -21,10 +21,9 @@ import (
 	"fmt"
 
 	"github.com/google/go-containerregistry/pkg/name"
-	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
 	"github.com/sigstore/cosign/v2/pkg/cosign"
-	"github.com/sigstore/cosign/v2/pkg/oci"
+	"github.com/sigstore/cosign/v2/pkg/oci/platform"
 	ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
 )
 
@@ -51,42 +50,9 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, attOpt
 		return err
 	}
 
-	idx, isIndex := se.(oci.SignedImageIndex)
-
-	// We only allow --platform on multiarch indexes
-	if attOptions.Platform != "" && !isIndex {
-		return fmt.Errorf("specified reference is not a multiarch image")
-	}
-
-	if attOptions.Platform != "" && isIndex {
-		targetPlatform, err := v1.ParsePlatform(attOptions.Platform)
-		if err != nil {
-			return fmt.Errorf("parsing platform: %w", err)
-		}
-		platforms, err := getIndexPlatforms(idx)
-		if err != nil {
-			return fmt.Errorf("getting available platforms: %w", err)
-		}
-
-		platforms = matchPlatform(targetPlatform, platforms)
-		if len(platforms) == 0 {
-			return fmt.Errorf("unable to find an attestation for %s", targetPlatform.String())
-		}
-		if len(platforms) > 1 {
-			return fmt.Errorf(
-				"platform spec matches more than one image architecture: %s",
-				platforms.String(),
-			)
-		}
-
-		nse, err := idx.SignedImage(platforms[0].hash)
-		if err != nil {
-			return fmt.Errorf("searching for %s image: %w", platforms[0].hash.String(), err)
-		}
-		if nse == nil {
-			return fmt.Errorf("unable to find image %s", platforms[0].hash.String())
-		}
-		se = nse
+	se, err = platform.SignedEntityForPlatform(se, attOptions.Platform)
+	if err != nil {
+		return err
 	}
 
 	attestations, err := cosign.FetchAttestations(se, predicateType)
diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go
index 55971abd..71eaaf0d 100644
--- a/cmd/cosign/cli/verify/verify_attestation.go
+++ b/cmd/cosign/cli/verify/verify_attestation.go
@@ -110,7 +110,8 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 	if c.CheckClaims {
 		co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
 	}
-	if !c.IgnoreSCT {
+	// Ignore Signed Certificate Timestamp if the flag is set or a key is provided
+	if !c.IgnoreSCT || c.KeyRef != "" {
 		co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
 		if err != nil {
 			return fmt.Errorf("getting ctlog public keys: %w", err)
diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go
index d629a9c3..079af421 100644
--- a/cmd/cosign/cli/verify/verify_blob_attestation.go
+++ b/cmd/cosign/cli/verify/verify_blob_attestation.go
@@ -189,7 +189,8 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st
 			}
 		}
 	}
-	if !c.IgnoreSCT {
+	// Ignore Signed Certificate Timestamp if the flag is set or a key is provided
+	if !c.IgnoreSCT || c.KeyRef != "" {
 		co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
 		if err != nil {
 			return fmt.Errorf("getting ctlog public keys: %w", err)
diff --git a/doc/cosign_attest-blob.md b/doc/cosign_attest-blob.md
index e620f4e3..e6d95e11 100644
--- a/doc/cosign_attest-blob.md
+++ b/doc/cosign_attest-blob.md
@@ -58,7 +58,7 @@ cosign attest-blob [flags]
       --slot string                       security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)
       --timestamp-server-url string       url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr
       --tlog-upload                       whether or not to upload to the tlog (default true)
-      --type string                       specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom")
+      --type string                       specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom")
   -y, --yes                               skip confirmation prompts for non-destructive operations
 ```
 
diff --git a/doc/cosign_attest.md b/doc/cosign_attest.md
index bf7c25e4..a613bcf5 100644
--- a/doc/cosign_attest.md
+++ b/doc/cosign_attest.md
@@ -68,7 +68,7 @@ cosign attest [flags]
       --slot string                                                                              security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)
       --timestamp-server-url string                                                              url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr
       --tlog-upload                                                                              whether or not to upload to the tlog (default true)
-      --type string                                                                              specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom")
+      --type string                                                                              specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom")
   -y, --yes                                                                                      skip confirmation prompts for non-destructive operations
 ```
 
diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md
index 61725949..f30a071f 100644
--- a/doc/cosign_verify-attestation.md
+++ b/doc/cosign_verify-attestation.md
@@ -86,7 +86,7 @@ cosign verify-attestation [flags]
       --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)
       --timestamp-certificate-chain string                                                       path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp
-      --type string                                                                              specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom")
+      --type string                                                                              specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom")
 ```
 
 ### Options inherited from parent commands
diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md
index fc2b8cf8..526e6189 100644
--- a/doc/cosign_verify-blob-attestation.md
+++ b/doc/cosign_verify-blob-attestation.md
@@ -54,7 +54,7 @@ cosign verify-blob-attestation [flags]
       --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)
       --timestamp-certificate-chain string              path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp
-      --type string                                     specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom")
+      --type string                                     specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom")
 ```
 
 ### Options inherited from parent commands
diff --git a/pkg/cosign/attestation/attestation.go b/pkg/cosign/attestation/attestation.go
index 52a20080..5c454c1b 100644
--- a/pkg/cosign/attestation/attestation.go
+++ b/pkg/cosign/attestation/attestation.go
@@ -23,7 +23,8 @@ import (
 	"strings"
 	"time"
 
-	slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
+	slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
+	slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
 
 	"github.com/in-toto/in-toto-golang/in_toto"
 )
@@ -99,7 +100,7 @@ type GenerateOpts struct {
 }
 
 // GenerateStatement returns an in-toto statement based on the provided
-// predicate type (custom|slsaprovenance|spdx|spdxjson|cyclonedx|link).
+// predicate type (custom|slsaprovenance|slsaprovenance02|slsaprovenance1|spdx|spdxjson|cyclonedx|link).
 func GenerateStatement(opts GenerateOpts) (interface{}, error) {
 	predicate, err := io.ReadAll(opts.Predicate)
 	if err != nil {
@@ -108,7 +109,11 @@ func GenerateStatement(opts GenerateOpts) (interface{}, error) {
 
 	switch opts.Type {
 	case "slsaprovenance":
-		return generateSLSAProvenanceStatement(predicate, opts.Digest, opts.Repo)
+		return generateSLSAProvenanceStatementSLSA02(predicate, opts.Digest, opts.Repo)
+	case "slsaprovenance02":
+		return generateSLSAProvenanceStatementSLSA02(predicate, opts.Digest, opts.Repo)
+	case "slsaprovenance1":
+		return generateSLSAProvenanceStatementSLSA1(predicate, opts.Digest, opts.Repo)
 	case "spdx":
 		return generateSPDXStatement(predicate, opts.Digest, opts.Repo, false)
 	case "spdxjson":
@@ -198,8 +203,8 @@ func generateCustomPredicate(rawPayload []byte, customType, timestamp string) (i
 	return result, nil
 }
 
-func generateSLSAProvenanceStatement(rawPayload []byte, digest string, repo string) (interface{}, error) {
-	var predicate slsa.ProvenancePredicate
+func generateSLSAProvenanceStatementSLSA02(rawPayload []byte, digest string, repo string) (interface{}, error) {
+	var predicate slsa02.ProvenancePredicate
 	err := checkRequiredJSONFields(rawPayload, reflect.TypeOf(predicate))
 	if err != nil {
 		return nil, fmt.Errorf("provenance predicate: %w", err)
@@ -209,7 +214,23 @@ func generateSLSAProvenanceStatement(rawPayload []byte, digest string, repo stri
 		return "", fmt.Errorf("unmarshal Provenance predicate: %w", err)
 	}
 	return in_toto.ProvenanceStatement{
-		StatementHeader: generateStatementHeader(digest, repo, slsa.PredicateSLSAProvenance),
+		StatementHeader: generateStatementHeader(digest, repo, slsa02.PredicateSLSAProvenance),
+		Predicate:       predicate,
+	}, nil
+}
+
+func generateSLSAProvenanceStatementSLSA1(rawPayload []byte, digest string, repo string) (interface{}, error) {
+	var predicate slsa1.ProvenancePredicate
+	err := checkRequiredJSONFields(rawPayload, reflect.TypeOf(predicate))
+	if err != nil {
+		return nil, fmt.Errorf("provenance predicate: %w", err)
+	}
+	err = json.Unmarshal(rawPayload, &predicate)
+	if err != nil {
+		return "", fmt.Errorf("unmarshal Provenance predicate: %w", err)
+	}
+	return in_toto.ProvenanceStatementSLSA1{
+		StatementHeader: generateStatementHeader(digest, repo, slsa1.PredicateSLSAProvenance),
 		Predicate:       predicate,
 	}, nil
 }