Repository: sigstore/cosign
Pull request #4102 (2025-03-10 16:07:20)

chore(deps): bump golang.org/x/term from 0.29.0 to 0.30.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.29.0 to 0.30.0.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang/term/commit/04218fdaf78fa213d4e82c988184a250f6c354c2"><code>04218fd</code></a> go.mod: update golang.org/x dependencies</li>
<li><a href="https://github.com/golang/term/commit/208db03875ede13ab6fe9213b87281e0d4ca7426"><code>208db03</code></a> all: upgrade go directive to at least 1.23.0 [generated]</li>
<li>See full diff in <a href="https://github.com/golang/term/compare/v0.29.0...v0.30.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/term&package-manager=go_modules&previous-version=0.29.0&new-version=0.30.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go
index 6ab0d7ca..33924c0d 100644
--- a/cmd/cosign/cli/options/verify.go
+++ b/cmd/cosign/cli/options/verify.go
@@ -31,6 +31,8 @@ type CommonVerifyOptions struct {
 	ExperimentalOCI11     bool
 	PrivateInfrastructure bool
 	UseSignedTimestamps   bool
+	NewBundleFormat       bool
+	TrustedRootPath       string
 }
 
 func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) {
@@ -42,7 +44,7 @@ func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) {
 			"Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp")
 
 	cmd.Flags().BoolVar(&o.UseSignedTimestamps, "use-signed-timestamps", false,
-		"use signed timestamps if available")
+		"verify rfc3161 timestamps")
 
 	cmd.Flags().BoolVar(&o.IgnoreTlog, "insecure-ignore-tlog", false,
 		"ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts "+
@@ -56,6 +58,13 @@ func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) {
 
 	cmd.Flags().IntVar(&o.MaxWorkers, "max-workers", cosign.DefaultMaxWorkers,
 		"the amount of maximum workers for parallel executions")
+
+	cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "",
+		"Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set.")
+
+	// TODO: have this default to true as a breaking change
+	cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false,
+		"expect the signature/attestation to be packaged in a Sigstore bundle")
 }
 
 // VerifyOptions is the top level wrapper for the `verify` command.
@@ -161,11 +170,9 @@ func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) {
 
 // VerifyBlobOptions is the top level wrapper for the `verify blob` command.
 type VerifyBlobOptions struct {
-	Key             string
-	Signature       string
-	BundlePath      string
-	NewBundleFormat bool
-	TrustedRootPath string
+	Key        string
+	Signature  string
+	BundlePath string
 
 	SecurityKey         SecurityKeyOptions
 	CertVerify          CertVerifyOptions
@@ -193,13 +200,6 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) {
 	cmd.Flags().StringVar(&o.BundlePath, "bundle", "",
 		"path to bundle FILE")
 
-	// TODO: have this default to true as a breaking change
-	cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false,
-		"output bundle in new format that contains all verification material")
-
-	cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "",
-		"path to trusted root FILE")
-
 	cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "",
 		"path to RFC3161 timestamp FILE")
 }
@@ -222,11 +222,9 @@ func (o *VerifyDockerfileOptions) AddFlags(cmd *cobra.Command) {
 
 // VerifyBlobAttestationOptions is the top level wrapper for the `verify-blob-attestation` command.
 type VerifyBlobAttestationOptions struct {
-	Key             string
-	SignaturePath   string
-	BundlePath      string
-	NewBundleFormat bool
-	TrustedRootPath string
+	Key           string
+	SignaturePath string
+	BundlePath    string
 
 	PredicateOptions
 	CheckClaims bool
@@ -258,13 +256,6 @@ func (o *VerifyBlobAttestationOptions) AddFlags(cmd *cobra.Command) {
 	cmd.Flags().StringVar(&o.BundlePath, "bundle", "",
 		"path to bundle FILE")
 
-	// TODO: have this default to true as a breaking change
-	cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false,
-		"output bundle in new format that contains all verification material")
-
-	cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "",
-		"path to trusted root FILE")
-
 	cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true,
 		"if true, verifies the provided blob's sha256 digest exists as an in-toto subject within the attestation. If false, only the DSSE envelope is verified.")
 
diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go
index 1289e7b1..f7bd51fd 100644
--- a/cmd/cosign/cli/sign/sign.go
+++ b/cmd/cosign/cli/sign/sign.go
@@ -599,7 +599,6 @@ func (c *SignerVerifier) Close() {
 
 func (c *SignerVerifier) Bytes(ctx context.Context) ([]byte, error) {
 	if c.Cert != nil {
-		ui.Infof(ctx, "using ephemeral certificate:\n%s", string(c.Cert))
 		return c.Cert, nil
 	}
 
diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go
index d306756c..f79af288 100644
--- a/cmd/cosign/cli/verify.go
+++ b/cmd/cosign/cli/verify.go
@@ -26,7 +26,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
-const ignoreTLogMessage = "Skipping tlog verification is an insecure practice that lacks of transparency and auditability verification for the %s."
+const ignoreTLogMessage = "Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the %s."
 
 func Verify() *cobra.Command {
 	o := &options.VerifyOptions{}
@@ -218,6 +218,7 @@ against the transparency log.`,
 
 			v := &verify.VerifyAttestationCommand{
 				RegistryOptions:              o.Registry,
+				CommonVerifyOptions:          o.CommonVerifyOptions,
 				CheckClaims:                  o.CheckClaims,
 				CertVerifyOptions:            o.CertVerify,
 				CertRef:                      o.CertVerify.Cert,
@@ -333,9 +334,9 @@ The blob may be specified as a path to a file or - for stdin.`,
 				Slot:                 o.SecurityKey.Slot,
 				RekorURL:             o.Rekor.URL,
 				BundlePath:           o.BundlePath,
-				NewBundleFormat:      o.NewBundleFormat,
 				RFC3161TimestampPath: o.RFC3161TimestampPath,
 				TSACertChainPath:     o.CommonVerifyOptions.TSACertChainPath,
+				NewBundleFormat:      o.CommonVerifyOptions.NewBundleFormat,
 			}
 			verifyBlobCmd := &verify.VerifyBlobCmd{
 				KeyOpts:                      ko,
@@ -345,7 +346,6 @@ The blob may be specified as a path to a file or - for stdin.`,
 				CARoots:                      o.CertVerify.CARoots,
 				CAIntermediates:              o.CertVerify.CAIntermediates,
 				SigRef:                       o.Signature,
-				TrustedRootPath:              o.TrustedRootPath,
 				CertGithubWorkflowTrigger:    o.CertVerify.CertGithubWorkflowTrigger,
 				CertGithubWorkflowSHA:        o.CertVerify.CertGithubWorkflowSha,
 				CertGithubWorkflowName:       o.CertVerify.CertGithubWorkflowName,
@@ -356,6 +356,7 @@ The blob may be specified as a path to a file or - for stdin.`,
 				Offline:                      o.CommonVerifyOptions.Offline,
 				IgnoreTlog:                   o.CommonVerifyOptions.IgnoreTlog,
 				UseSignedTimestamps:          o.CommonVerifyOptions.UseSignedTimestamps,
+				TrustedRootPath:              o.CommonVerifyOptions.TrustedRootPath,
 			}
 
 			ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout)
@@ -404,9 +405,9 @@ The blob may be specified as a path to a file.`,
 				Slot:                 o.SecurityKey.Slot,
 				RekorURL:             o.Rekor.URL,
 				BundlePath:           o.BundlePath,
-				NewBundleFormat:      o.NewBundleFormat,
 				RFC3161TimestampPath: o.RFC3161TimestampPath,
 				TSACertChainPath:     o.CommonVerifyOptions.TSACertChainPath,
+				NewBundleFormat:      o.CommonVerifyOptions.NewBundleFormat,
 			}
 			v := verify.VerifyBlobAttestationCommand{
 				KeyOpts:                      ko,
@@ -414,7 +415,6 @@ The blob may be specified as a path to a file.`,
 				CheckClaims:                  o.CheckClaims,
 				SignaturePath:                o.SignaturePath,
 				CertVerifyOptions:            o.CertVerify,
-				TrustedRootPath:              o.TrustedRootPath,
 				CertRef:                      o.CertVerify.Cert,
 				CertChain:                    o.CertVerify.CertChain,
 				CARoots:                      o.CertVerify.CARoots,
@@ -429,6 +429,7 @@ The blob may be specified as a path to a file.`,
 				Offline:                      o.CommonVerifyOptions.Offline,
 				IgnoreTlog:                   o.CommonVerifyOptions.IgnoreTlog,
 				UseSignedTimestamps:          o.CommonVerifyOptions.UseSignedTimestamps,
+				TrustedRootPath:              o.CommonVerifyOptions.TrustedRootPath,
 			}
 			// We only use the blob if we are checking claims.
 			if len(args) == 0 && o.CheckClaims {
diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go
index f5fc86bf..9d79846d 100644
--- a/cmd/cosign/cli/verify/verify.go
+++ b/cmd/cosign/cli/verify/verify.go
@@ -41,6 +41,7 @@ import (
 	"github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key"
 	"github.com/sigstore/cosign/v2/pkg/oci"
 	sigs "github.com/sigstore/cosign/v2/pkg/signature"
+	"github.com/sigstore/sigstore-go/pkg/root"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 	"github.com/sigstore/sigstore/pkg/signature"
 	"github.com/sigstore/sigstore/pkg/signature/payload"
@@ -51,6 +52,7 @@ import (
 type VerifyCommand struct {
 	options.RegistryOptions
 	options.CertVerifyOptions
+	options.CommonVerifyOptions
 	CheckClaims                  bool
 	KeyRef                       string
 	CertRef                      string
@@ -134,7 +136,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
 		MaxWorkers:                   c.MaxWorkers,
 		ExperimentalOCI11:            c.ExperimentalOCI11,
 		UseSignedTimestamps:          c.TSACertChainPath != "" || c.UseSignedTimestamps,
+		NewBundleFormat:              c.NewBundleFormat,
 	}
+
+	if c.TrustedRootPath != "" {
+		co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
+		if err != nil {
+			return fmt.Errorf("loading trusted root: %w", err)
+		}
+	}
+
 	if c.CheckClaims {
 		co.ClaimVerifier = cosign.SimpleClaimVerifier
 	}
diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md
index 0b64d614..3ee9b0d3 100644
--- a/doc/cosign_dockerfile_verify.md
+++ b/doc/cosign_dockerfile_verify.md
@@ -77,6 +77,7 @@ cosign dockerfile verify [flags]
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
       --local-image                                                                              whether the specified image is a path to an image saved locally via 'cosign save'
       --max-workers int                                                                          the amount of maximum workers for parallel executions (default 10)
+      --new-bundle-format                                                                        expect the signature/attestation to be packaged in a Sigstore bundle
       --offline                                                                                  only allow offline verification
   -o, --output string                                                                            output format for the signing image information (json|text) (default "json")
       --payload string                                                                           payload path or remote URL
@@ -95,7 +96,8 @@ cosign dockerfile verify [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
-      --use-signed-timestamps                                                                    use signed timestamps if available
+      --trusted-root string                                                                      Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set.
+      --use-signed-timestamps                                                                    verify rfc3161 timestamps
 ```
 
 ### Options inherited from parent commands
diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md
index 4bd31816..0914e691 100644
--- a/doc/cosign_manifest_verify.md
+++ b/doc/cosign_manifest_verify.md
@@ -71,6 +71,7 @@ cosign manifest verify [flags]
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
       --local-image                                                                              whether the specified image is a path to an image saved locally via 'cosign save'
       --max-workers int                                                                          the amount of maximum workers for parallel executions (default 10)
+      --new-bundle-format                                                                        expect the signature/attestation to be packaged in a Sigstore bundle
       --offline                                                                                  only allow offline verification
   -o, --output string                                                                            output format for the signing image information (json|text) (default "json")
       --payload string                                                                           payload path or remote URL
@@ -89,7 +90,8 @@ cosign manifest verify [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
-      --use-signed-timestamps                                                                    use signed timestamps if available
+      --trusted-root string                                                                      Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set.
+      --use-signed-timestamps                                                                    verify rfc3161 timestamps
 ```
 
 ### Options inherited from parent commands
diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md
index 92655ccc..cb966881 100644
--- a/doc/cosign_verify-blob.md
+++ b/doc/cosign_verify-blob.md
@@ -84,7 +84,7 @@ cosign verify-blob [flags]
       --insecure-ignore-tlog                            ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log
       --key string                                      path to the public key file, KMS URI or Kubernetes Secret
       --max-workers int                                 the amount of maximum workers for parallel executions (default 10)
-      --new-bundle-format                               output bundle in new format that contains all verification material
+      --new-bundle-format                               expect the signature/attestation to be packaged in a Sigstore bundle
       --offline                                         only allow offline verification
       --private-infrastructure                          skip transparency log verification when verifying artifacts in a privately deployed infrastructure
       --rekor-url string                                address of rekor STL server (default "https://rekor.sigstore.dev")
@@ -94,8 +94,8 @@ cosign verify-blob [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
-      --trusted-root string                             path to trusted root FILE
-      --use-signed-timestamps                           use signed timestamps if available
+      --trusted-root string                             Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set.
+      --use-signed-timestamps                           verify rfc3161 timestamps
 ```
 
 ### Options inherited from parent commands
diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md
index de9e10dd..42ee11c7 100644
--- a/doc/cosign_verify.md
+++ b/doc/cosign_verify.md
@@ -98,6 +98,7 @@ cosign verify [flags]
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
       --local-image                                                                              whether the specified image is a path to an image saved locally via 'cosign save'
       --max-workers int                                                                          the amount of maximum workers for parallel executions (default 10)
+      --new-bundle-format                                                                        expect the signature/attestation to be packaged in a Sigstore bundle
       --offline                                                                                  only allow offline verification
   -o, --output string                                                                            output format for the signing image information (json|text) (default "json")
       --payload string                                                                           payload path or remote URL
@@ -116,7 +117,8 @@ cosign verify [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
-      --use-signed-timestamps                                                                    use signed timestamps if available
+      --trusted-root string                                                                      Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set.
+      --use-signed-timestamps                                                                    verify rfc3161 timestamps
 ```
 
 ### Options inherited from parent commands
diff --git a/go.mod b/go.mod
index 5d765f28..ce6e8589 100644
--- a/go.mod
+++ b/go.mod
@@ -53,7 +53,7 @@ require (
 	golang.org/x/crypto v0.35.0
 	golang.org/x/oauth2 v0.27.0
 	golang.org/x/sync v0.11.0
-	golang.org/x/term v0.29.0
+	golang.org/x/term v0.30.0
 	google.golang.org/api v0.223.0
 	google.golang.org/protobuf v1.36.5
 	k8s.io/api v0.28.3
@@ -261,7 +261,7 @@ require (
 	golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
 	golang.org/x/mod v0.22.0 // indirect
 	golang.org/x/net v0.35.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
 	golang.org/x/text v0.22.0 // indirect
 	golang.org/x/time v0.10.0 // indirect
 	golang.org/x/tools v0.29.0 // indirect
diff --git a/go.sum b/go.sum
index 74b767a3..3ef52ebf 100644
--- a/go.sum
+++ b/go.sum
@@ -860,8 +860,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -871,8 +871,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go
index e565052d..b2c9ee66 100644
--- a/pkg/cosign/verify.go
+++ b/pkg/cosign/verify.go
@@ -38,6 +38,7 @@ import (
 	"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
 	"github.com/digitorus/timestamp"
 	"github.com/go-openapi/runtime"
+
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
@@ -53,6 +54,7 @@ import (
 	ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
 	"github.com/sigstore/cosign/v2/pkg/oci/static"
 	"github.com/sigstore/cosign/v2/pkg/types"
+	protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
 	"github.com/sigstore/rekor/pkg/generated/client"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	rekor_types "github.com/sigstore/rekor/pkg/types"
@@ -61,6 +63,7 @@ import (
 	intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
 	intoto_v002 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.2"
 	rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
+	sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
 	"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
 	"github.com/sigstore/sigstore-go/pkg/root"
 	"github.com/sigstore/sigstore-go/pkg/verify"
@@ -179,11 +182,18 @@ type verifyTrustedMaterial struct {
 }
 
 func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) {
+	if v.keyTrustedMaterial == nil {
+		return nil, fmt.Errorf("no public key material available")
+	}
 	return v.keyTrustedMaterial.PublicKeyVerifier(hint)
 }
 
 // verificationOptions returns the verification options for verifying with sigstore-go.
 func (co *CheckOpts) verificationOptions() (trustedMaterial root.TrustedMaterial, verifierOptions []verify.VerifierOption, policyOptions []verify.PolicyOption, err error) {
+	if co.TrustedMaterial == nil {
+		return nil, nil, nil, fmt.Errorf("TrustMaterial is required")
+	}
+
 	policyOptions = make([]verify.PolicyOption, 0)
 
 	if len(co.Identities) > 0 {
@@ -579,6 +589,10 @@ func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co
 		}
 	}
 
+	if co.NewBundleFormat {
+		return nil, false, errors.New("bundle support for image signatures is not yet implemented")
+	}
+
 	// Enforce this up front.
 	if co.RootCerts == nil && co.SigVerifier == nil {
 		return nil, false, errors.New("one of verifier or root certs is required")
@@ -967,8 +981,11 @@ func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name
 // If there were no valid attestations, we return an error.
 func VerifyImageAttestations(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) {
 	// Enforce this up front.
-	if co.RootCerts == nil && co.SigVerifier == nil {
-		return nil, false, errors.New("one of verifier or root certs is required")
+	if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil {
+		return nil, false, errors.New("one of verifier, root certs, or TrustedMaterial is required")
+	}
+	if co.NewBundleFormat {
+		return verifyImageAttestationsSigstoreBundle(ctx, signedImgRef, co)
 	}
 
 	// This is a carefully optimized sequence for fetching the attestations of
@@ -1500,3 +1517,133 @@ func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name
 
 	return verifySignatures(ctx, sigs, h, co)
 }
+
+func getBundles(_ context.Context, signedImgRef name.Reference, co *CheckOpts) ([]*sgbundle.Bundle, *v1.Hash, error) {
+	// This is a carefully optimized sequence for fetching the signatures of the
+	// entity that minimizes registry requests when supplied with a digest input
+	digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...)
+	if err != nil {
+		if terr := (&transport.Error{}); errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
+			return nil, nil, &ErrImageTagNotFound{
+				fmt.Errorf("image tag not found: %w", err),
+			}
+		}
+		return nil, nil, err
+	}
+	h, err := v1.NewHash(digest.Identifier())
+	if err != nil {
+		return nil, nil, err
+	}
+
+	index, err := ociremote.Referrers(digest, "", co.RegistryClientOpts...)
+	if err != nil {
+		return nil, nil, err
+	}
+	var bundles = make([]*sgbundle.Bundle, 0, len(index.Manifests))
+	for _, result := range index.Manifests {
+		st, err := name.ParseReference(fmt.Sprintf("%s@%s", digest.Repository, result.Digest.String()))
+		if err != nil {
+			return nil, nil, err
+		}
+		bundle, err := ociremote.Bundle(st, co.RegistryClientOpts...)
+		if err != nil {
+			// There may be non-Sigstore referrers in the index, so we can ignore them.
+			// TODO: Should we surface any errors here (e.g. if the bundle is invalid)?
+			continue
+		}
+		bundles = append(bundles, bundle)
+	}
+
+	if len(bundles) == 0 {
+		return nil, nil, &ErrNoMatchingAttestations{
+			fmt.Errorf("no valid bundles exist in registry"),
+		}
+	}
+
+	return bundles, &h, nil
+}
+
+// verifyImageAttestationsSigstoreBundle verifies attestations from attached sigstore bundles
+func verifyImageAttestationsSigstoreBundle(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedAttestations []oci.Signature, atLeastOneBundleVerified bool, err error) {
+	bundles, hash, err := getBundles(ctx, signedImgRef, co)
+	if err != nil {
+		return nil, false, err
+	}
+
+	digestBytes, err := hex.DecodeString(hash.Hex)
+	if err != nil {
+		return nil, false, err
+	}
+
+	artifactPolicyOption := verify.WithArtifactDigest(hash.Algorithm, digestBytes)
+
+	attestations := make([]oci.Signature, len(bundles))
+	bundlesVerified := make([]bool, len(bundles))
+
+	workers := co.MaxWorkers
+	if co.MaxWorkers == 0 {
+		workers = cosign.DefaultMaxWorkers
+	}
+	t := throttler.New(workers, len(bundles))
+	for i, bundle := range bundles {
+		go func(bundle *sgbundle.Bundle, index int) {
+			var att oci.Signature
+			if err := func(bundle *sgbundle.Bundle) error {
+				_, err := VerifyNewBundle(ctx, co, artifactPolicyOption, bundle)
+				if err != nil {
+					return err
+				}
+				dsse, ok := bundle.Content.(*protobundle.Bundle_DsseEnvelope)
+				if !ok {
+					return fmt.Errorf("bundle does not contain a DSSE envelope")
+				}
+				payload, err := json.Marshal(dsse.DsseEnvelope)
+				if err != nil {
+					return fmt.Errorf("marshaling DSSE envelope: %w", err)
+				}
+
+				// We will return a slice of `[]oci.Signature` from this function for compatibility
+				// with the rest of the codebase. To do that, we wrap the verification output in a
+				// `oci.Signature` using static.NewAttestation(). This type may contain additional
+				// data such as the certificate chain, and rekor/tsa data, but for now we only use
+				// the payload (DSSE). TODO: Add additional data to returned `oci.Signature`. This
+				// can be done by passing a list of static.Option to NewAttestation (e.g. static.WithCertChain()).
+				// Depends on https://github.com/sigstore/sigstore-go/issues/328
+				att, err = static.NewAttestation(payload)
+				if err != nil {
+					return err
+				}
+				bundlesVerified[index] = true
+
+				return err
+			}(bundle); err != nil {
+				t.Done(err)
+				return
+			}
+
+			attestations[index] = att
+			t.Done(nil)
+		}(bundle, i)
+
+		// wait till workers are available
+		t.Throttle()
+	}
+
+	for _, a := range attestations {
+		if a != nil {
+			checkedAttestations = append(checkedAttestations, a)
+		}
+	}
+
+	for _, verified := range bundlesVerified {
+		atLeastOneBundleVerified = atLeastOneBundleVerified || verified
+	}
+
+	if len(checkedAttestations) == 0 {
+		return nil, false, &ErrNoMatchingAttestations{
+			fmt.Errorf("no matching attestations: %w", errors.Join(t.Errs()...)),
+		}
+	}
+
+	return checkedAttestations, atLeastOneBundleVerified, nil
+}
diff --git a/pkg/oci/remote/referrers.go b/pkg/oci/remote/referrers.go
index 24cb802e..1b67cdf3 100644
--- a/pkg/oci/remote/referrers.go
+++ b/pkg/oci/remote/referrers.go
@@ -25,7 +25,9 @@ import (
 func Referrers(d name.Digest, artifactType string, opts ...Option) (*v1.IndexManifest, error) {
 	o := makeOptions(name.Repository{}, opts...)
 	rOpt := o.ROpt
-	rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType))
+	if artifactType != "" {
+		rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType))
+	}
 	idx, err := remote.Referrers(d, rOpt...)
 	if err != nil {
 		return nil, err
diff --git a/pkg/oci/remote/signatures.go b/pkg/oci/remote/signatures.go
index 825d8072..c24e3a20 100644
--- a/pkg/oci/remote/signatures.go
+++ b/pkg/oci/remote/signatures.go
@@ -17,7 +17,9 @@ package remote
 
 import (
 	"errors"
+	"io"
 	"net/http"
+	"strings"
 
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -26,6 +28,7 @@ import (
 	"github.com/sigstore/cosign/v2/pkg/oci"
 	"github.com/sigstore/cosign/v2/pkg/oci/empty"
 	"github.com/sigstore/cosign/v2/pkg/oci/internal/signature"
+	sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
 )
 
 const maxLayers = 1000
@@ -49,6 +52,45 @@ func Signatures(ref name.Reference, opts ...Option) (oci.Signatures, error) {
 	}, nil
 }
 
+func Bundle(ref name.Reference, opts ...Option) (*sgbundle.Bundle, error) {
+	o := makeOptions(ref.Context(), opts...)
+	img, err := remoteImage(ref, o.ROpt...)
+	if err != nil {
+		return nil, err
+	}
+	layers, err := img.Layers()
+	if err != nil {
+		return nil, err
+	}
+	if len(layers) != 1 {
+		return nil, errors.New("expected exactly one layer")
+	}
+	mediaType, err := layers[0].MediaType()
+	if err != nil {
+		return nil, err
+	}
+	if !strings.HasPrefix(string(mediaType), "application/vnd.dev.sigstore.bundle") {
+		return nil, errors.New("expected bundle layer")
+	}
+	layer0, err := layers[0].Uncompressed()
+	if err != nil {
+		return nil, err
+	}
+	bundleBytes, err := io.ReadAll(layer0)
+	if err != nil {
+		return nil, err
+	}
+	b := &sgbundle.Bundle{}
+	err = b.UnmarshalJSON(bundleBytes)
+	if err != nil {
+		return nil, err
+	}
+	if !b.MinVersion("v0.3") {
+		return nil, errors.New("bundle version too old")
+	}
+	return b, nil
+}
+
 type sigs struct {
 	v1.Image
 }
diff --git a/pkg/oci/remote/write.go b/pkg/oci/remote/write.go
index d0614768..d353c6b0 100644
--- a/pkg/oci/remote/write.go
+++ b/pkg/oci/remote/write.go
@@ -20,6 +20,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"os"
+	"time"
 
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -29,6 +30,7 @@ import (
 	ociexperimental "github.com/sigstore/cosign/v2/internal/pkg/oci/remote"
 	"github.com/sigstore/cosign/v2/pkg/oci"
 	ctypes "github.com/sigstore/cosign/v2/pkg/types"
+	sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
 )
 
 // WriteSignedImageIndexImages writes the images within the image index
@@ -221,3 +223,126 @@ func (taggable taggableManifest) RawManifest() ([]byte, error) {
 func (taggable taggableManifest) MediaType() (types.MediaType, error) {
 	return taggable.mediaType, nil
 }
+
+func WriteAttestationNewBundleFormat(d name.Digest, bundleBytes []byte, predicateType string, opts ...Option) error {
+	o := makeOptions(d.Repository, opts...)
+
+	signTarget := d.String()
+	ref, err := name.ParseReference(signTarget, o.NameOpts...)
+	if err != nil {
+		return err
+	}
+	desc, err := remote.Head(ref, o.ROpt...)
+	if err != nil {
+		return err
+	}
+
+	// Write the empty config layer
+	configLayer := static.NewLayer([]byte("{}"), "application/vnd.oci.image.config.v1+json")
+	configDigest, err := configLayer.Digest()
+	if err != nil {
+		return fmt.Errorf("failed to calculate digest: %w", err)
+	}
+	configSize, err := configLayer.Size()
+	if err != nil {
+		return fmt.Errorf("failed to calculate size: %w", err)
+	}
+	err = remote.WriteLayer(d.Repository, configLayer, o.ROpt...)
+	if err != nil {
+		return fmt.Errorf("failed to upload layer: %w", err)
+	}
+
+	// generate bundle media type string
+	bundleMediaType, err := sgbundle.MediaTypeString("0.3")
+	if err != nil {
+		return fmt.Errorf("failed to generate bundle media type string: %w", err)
+	}
+
+	// Write the bundle layer
+	layer := static.NewLayer(bundleBytes, types.MediaType(bundleMediaType))
+	blobDigest, err := layer.Digest()
+	if err != nil {
+		return fmt.Errorf("failed to calculate digest: %w", err)
+	}
+
+	blobSize, err := layer.Size()
+	if err != nil {
+		return fmt.Errorf("failed to calculate size: %w", err)
+	}
+
+	err = remote.WriteLayer(d.Repository, layer, o.ROpt...)
+	if err != nil {
+		return fmt.Errorf("failed to upload layer: %w", err)
+	}
+
+	// Create a manifest that includes the blob as a layer
+	manifest := referrerManifest{v1.Manifest{
+		SchemaVersion: 2,
+		MediaType:     types.OCIManifestSchema1,
+		Config: v1.Descriptor{
+			MediaType:    types.MediaType("application/vnd.oci.empty.v1+json"),
+			ArtifactType: bundleMediaType,
+			Digest:       configDigest,
+			Size:         configSize,
+		},
+		Layers: []v1.Descriptor{
+			{
+				MediaType: types.MediaType(bundleMediaType),
+				Digest:    blobDigest,
+				Size:      blobSize,
+			},
+		},
+		Subject: &v1.Descriptor{
+			MediaType: desc.MediaType,
+			Digest:    desc.Digest,
+			Size:      desc.Size,
+		},
+		Annotations: map[string]string{
+			"org.opencontainers.image.created":  time.Now().UTC().Format(time.RFC3339),
+			"dev.sigstore.bundle.content":       "dsse-envelope",
+			"dev.sigstore.bundle.predicateType": predicateType,
+		},
+	}, bundleMediaType}
+
+	targetRef, err := manifest.targetRef(d.Repository)
+	if err != nil {
+		return fmt.Errorf("failed to create target reference: %w", err)
+	}
+
+	if err := remote.Put(targetRef, manifest, o.ROpt...); err != nil {
+		return fmt.Errorf("failed to upload manifest: %w", err)
+	}
+
+	return nil
+}
+
+// referrerManifest implements Taggable for use in remote.Put.
+// This type also augments the built-in v1.Manifest with an ArtifactType field
+// which is part of the OCI 1.1 Image Manifest spec but is unsupported by
+// go-containerregistry at this time.
+// See https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#image-manifest-property-descriptions
+// and https://github.com/google/go-containerregistry/pull/1931
+type referrerManifest struct {
+	v1.Manifest
+	ArtifactType string `json:"artifactType,omitempty"`
+}
+
+func (r referrerManifest) RawManifest() ([]byte, error) {
+	return json.Marshal(r)
+}
+
+func (r referrerManifest) targetRef(repo name.Repository) (name.Reference, error) {
+	manifestBytes, err := r.RawManifest()
+	if err != nil {
+		return nil, err
+	}
+	digest, _, err := v1.SHA256(bytes.NewReader(manifestBytes))
+	if err != nil {
+		return nil, err
+	}
+	return name.ParseReference(fmt.Sprintf("%s/%s@%s", repo.RegistryStr(), repo.RepositoryStr(), digest.String()))
+}
+
+func (r referrerManifest) MediaType() (types.MediaType, error) {
+	return types.OCIManifestSchema1, nil
+}


Test patch:
diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go
index 63e8d4b3..8681e41a 100644
--- a/cmd/cosign/cli/attest.go
+++ b/cmd/cosign/cli/attest.go
@@ -91,6 +91,7 @@ func Attest() *cobra.Command {
 				TSAClientCert:            o.TSAClientCert,
 				TSAServerName:            o.TSAServerName,
 				TSAServerURL:             o.TSAServerURL,
+				NewBundleFormat:          o.NewBundleFormat,
 			}
 			attestCommand := attest.AttestCommand{
 				KeyOpts:                 ko,
diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go
index f468c043..6a844372 100644
--- a/cmd/cosign/cli/attest/attest.go
+++ b/cmd/cosign/cli/attest/attest.go
@@ -49,7 +49,7 @@ import (
 
 type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)
 
-func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
+func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*models.LogEntryAnon, error) {
 	rekorBytes, err := sv.Bytes(ctx)
 	if err != nil {
 		return nil, err
@@ -64,7 +64,7 @@ func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string,
 		return nil, err
 	}
 	fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex)
-	return cbundle.EntryToBundle(entry), nil
+	return entry, nil
 }
 
 // nolint
@@ -174,15 +174,23 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
 	if sv.Cert != nil {
 		opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
 	}
+	var timestampBytes []byte
+	var tsaPayload []byte
 	if c.KeyOpts.TSAServerURL != "" {
-		// TODO - change this when we implement protobuf / new bundle support
+		// We need to decide what signature to send to the timestamp authority.
 		//
-		// Historically, cosign sent the entire JSON DSSE Envelope to the
-		// timestamp authority. However, when sigstore clients are verifying a
-		// bundle they will use the DSSE Sig field, so we choose what signature
-		// to send to the timestamp authority based on our output format.
-		//
-		// See cmd/cosign/cli/attest/attest_blob.go
+		// Historically, cosign sent `signedPayload`, which is the entire JSON DSSE
+		// Envelope. However, when sigstore clients are verifying a bundle they
+		// will use the DSSE Sig field, so we choose what signature to send to
+		// the timestamp authority based on our output format.
+		if c.KeyOpts.NewBundleFormat {
+			tsaPayload, err = getEnvelopeSigBytes(signedPayload)
+			if err != nil {
+				return err
+			}
+		} else {
+			tsaPayload = signedPayload
+		}
 		tc := tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL)
 		if c.KeyOpts.TSAClientCert != "" {
 			tc = tsaclient.NewTSAClientMTLS(c.KeyOpts.TSAServerURL,
@@ -192,11 +200,11 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
 				c.KeyOpts.TSAServerName,
 			)
 		}
-		responseBytes, err := tsa.GetTimestampedSignature(signedPayload, tc)
+		timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tc)
 		if err != nil {
 			return err
 		}
-		bundle := cbundle.TimestampToRFC3161Timestamp(responseBytes)
+		bundle := cbundle.TimestampToRFC3161Timestamp(timestampBytes)
 
 		opts = append(opts, static.WithRFC3161Timestamp(bundle))
 	}
@@ -217,8 +225,9 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
 	if err != nil {
 		return fmt.Errorf("should upload to tlog: %w", err)
 	}
+	var rekorEntry *models.LogEntryAnon
 	if shouldUpload {
-		bundle, err := uploadToTlog(ctx, sv, c.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
+		rekorEntry, err = uploadToTlog(ctx, sv, c.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
 			if c.RekorEntryType == "intoto" {
 				return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b)
 			} else {
@@ -229,7 +238,7 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
 		if err != nil {
 			return err
 		}
-		opts = append(opts, static.WithBundle(bundle))
+		opts = append(opts, static.WithBundle(cbundle.EntryToBundle(rekorEntry)))
 	}
 
 	sig, err := static.NewAttestation(signedPayload, opts...)
@@ -237,6 +246,18 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
 		return err
 	}
 
+	if c.KeyOpts.NewBundleFormat {
+		signerBytes, err := sv.Bytes(ctx)
+		if err != nil {
+			return err
+		}
+		bundleBytes, err := makeNewBundle(sv, rekorEntry, payload, signedPayload, signerBytes, timestampBytes)
+		if err != nil {
+			return err
+		}
+		return ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, predicateType, ociremoteOpts...)
+	}
+
 	// We don't actually need to access the remote entity to attach things to it
 	// so we use a placeholder here.
 	se := ociremote.SignedUnknown(digest, ociremoteOpts...)
diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go
index 528da791..8b8d2a3e 100644
--- a/cmd/cosign/cli/attest/attest_blob.go
+++ b/cmd/cosign/cli/attest/attest_blob.go
@@ -163,6 +163,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
 
 	var rfc3161Timestamp *cbundle.RFC3161Timestamp
 	var timestampBytes []byte
+	var tsaPayload []byte
 	var rekorEntry *models.LogEntryAnon
 
 	if c.KeyOpts.TSAServerURL != "" {
@@ -182,28 +183,16 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
 		// will use the DSSE Sig field, so we choose what signature to send to
 		// the timestamp authority based on our output format.
 		if c.NewBundleFormat {
-			var envelope dsse.Envelope
-			err = json.Unmarshal(sig, &envelope)
-			if err != nil {
-				return err
-			}
-			if len(envelope.Signatures) == 0 {
-				return fmt.Errorf("envelope has no signatures")
-			}
-			envelopeSigBytes, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig)
-			if err != nil {
-				return err
-			}
-
-			timestampBytes, err = tsa.GetTimestampedSignature(envelopeSigBytes, tc)
+			tsaPayload, err = getEnvelopeSigBytes(sig)
 			if err != nil {
 				return err
 			}
 		} else {
-			timestampBytes, err = tsa.GetTimestampedSignature(sig, tc)
-			if err != nil {
-				return err
-			}
+			tsaPayload = sig
+		}
+		timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tc)
+		if err != nil {
+			return err
 		}
 		rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes)
 		// TODO: Consider uploading RFC3161 TS to Rekor
diff --git a/cmd/cosign/cli/attest/common.go b/cmd/cosign/cli/attest/common.go
index b9bb6dce..e5f4589a 100644
--- a/cmd/cosign/cli/attest/common.go
+++ b/cmd/cosign/cli/attest/common.go
@@ -15,9 +15,13 @@
 package attest
 
 import (
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"io"
 	"os"
+
+	"github.com/secure-systems-lab/go-securesystemslib/dsse"
 )
 
 func predicateReader(predicatePath string) (io.ReadCloser, error) {
@@ -33,3 +37,15 @@ func predicateReader(predicatePath string) (io.ReadCloser, error) {
 	}
 	return f, nil
 }
+
+func getEnvelopeSigBytes(envelopeBytes []byte) ([]byte, error) {
+	var envelope dsse.Envelope
+	err := json.Unmarshal(envelopeBytes, &envelope)
+	if err != nil {
+		return nil, err
+	}
+	if len(envelope.Signatures) == 0 {
+		return nil, fmt.Errorf("envelope has no signatures")
+	}
+	return base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig)
+}
diff --git a/cmd/cosign/cli/options/attest.go b/cmd/cosign/cli/options/attest.go
index 4602629b..6a4bc702 100644
--- a/cmd/cosign/cli/options/attest.go
+++ b/cmd/cosign/cli/options/attest.go
@@ -36,6 +36,7 @@ type AttestOptions struct {
 	TSAServerURL            string
 	RekorEntryType          string
 	RecordCreationTimestamp bool
+	NewBundleFormat         bool
 
 	Rekor       RekorOptions
 	Fulcio      FulcioOptions
@@ -106,4 +107,6 @@ func (o *AttestOptions) AddFlags(cmd *cobra.Command) {
 
 	cmd.Flags().BoolVar(&o.RecordCreationTimestamp, "record-creation-timestamp", false,
 		"set the createdAt timestamp in the attestation artifact to the time it was created; by default, cosign sets this to the zero value")
+
+	cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false, "attach a Sigstore bundle using OCI referrers API")
 }
diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go
index bf25cbb4..935d5036 100644
--- a/cmd/cosign/cli/verify/verify_attestation.go
+++ b/cmd/cosign/cli/verify/verify_attestation.go
@@ -37,6 +37,7 @@ import (
 	"github.com/sigstore/cosign/v2/pkg/oci"
 	"github.com/sigstore/cosign/v2/pkg/policy"
 	sigs "github.com/sigstore/cosign/v2/pkg/signature"
+	"github.com/sigstore/sigstore-go/pkg/root"
 )
 
 // VerifyAttestationCommand verifies a signature on a supplied container image
@@ -44,6 +45,7 @@ import (
 type VerifyAttestationCommand struct {
 	options.RegistryOptions
 	options.CertVerifyOptions
+	options.CommonVerifyOptions
 	CheckClaims                  bool
 	KeyRef                       string
 	CertRef                      string
@@ -109,12 +111,24 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 		IgnoreTlog:                   c.IgnoreTlog,
 		MaxWorkers:                   c.MaxWorkers,
 		UseSignedTimestamps:          c.TSACertChainPath != "" || c.UseSignedTimestamps,
+		NewBundleFormat:              c.NewBundleFormat,
 	}
 	if c.CheckClaims {
 		co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
 	}
+
+	if c.NewBundleFormat {
+		if err = checkSigstoreBundleUnsupportedOptions(c); err != nil {
+			return err
+		}
+		co.TrustedMaterial, err = loadTrustedRoot(ctx, c.TrustedRootPath)
+		if err != nil {
+			return err
+		}
+	}
+
 	// Ignore Signed Certificate Timestamp if the flag is set or a key is provided
-	if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) {
+	if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) && !c.NewBundleFormat {
 		co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
 		if err != nil {
 			return fmt.Errorf("getting ctlog public keys: %w", err)
@@ -122,7 +136,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 	}
 
 	// If we are using signed timestamps, we need to load the TSA certificates
-	if co.UseSignedTimestamps {
+	if co.UseSignedTimestamps && !c.NewBundleFormat {
 		tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets)
 		if err != nil {
 			return fmt.Errorf("unable to load TSA certificates: %w", err)
@@ -132,7 +146,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 		co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts
 	}
 
-	if !c.IgnoreTlog {
+	if !c.IgnoreTlog && !co.NewBundleFormat {
 		if c.RekorURL != "" {
 			rekorClient, err := rekor.NewClient(c.RekorURL)
 			if err != nil {
@@ -178,6 +192,10 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 			return fmt.Errorf("initializing piv token verifier: %w", err)
 		}
 	case c.CertRef != "":
+		if c.NewBundleFormat {
+			// This shouldn't happen because we already checked for this above in checkSigstoreBundleUnsupportedOptions
+			return fmt.Errorf("unsupported: certificate reference currently not supported with --new-bundle-format")
+		}
 		cert, err := loadCertFromFileOrURL(c.CertRef)
 		if err != nil {
 			return fmt.Errorf("loading certificate from reference: %w", err)
@@ -214,6 +232,17 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 			}
 			co.SCT = sct
 		}
+	case c.TrustedRootPath != "":
+		if !c.NewBundleFormat {
+			return fmt.Errorf("unsupported: trusted root path currently only supported with --new-bundle-format")
+		}
+
+		// If a trusted root path is provided, we will use it to verify the bundle.
+		// Otherwise, the verifier will default to the public good instance.
+		co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
+		if err != nil {
+			return fmt.Errorf("creating trusted root from path: %w", err)
+		}
 	case c.CARoots != "":
 		// CA roots + possible intermediates are already loaded into co.RootCerts with the call to
 		// loadCertsKeylessVerification above.
@@ -318,3 +347,19 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 
 	return nil
 }
+
+func checkSigstoreBundleUnsupportedOptions(c *VerifyAttestationCommand) error {
+	if c.CertRef != "" {
+		return fmt.Errorf("unsupported: certificate may not be provided using --certificate when using --new-bundle-format (cert must be in bundle)")
+	}
+	if c.CertChain != "" {
+		return fmt.Errorf("unsupported: certificate chain may not be provided using --certificate-chain when using --new-bundle-format (cert must be in bundle)")
+	}
+	if c.CARoots != "" || c.CAIntermediates != "" {
+		return fmt.Errorf("unsupported: CA roots/intermediates must be provided using --trusted-root when using --new-bundle-format")
+	}
+	if c.TSACertChainPath != "" {
+		return fmt.Errorf("unsupported: TSA certificate chain path may only be provided using --trusted-root when using --new-bundle-format")
+	}
+	return nil
+}
diff --git a/doc/cosign_attest.md b/doc/cosign_attest.md
index 2a1171c3..1f647e83 100644
--- a/doc/cosign_attest.md
+++ b/doc/cosign_attest.md
@@ -57,6 +57,7 @@ cosign attest [flags]
       --insecure-skip-verify                                                                     skip verifying fulcio published to the SCT (this should only be used for testing).
       --k8s-keychain                                                                             whether to use the kubernetes keychain instead of the default keychain (supports workload identity).
       --key string                                                                               path to the private key file, KMS URI or Kubernetes Secret
+      --new-bundle-format                                                                        attach a Sigstore bundle using OCI referrers API
       --no-upload                                                                                do not upload the generated attestation
       --oidc-client-id string                                                                    OIDC client ID for application (default "sigstore")
       --oidc-client-secret-file string                                                           Path to file containing OIDC client secret for application
diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md
index d370f05c..b2990564 100644
--- a/doc/cosign_verify-attestation.md
+++ b/doc/cosign_verify-attestation.md
@@ -81,6 +81,7 @@ cosign verify-attestation [flags]
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
       --local-image                                                                              whether the specified image is a path to an image saved locally via 'cosign save'
       --max-workers int                                                                          the amount of maximum workers for parallel executions (default 10)
+      --new-bundle-format                                                                        expect the signature/attestation to be packaged in a Sigstore bundle
       --offline                                                                                  only allow offline verification
   -o, --output string                                                                            output format for the signing image information (json|text) (default "json")
       --policy strings                                                                           specify CUE or Rego files with policies to be used for validation
@@ -97,8 +98,9 @@ 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
+      --trusted-root string                                                                      Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set.
       --type string                                                                              specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|openvex|custom) or an URI (default "custom")
-      --use-signed-timestamps                                                                    use signed timestamps if available
+      --use-signed-timestamps                                                                    verify rfc3161 timestamps
 ```
 
 ### Options inherited from parent commands
diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md
index a7c29335..948eaf45 100644
--- a/doc/cosign_verify-blob-attestation.md
+++ b/doc/cosign_verify-blob-attestation.md
@@ -49,7 +49,7 @@ cosign verify-blob-attestation [flags]
       --insecure-ignore-tlog                            ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log
       --key string                                      path to the public key file, KMS URI or Kubernetes Secret
       --max-workers int                                 the amount of maximum workers for parallel executions (default 10)
-      --new-bundle-format                               output bundle in new format that contains all verification material
+      --new-bundle-format                               expect the signature/attestation to be packaged in a Sigstore bundle
       --offline                                         only allow offline verification
       --private-infrastructure                          skip transparency log verification when verifying artifacts in a privately deployed infrastructure
       --rekor-url string                                address of rekor STL server (default "https://rekor.sigstore.dev")
@@ -59,9 +59,9 @@ 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
-      --trusted-root string                             path to trusted root FILE
+      --trusted-root string                             Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set.
       --type string                                     specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|openvex|custom) or an URI (default "custom")
-      --use-signed-timestamps                           use signed timestamps if available
+      --use-signed-timestamps                           verify rfc3161 timestamps
 ```
 
 ### Options inherited from parent commands
diff --git a/pkg/cosign/testdata/oci-attestation.sigstore.json b/pkg/cosign/testdata/oci-attestation.sigstore.json
new file mode 100644
index 00000000..c3b65b2a
--- /dev/null
+++ b/pkg/cosign/testdata/oci-attestation.sigstore.json
@@ -0,0 +1,1 @@
+{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIICzzCCAlWgAwIBAgIUF2e+Ci0YsLnPaFUknbNDtUcqXl0wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwMjI4MTkxODExWhcNMjUwMjI4MTkyODExWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk/CiYRmA9zsa7ams/+fI+wGeXOFxng4J0wSj1jub7kxArHhGwvgcVE3ubrGXNQB9X3ksG0F+MiXHG8gRzxrr0KOCAXQwggFwMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUcPdotYluBHI4fkYy3yij5SY4728wHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHgYDVR0RAQH/BBQwEoEQY29keUBzb3lsYW5kLmNvbTAsBgorBgEEAYO/MAEBBB5odHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwLgYKKwYBBAGDvzABCAQgDB5odHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwgYoGCisGAQQB1nkCBAIEfAR6AHgAdgDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAZVN/q3JAAAEAwBHMEUCIQCdLrw1b9rE6rFd+NAJVPaw4DmH0YNOH2AYIOc4g26WmgIgG7Gub5hz7gzFpvhdb9HQsObWfTNcUiSOmrnPlmaQaKUwCgYIKoZIzj0EAwMDaAAwZQIxAMwB66cCKVhOYq2JMx20FPfJXD6K6oaUV8aIlCiW/YLLZdkWjTWkXt4qIbPgUeAl6wIwdCmnnSMw9fG7FuKz14RYFSbE1VLhNiWhGYpfCj00FtNRoXF+NFQ7DOKjjq7xPjza"},"tlogEntries":[{"logIndex":"175508996","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1740770291","inclusionPromise":{"signedEntryTimestamp":"MEUCIQDIgcWm5sXPh52rA/3BLyvfymD4RLEze5cx0WrDCF7tXAIgDtDq9wgYo8Z6RQ/LIaMRY/clocjCbX1qZTI9EVrGbzQ="},"inclusionProof":{"logIndex":"53604734","rootHash":"nd/V1IEweXlu0reIQGaGK1d51xW4Rj8fUyTrmcUkrMY=","treeSize":"53604735","hashes":["p7adBuLwiHyxFA7sjjx9Jt+YJ9NJ6pPRsqrlFv/Prl4=","V8kTVhZDHU0oMb8SoiG+IJzfi1+zs8k07t7msfdk3DU=","Y9K/VuEpG5hexjkO0eGf5aWgfD1no2I/HmjLL79pGOw=","49xFd2wqEUENt7vh3yabkMXtEu6nNJxA6xJDF35HkvQ=","fldHlNczrXBcOY4UnkC1im8c3AZGOojtqHIrnWG81FA=","9JKlhk57UmiGCygvf555Z0loJkdvt8BLIwGeMAuJM1o=","JuKjSUhL4pgP2+mTbT/X01oQa16n9Nh2ruwSgN9Ft3o=","6g/GERajYnzZjZ0EXQIVHpGIKry1LMG0+RJhvtcbSwI=","62MSvX4iwxK6VPg9s7NR+Y1jiYkd/SsZpVoaUYjD8wg=","K7s/kEEUl/59VblorlWa2aJO8TuH5uy8kn/7qSY+T94=","GlND1TZKB9H1XkH8gmyEmNId6gWGYnXjA0ykMIJaI44=","315PrxqKtUW8bpqBdn6SMyj+smOwK7jCa7Px7i4AIOQ=","lL6jxdDTg23iUvuwRop1833jkmWSvr7sLBM3hXZ8tTU=","eExzddanJoxYTKoErFFSTDFUR3UvwaaXxcddWwQJd7I=","ebCKJ53lKWPqIx8mXXgznF9DGoQv70J7JTlFAav6s5E=","vemyaMj0Na1LMjbB/9Dmkq8T+jAb3o+yCESgAayUABU="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n53604735\nnd/V1IEweXlu0reIQGaGK1d51xW4Rj8fUyTrmcUkrMY=\n\n— rekor.sigstore.dev wNI9ajBFAiEAuh6liXP/rBKWjuO/W8jooM66eVHSXo3Cf8kEAIryZLACIAfCuhLOHpSnU3WA68gZci57o2JuwX7S5H6ANbGePH58\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiYzZhZGFhZjE0OTA4NGYxOTc3YzE0YjViMTQxMmY4ODI1OGU1ODMwZDg3YTVjNjQ0MDNjNDAzYjliNzNmZjEzNiJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjE1NjkyNjE2ZTljNDM1OTA3YzQxNDExNDY2ZTQ5M2YxOTVhMzljYTY4Yzk3M2E0ZjUzOTBkZjNkMzAxMzk4YmQifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVRQ0lEb21pdVlWZ05LeWZZei9sZXF2dEVZMnh1aWZ0ZWlnanErTGZTaHU5MkJCQWlCeGl4c0daTllCVlJoR2pYNk9aaVBRQklvVWYraThWZ1BoLy9BVC96NFZnZz09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VONmVrTkRRV3hYWjBGM1NVSkJaMGxWUmpKbEswTnBNRmx6VEc1UVlVWlZhMjVpVGtSMFZXTnhXR3d3ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmQwMXFTVFJOVkd0NFQwUkZlRmRvWTA1TmFsVjNUV3BKTkUxVWEzbFBSRVY0VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVnJMME5wV1ZKdFFUbDZjMkUzWVcxekx5dG1TU3QzUjJWWVQwWjRibWMwU2pCM1Uyb0tNV3AxWWpkcmVFRnlTR2hIZDNablkxWkZNM1ZpY2tkWVRsRkNPVmd6YTNOSE1FWXJUV2xZU0VjNFoxSjZlSEp5TUV0UFEwRllVWGRuWjBaM1RVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVmpVR1J2Q25SWmJIVkNTRWswWm10WmVUTjVhV28xVTFrME56STRkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMGhuV1VSV1VqQlNRVkZJTDBKQ1VYZEZiMFZSV1RJNWEyVlZRbnBpTTJ4eldWYzFhMHh0VG5aaVZFRnpRbWR2Y2tKblJVVkJXVTh2VFVGRlFncENRalZ2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2M1Ym1GWE5IWmlNa1l4WkVkbmQweG5XVXRMZDFsQ1FrRkhSSFo2UVVKRFFWRm5Da1JDTlc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFppUnpsdVlWYzBkbUl5UmpGa1IyZDNaMWx2UjBOcGMwZEJVVkZDTVc1clEwSkJTVVVLWmtGU05rRklaMEZrWjBSa1VGUkNjWGh6WTFKTmJVMWFTR2g1V2xwNlkwTnZhM0JsZFU0ME9ISm1LMGhwYmt0QlRIbHVkV3BuUVVGQldsWk9MM0V6U2dwQlFVRkZRWGRDU0UxRlZVTkpVVU5rVEhKM01XSTVja1UyY2taa0swNUJTbFpRWVhjMFJHMUlNRmxPVDBneVFWbEpUMk0wWnpJMlYyMW5TV2RITjBkMUNtSTFhSG8zWjNwR2NIWm9aR0k1U0ZGelQySlhabFJPWTFWcFUwOXRjbTVRYkcxaFVXRkxWWGREWjFsSlMyOWFTWHBxTUVWQmQwMUVZVUZCZDFwUlNYZ0tRVTEzUWpZMlkwTkxWbWhQV1hFeVNrMTRNakJHVUdaS1dFUTJTelp2WVZWV09HRkpiRU5wVnk5WlRFeGFaR3RYYWxSWGExaDBOSEZKWWxCblZXVkJiQW8yZDBsM1pFTnRibTVUVFhjNVprYzNSblZMZWpFMFVsbEdVMkpGTVZaTWFFNXBWMmhIV1hCbVEyb3dNRVowVGxKdldFWXJUa1pSTjBSUFMycHFjVGQ0Q2xCcWVtRUtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifV19fQ=="}]},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2Nvc2lnbi5zaWdzdG9yZS5kZXYvYXR0ZXN0YXRpb24vdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiMTI3LjAuMC4xOjYwOTQzL3JlcG8iLCJkaWdlc3QiOnsic2hhMjU2IjoiNzMyMTEyMjcwZDdlNTk0MThhOGMwODBiMTM0YjI0Y2FiZDY3ZDI1MGQwZDAxNDdhOTdlZDk1YmE1YzI4MGFhNCJ9fV0sInByZWRpY2F0ZSI6eyJEYXRhIjoie1wiZm9vXCI6IFwiYmFyXCJ9XG4iLCJUaW1lc3RhbXAiOiIyMDI1LTAyLTI4VDE5OjE4OjExWiJ9fQ==","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEQCIDomiuYVgNKyfYz/leqvtEY2xuifteigjq+LfShu92BBAiBxixsGZNYBVRhGjX6OZiPQBIoUf+i8VgPh//AT/z4Vgg=="}]}}
diff --git a/pkg/cosign/testdata/trusted_root_pgi.json b/pkg/cosign/testdata/trusted_root_pgi.json
new file mode 100644
index 00000000..b8706cb3
--- /dev/null
+++ b/pkg/cosign/testdata/trusted_root_pgi.json
@@ -0,0 +1,90 @@
+{
+  "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
+  "tlogs": [
+    {
+      "baseUrl": "https://rekor.sigstore.dev",
+      "hashAlgorithm": "SHA2_256",
+      "publicKey": {
+        "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==",
+        "keyDetails": "PKIX_ECDSA_P256_SHA_256",
+        "validFor": {
+          "start": "2021-01-12T11:53:27.000Z"
+        }
+      },
+      "logId": {
+        "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
+      }
+    }
+  ],
+  "certificateAuthorities": [
+    {
+      "subject": {
+        "organization": "sigstore.dev",
+        "commonName": "sigstore"
+      },
+      "uri": "https://fulcio.sigstore.dev",
+      "certChain": {
+        "certificates": [
+          {
+            "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="
+          }
+        ]
+      },
+      "validFor": {
+        "start": "2021-03-07T03:20:29.000Z",
+        "end": "2022-12-31T23:59:59.999Z"
+      }
+    },
+    {
+      "subject": {
+        "organization": "sigstore.dev",
+        "commonName": "sigstore"
+      },
+      "uri": "https://fulcio.sigstore.dev",
+      "certChain": {
+        "certificates": [
+          {
+            "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="
+          },
+          {
+            "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
+          }
+        ]
+      },
+      "validFor": {
+        "start": "2022-04-13T20:06:15.000Z"
+      }
+    }
+  ],
+  "ctlogs": [
+    {
+      "baseUrl": "https://ctfe.sigstore.dev/test",
+      "hashAlgorithm": "SHA2_256",
+      "publicKey": {
+        "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==",
+        "keyDetails": "PKIX_ECDSA_P256_SHA_256",
+        "validFor": {
+          "start": "2021-03-14T00:00:00.000Z",
+          "end": "2022-10-31T23:59:59.999Z"
+        }
+      },
+      "logId": {
+        "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="
+      }
+    },
+    {
+      "baseUrl": "https://ctfe.sigstore.dev/2022",
+      "hashAlgorithm": "SHA2_256",
+      "publicKey": {
+        "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==",
+        "keyDetails": "PKIX_ECDSA_P256_SHA_256",
+        "validFor": {
+          "start": "2022-10-20T00:00:00.000Z"
+        }
+      },
+      "logId": {
+        "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="
+      }
+    }
+  ]
+}
diff --git a/pkg/cosign/verify_oci_test.go b/pkg/cosign/verify_oci_test.go
new file mode 100644
index 00000000..aa80eabf
--- /dev/null
+++ b/pkg/cosign/verify_oci_test.go
@@ -0,0 +1,238 @@
+// Copyright 2025 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 cosign
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+
+	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/registry"
+	"github.com/google/go-containerregistry/pkg/v1/empty"
+	"github.com/google/go-containerregistry/pkg/v1/random"
+	"github.com/google/go-containerregistry/pkg/v1/remote"
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/protobuf/proto"
+
+	ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
+	sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
+	"github.com/sigstore/sigstore-go/pkg/root"
+)
+
+//go:embed testdata/oci-attestation.sigstore.json
+var testAttestation []byte
+
+//go:embed testdata/trusted_root_pgi.json
+var testTrustedRootPGI []byte
+
+func TestGetBundles_Empty(t *testing.T) {
+	r := registry.New(registry.WithReferrersSupport(true))
+	s := httptest.NewServer(r)
+	defer s.Close()
+
+	u, err := url.Parse(s.URL)
+	assert.NoError(t, err)
+
+	ref, err := name.ParseReference(fmt.Sprintf("%s/repo:tag", u.Host))
+	assert.NoError(t, err)
+
+	// If tag doesn't exist, should return ErrImageTagNotFound
+	bundles, hash, err := getBundles(context.Background(), ref, &CheckOpts{})
+	imgTagNotFound := &ErrImageTagNotFound{}
+	assert.ErrorAs(t, err, &imgTagNotFound)
+	assert.Len(t, bundles, 0)
+	assert.Nil(t, hash)
+
+	// Write an image
+	img, err := random.Image(10, 10)
+	assert.NoError(t, err)
+	assert.NoError(t, remote.Write(ref, img))
+
+	// Check that no matching attestation error is returned
+	bundles, hash, err = getBundles(context.Background(), ref, &CheckOpts{})
+	var noMatchErr *ErrNoMatchingAttestations
+	assert.ErrorAs(t, err, &noMatchErr)
+	assert.Len(t, bundles, 0)
+	assert.Nil(t, hash)
+
+	// Get digest from tag
+	desc, err := remote.Head(ref)
+	assert.NoError(t, err)
+	digestRef := ref.Context().Digest(desc.Digest.String())
+
+	// Write invalid attestation
+	err = ociremote.WriteAttestationNewBundleFormat(digestRef, []byte("invalid"), "foo/bar")
+	assert.NoError(t, err)
+
+	// Should still return no matching attestation error, as it failed to parse the bundle
+	bundles, hash, err = getBundles(context.Background(), ref, &CheckOpts{})
+	assert.ErrorAs(t, err, &noMatchErr)
+	assert.Len(t, bundles, 0)
+	assert.Nil(t, hash)
+}
+
+func TestGetBundles_Valid(t *testing.T) {
+	r := registry.New(registry.WithReferrersSupport(true))
+	s := httptest.NewServer(r)
+	defer s.Close()
+
+	u, err := url.Parse(s.URL)
+	assert.NoError(t, err)
+
+	ref, err := name.ParseReference(fmt.Sprintf("%s/repo:tag", u.Host))
+	assert.NoError(t, err)
+
+	// Test data uses empty image
+	assert.NoError(t, remote.Write(ref, empty.Image))
+
+	// Get digest from tag
+	desc, err := remote.Head(ref)
+	assert.NoError(t, err)
+	digestRef := ref.Context().Digest(desc.Digest.String())
+
+	// Write valid test attestation
+	err = ociremote.WriteAttestationNewBundleFormat(digestRef, testAttestation, "https://cosign.sigstore.dev/attestation/v1")
+	assert.NoError(t, err)
+
+	// Retrieve the attestation
+	bundles, hash, err := getBundles(context.Background(), ref, &CheckOpts{})
+	assert.NoError(t, err)
+	assert.Len(t, bundles, 1)
+	assert.NotNil(t, hash)
+
+	// Compare the output to the test data
+	expected := sgbundle.Bundle{}
+	err = expected.UnmarshalJSON(testAttestation)
+	assert.NoError(t, err)
+	if !proto.Equal(bundles[0].Bundle, &expected) {
+		t.Errorf("got %v, want %v", bundles[0].Bundle, &expected)
+	}
+}
+
+// TODO: This test is getting long and maybe should be refactored into a
+// table-based test to exercise more permutations.
+func TestVerifyImageAttestationsSigstoreBundle(t *testing.T) {
+	r := registry.New(registry.WithReferrersSupport(true))
+	s := httptest.NewServer(r)
+	defer s.Close()
+
+	u, err := url.Parse(s.URL)
+	assert.NoError(t, err)
+
+	ref, err := name.ParseReference(fmt.Sprintf("%s/repo:tag", u.Host))
+	assert.NoError(t, err)
+
+	ref2, err := name.ParseReference(fmt.Sprintf("%s/repo:tag2", u.Host))
+	assert.NoError(t, err)
+
+	// Parse test root
+	trustedRoot, err := root.NewTrustedRootFromJSON(testTrustedRootPGI)
+	assert.NoError(t, err)
+
+	// Test data uses empty image
+	assert.NoError(t, remote.Write(ref, empty.Image))
+
+	// Also write a second image to test that we only verify the correct image
+	randomImage, err := random.Image(10, 10)
+	assert.NoError(t, err)
+	assert.NoError(t, remote.Write(ref2, randomImage))
+
+	// Attempt to verify non-existent attestation
+	atts, bundleVerified, err := VerifyImageAttestations(context.Background(), ref, &CheckOpts{
+		TrustedMaterial: trustedRoot,
+		NewBundleFormat: true,
+		Identities: []Identity{
+			{
+				IssuerRegExp:  ".*",
+				SubjectRegExp: ".*",
+			},
+		},
+	})
+	var errNoMatchingAttestations *ErrNoMatchingAttestations
+	assert.ErrorAs(t, err, &errNoMatchingAttestations)
+	assert.False(t, bundleVerified)
+	assert.Len(t, atts, 0)
+
+	// Get digest from tag
+	desc, err := remote.Head(ref)
+	assert.NoError(t, err)
+	digestRef := ref.Context().Digest(desc.Digest.String())
+
+	// Write valid test attestation
+	err = ociremote.WriteAttestationNewBundleFormat(digestRef, testAttestation, "https://cosign.sigstore.dev/attestation/v1")
+	assert.NoError(t, err)
+
+	// Verify the attestation
+	atts, bundleVerified, err = VerifyImageAttestations(context.Background(), ref, &CheckOpts{
+		TrustedMaterial: trustedRoot,
+		NewBundleFormat: true,
+		Identities: []Identity{
+			{
+				IssuerRegExp:  ".*",
+				SubjectRegExp: ".*",
+			},
+		},
+	})
+	assert.NoError(t, err)
+	assert.True(t, bundleVerified)
+	assert.Len(t, atts, 1)
+
+	// Wrong identity should not verify
+	atts, bundleVerified, err = VerifyImageAttestations(context.Background(), ref, &CheckOpts{
+		TrustedMaterial: trustedRoot,
+		NewBundleFormat: true,
+		Identities: []Identity{
+			{
+				IssuerRegExp:  ".*",
+				SubjectRegExp: "wrong",
+			},
+		},
+	})
+	assert.ErrorAs(t, err, &errNoMatchingAttestations)
+	assert.False(t, bundleVerified)
+	assert.Len(t, atts, 0)
+
+	// Add same attestation to different image with different digest to test that we only verify the correct image
+	// Get digest from tag
+	desc, err = remote.Head(ref2)
+	assert.NoError(t, err)
+	digestRef = ref2.Context().Digest(desc.Digest.String())
+
+	// Write valid test attestation
+	err = ociremote.WriteAttestationNewBundleFormat(digestRef, testAttestation, "https://cosign.sigstore.dev/attestation/v1")
+	assert.NoError(t, err)
+
+	// Verify the attestation
+	atts, bundleVerified, err = VerifyImageAttestations(context.Background(), ref2, &CheckOpts{
+		TrustedMaterial: trustedRoot,
+		NewBundleFormat: true,
+		Identities: []Identity{
+			{
+				IssuerRegExp:  ".*",
+				SubjectRegExp: ".*",
+			},
+		},
+	})
+	assert.Error(t, err)
+	assert.ErrorAs(t, err, &errNoMatchingAttestations)
+	// TODO: This error message comes from sigstore-go. We may want to update the upstream with a sentinal error type.
+	assert.ErrorContains(t, err, "provided artifact digest does not match any digest in statement")
+	assert.False(t, bundleVerified)
+	assert.Len(t, atts, 0)
+}
diff --git a/test/e2e_test.go b/test/e2e_test.go
index 48a6d80a..9a9097f7 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -750,12 +750,15 @@ func TestSignVerifyWithTUFMirror(t *testing.T) {
 }
 
 func TestAttestVerify(t *testing.T) {
-	attestVerify(t,
-		"slsaprovenance",
-		`{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`,
-		`predicate: builder: id: "2"`,
-		`predicate: builder: id: "1"`,
-	)
+	for _, newBundleFormat := range []bool{false, true} {
+		attestVerify(t,
+			newBundleFormat,
+			"slsaprovenance",
+			`{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`,
+			`predicate: builder: id: "2"`,
+			`predicate: builder: id: "1"`,
+		)
+	}
 }
 
 func TestAttestVerifySPDXJSON(t *testing.T) {
@@ -763,12 +766,15 @@ func TestAttestVerifySPDXJSON(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	attestVerify(t,
-		"spdxjson",
-		string(attestationBytes),
-		`predicate: spdxVersion: "SPDX-2.2"`,
-		`predicate: spdxVersion: "SPDX-9.9"`,
-	)
+	for _, newBundleFormat := range []bool{false, true} {
+		attestVerify(t,
+			newBundleFormat,
+			"spdxjson",
+			string(attestationBytes),
+			`predicate: spdxVersion: "SPDX-2.2"`,
+			`predicate: spdxVersion: "SPDX-9.9"`,
+		)
+	}
 }
 
 func TestAttestVerifyCycloneDXJSON(t *testing.T) {
@@ -776,12 +782,15 @@ func TestAttestVerifyCycloneDXJSON(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	attestVerify(t,
-		"cyclonedx",
-		string(attestationBytes),
-		`predicate: specVersion: "1.4"`,
-		`predicate: specVersion: "7.7"`,
-	)
+	for _, newBundleFormat := range []bool{false, true} {
+		attestVerify(t,
+			newBundleFormat,
+			"cyclonedx",
+			string(attestationBytes),
+			`predicate: specVersion: "1.4"`,
+			`predicate: specVersion: "7.7"`,
+		)
+	}
 }
 
 func TestAttestVerifyURI(t *testing.T) {
@@ -789,15 +798,18 @@ func TestAttestVerifyURI(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	attestVerify(t,
-		"https://example.com/TestResult/v1",
-		string(attestationBytes),
-		`predicate: passed: true`,
-		`predicate: passed: false"`,
-	)
+	for _, newBundleFormat := range []bool{false, true} {
+		attestVerify(t,
+			newBundleFormat,
+			"https://example.com/TestResult/v1",
+			string(attestationBytes),
+			`predicate: passed: true`,
+			`predicate: passed: false"`,
+		)
+	}
 }
 
-func attestVerify(t *testing.T, predicateType, attestation, goodCue, badCue string) {
+func attestVerify(t *testing.T, newBundleFormat bool, predicateType, attestation, goodCue, badCue string) {
 	repo, stop := reg(t)
 	defer stop()
 	td := t.TempDir()
@@ -826,6 +838,10 @@ func attestVerify(t *testing.T, predicateType, attestation, goodCue, badCue stri
 		MaxWorkers: 10,
 	}
 
+	if newBundleFormat {
+		verifyAttestation.NewBundleFormat = true
+	}
+
 	// Fail case when using without type and policy flag
 	mustErr(verifyAttestation.Exec(ctx, []string{imgName}), t)
 
@@ -834,7 +850,7 @@ func attestVerify(t *testing.T, predicateType, attestation, goodCue, badCue stri
 	}
 
 	// Now attest the image
-	ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc}
+	ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, NewBundleFormat: newBundleFormat}
 	attestCmd := attest.AttestCommand{
 		KeyOpts:        ko,
 		PredicatePath:  attestationPath,