CVE-2022-36056 (published 2022-09-14T20:15Z)

Cosign is a project under the sigstore organization which aims to make signatures invisible infrastructure. In versions prior to 1.12.0 a number of vulnerabilities have been found in cosign verify-blob, where Cosign would successfully verify an artifact when verification should have failed. First a cosign bundle can be crafted to successfully verify a blob even if the embedded rekorBundle does not reference the given signature. Second, when providing identity flags, the email and issuer of a certificate is not checked when verifying a Rekor bundle, and the GitHub Actions identity is never checked. Third, providing an invalid Rekor bundle without the experimental flag results in a successful verification. And fourth an invalid transparency log entry will result in immediate success for verification. Details and examples of these issues can be seen in the GHSA-8gw7-4j42-w388 advisory linked. Users are advised to upgrade to 1.12.0. There are no known workarounds for these issues.

CVSS v3 base score: 5.5, MEDIUM (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-347
cwe_name: Improper Verification of Cryptographic Signature
description: The product does not verify, or incorrectly verifies, the cryptographic signature for data.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/sigstore/cosign
commit_details: repo_url: https://github.com/sigstore/cosign
author: asraa
author_date: 2022-09-14 03:37:41-05:00
msg: Merge pull request from GHSA-8gw7-4j42-w388

* wip

Signed-off-by: Asra Ali <asraa@google.com>

more tests

Signed-off-by: Asra Ali <asraa@google.com>

finish tests

Signed-off-by: Asra Ali <asraa@google.com>

Add explicit testcase for failure in verifytlogentry

Signed-off-by: Asra Ali <asraa@google.com>

add testing for invalid provided bundle fails

Signed-off-by: Asra Ali <asraa@google.com>

update

Signed-off-by: Asra Ali <asraa@google.com>

address hayden comments

Signed-off-by: Asra Ali <asraa@google.com>

update

Signed-off-by: Asra Ali <asraa@google.com>

* fix: verify RekorBundle payload references blob

Co-authored-by: Cody Soyland <codysoyland@github.com>
Co-authored-by: Asra Ali <asraa@google.com>

* Add test for invalid blob signature causing error

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Add tests for checking identity flags

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* address bob's comment

Signed-off-by: Asra Ali <asraa@google.com>

* add comment on intoto multisig

Signed-off-by: Asra Ali <asraa@google.com>

Signed-off-by: Asra Ali <asraa@google.com>
Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
Co-authored-by: Cody Soyland <codysoyland@github.com>
Co-authored-by: Hayden Blauzvern <hblauzvern@google.com>
repository: repo_url: https://github.com/sigstore/cosign
repo_name: sigstore/cosign
description: Code signing and transparency for containers and binaries
repo_language: Go
owner: sigstore
file_changes: filename: verify_blob.go
change_type: ModificationType.MODIFY
diff: @@ -19,7 +19,7 @@ import (
 	"bytes"
 	"context"
 	"crypto"
-	_ "crypto/sha256" // for `crypto.SHA256`
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/hex"
@@ -28,6 +28,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/go-openapi/runtime"
@@ -37,6 +38,7 @@ import (
 	"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
 	"github.com/sigstore/cosign/pkg/blob"
 	"github.com/sigstore/cosign/pkg/cosign"
+	"github.com/sigstore/cosign/pkg/cosign/bundle"
 	"github.com/sigstore/cosign/pkg/cosign/pivkey"
 	"github.com/sigstore/cosign/pkg/cosign/pkcs11key"
 	sigs "github.com/sigstore/cosign/pkg/signature"
@@ -45,11 +47,15 @@ import (
 	ctypes "github.com/sigstore/cosign/pkg/types"
 	"github.com/sigstore/rekor/pkg/generated/client"
 	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/pki"
 	"github.com/sigstore/rekor/pkg/types"
-	hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
-	rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
+	"github.com/sigstore/rekor/pkg/types/hashedrekord"
+	hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
+	"github.com/sigstore/rekor/pkg/types/intoto"
+	intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
+	"github.com/sigstore/rekor/pkg/types/rekord"
+	rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
-	"github.com/sigstore/sigstore/pkg/signature"
 	"github.com/sigstore/sigstore/pkg/signature/dsse"
 	signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
 )
@@ -65,14 +71,13 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 	certGithubWorkflowName,
 	certGithubWorkflowRepository,
 	certGithubWorkflowRef string, enforceSCT bool) error {
-	var verifier signature.Verifier
 	var cert *x509.Certificate
 
 	if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() && ko.BundlePath == "" {
 		return &options.PubKeyParseError{}
 	}
 
-	sig, b64sig, err := signatures(sigRef, ko.BundlePath)
+	sig, err := signatures(sigRef, ko.BundlePath)
 	if err != nil {
 		return err
 	}
@@ -82,14 +87,44 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 		return err
 	}
 
+	co := &cosign.CheckOpts{
+		CertEmail:                    certEmail,
+		CertOidcIssuer:               certOidcIssuer,
+		CertGithubWorkflowTrigger:    certGithubWorkflowTrigger,
+		CertGithubWorkflowSha:        certGithubWorkflowSha,
+		CertGithubWorkflowName:       certGithubWorkflowName,
+		CertGithubWorkflowRepository: certGithubWorkflowRepository,
+		CertGithubWorkflowRef:        certGithubWorkflowRef,
+		EnforceSCT:                   enforceSCT,
+	}
+	if options.EnableExperimental() {
+		if ko.RekorURL != "" {
+			rekorClient, err := rekor.NewClient(ko.RekorURL)
+			if err != nil {
+				return fmt.Errorf("creating Rekor client: %w", err)
+			}
+			co.RekorClient = rekorClient
+		}
+	}
+	if certRef == "" || options.EnableExperimental() {
+		co.RootCerts, err = fulcio.GetRoots()
+		if err != nil {
+			return fmt.Errorf("getting Fulcio roots: %w", err)
+		}
+		co.IntermediateCerts, err = fulcio.GetIntermediates()
+		if err != nil {
+			return fmt.Errorf("getting Fulcio intermediates: %w", err)
+		}
+	}
+
 	// Keys are optional!
 	switch {
 	case ko.KeyRef != "":
-		verifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef)
+		co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef)
 		if err != nil {
 			return fmt.Errorf("loading public key: %w", err)
 		}
-		pkcs11Key, ok := verifier.(*pkcs11key.Key)
+		pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key)
 		if ok {
 			defer pkcs11Key.Close()
 		}
@@ -99,7 +134,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 			return fmt.Errorf("opening piv token: %w", err)
 		}
 		defer sk.Close()
-		verifier, err = sk.Verifier()
+		co.SigVerifier, err = sk.Verifier()
 		if err != nil {
 			return fmt.Errorf("loading public key from token: %w", err)
 		}
@@ -108,27 +143,8 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 		if err != nil {
 			return err
 		}
-		co := &cosign.CheckOpts{
-			CertEmail:                    certEmail,
-			CertOidcIssuer:               certOidcIssuer,
-			CertGithubWorkflowTrigger:    certGithubWorkflowTrigger,
-			CertGithubWorkflowSha:        certGithubWorkflowSha,
-			CertGithubWorkflowName:       certGithubWorkflowName,
-			CertGithubWorkflowRepository: certGithubWorkflowRepository,
-			CertGithubWorkflowRef:        certGithubWorkflowRef,
-			EnforceSCT:                   enforceSCT,
-		}
 		if certChain == "" {
-			// If no certChain is passed, the Fulcio root certificate will be used
-			co.RootCerts, err = fulcio.GetRoots()
-			if err != nil {
-				return fmt.Errorf("getting Fulcio roots: %w", err)
-			}
-			co.IntermediateCerts, err = fulcio.GetIntermediates()
-			if err != nil {
-				return fmt.Errorf("getting Fulcio intermediates: %w", err)
-			}
-			verifier, err = cosign.ValidateAndUnpackCert(cert, co)
+			co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co)
 			if err != nil {
 				return err
 			}
@@ -138,7 +154,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 			if err != nil {
 				return err
 			}
-			verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
+			co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
 			if err != nil {
 				return err
 			}
@@ -151,7 +167,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 		if b.Cert == "" {
 			return fmt.Errorf("bundle does not contain cert for verification, please provide public key")
 		}
-		// cert can either be a cert or public key
+		// b.Cert can either be a certificate or public key
 		certBytes := []byte(b.Cert)
 		if isb64(certBytes) {
 			certBytes, _ = base64.StdEncoding.DecodeString(b.Cert)
@@ -159,20 +175,19 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 		cert, err = loadCertFromPEM(certBytes)
 		if err != nil {
 			// check if cert is actually a public key
-			verifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256)
+			co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256)
 		} else {
-			verifier, err = signature.LoadVerifier(cert.PublicKey, crypto.SHA256)
+			co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co)
+			if err != nil {
+				return err
+			}
 		}
 		if err != nil {
 			return err
 		}
+	// No certificate is provided: search by artifact sha in the TLOG.
 	case options.EnableExperimental():
-		rClient, err := rekor.NewClient(ko.RekorURL)
-		if err != nil {
-			return err
-		}
-
-		uuids, err := cosign.FindTLogEntriesByPayload(ctx, rClient, blobBytes)
+		uuids, err := cosign.FindTLogEntriesByPayload(ctx, co.RekorClient, blobBytes)
 		if err != nil {
 			return err
 		}
@@ -180,21 +195,49 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 		if len(uuids) == 0 {
 			return errors.New("could not find a tlog entry for provided blob")
 		}
-		return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes, enforceSCT)
-	}
 
-	// Use the DSSE verifier if the payload is a DSSE with the In-Toto format.
-	if isIntotoDSSE(blobBytes) {
-		verifier = dsse.WrapVerifier(verifier)
-	}
+		// Iterate through and try to find a matching Rekor entry.
+		// This does not support intoto properly! c/f extractCerts and
+		// the verifier.
+		for _, u := range uuids {
+			tlogEntry, err := cosign.GetTlogEntry(ctx, co.RekorClient, u)
+			if err != nil {
+				continue
+			}
+
+			// Note that this will error out if the TLOG entry was signed with a
+			// raw public key. Again, using search on artifact sha is unreliable.
+			certs, err := extractCerts(tlogEntry)
+			if err != nil {
+				continue
+			}
+
+			cert := certs[0]
+			co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co)
+			if err != nil {
+				continue
+			}
+
+			if err := verifyBlob(ctx, co, blobBytes, sig, cert,
+				ko.BundlePath, tlogEntry); err == nil {
+				// We found a succesful Rekor entry!
+				fmt.Fprintln(os.Stderr, "Verified OK")
+				return nil
+			}
+		}
+
+		// No successful Rekor entry found.
+		fmt.Fprintln(os.Stderr, `WARNING: No valid entries were found in rekor to verify this blob.
+
+Transparency log support for blobs is experimental, and occasionally an entry isn't found even if one exists.
+
+We recommend requesting the certificate/signature from the original signer of this blob and manually verifying with cosign verify-blob --cert [cert] --signature [signature].`)
+		return fmt.Errorf("could not find a valid tlog entry for provided blob, found %d invalid entries", len(uuids))
 
-	// verify the signature
-	if err := verifier.VerifySignature(bytes.NewReader([]byte(sig)), bytes.NewReader(blobBytes)); err != nil {
-		return err
 	}
 
-	// verify the rekor entry
-	if err := verifyRekorEntry(ctx, ko, nil, verifier, cert, b64sig, blobBytes); err != nil {
+	// Performs all blob verification.
+	if err := verifyBlob(ctx, co, blobBytes, sig, cert, ko.BundlePath, nil); err != nil {
 		return err
 	}
 
@@ -202,69 +245,145 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 	return nil
 }
 
-func verifySigByUUID(ctx context.Context, ko options.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string,
-	uuids []string, blobBytes []byte, enforceSCT bool) error {
-	var validSigExists bool
-	for _, u := range uuids {
-		tlogEntry, err := cosign.GetTlogEntry(ctx, rClient, u)
+/* Verify Blob main entry point. This will perform the following:
+   1. Verifies the signature on the blob using the provided verifier.
+   2. Checks for transparency log entry presence:
+        a. Verifies the Rekor entry in the bundle, if provided. OR
+        b. If we don't have a Rekor entry retrieved via cert, do an online lookup (assuming
+           we are in experimental mode).
+        c. Uses the provided Rekor entry (may have been retrieved through Rekor SearchIndex) OR
+   3. If a certificate is provided, check it's expiration.
+*/
+// TODO: Make a version of this public. This could be VerifyBlobCmd, but we need to
+// clean up the args into CheckOpts or use KeyOpts here to resolve different KeyOpts.
+func verifyBlob(ctx context.Context, co *cosign.CheckOpts,
+	blobBytes []byte, sig string, cert *x509.Certificate,
+	bundlePath string, e *models.LogEntryAnon) error {
+	if cert != nil {
+		// This would have already be done in the main entrypoint, but do this for robustness.
+		var err error
+		co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co)
 		if err != nil {
-			continue
+			return fmt.Errorf("validating cert: %w", err)
 		}
+	}
 
-		certs, err := extractCerts(tlogEntry)
-		if err != nil {
-			continue
-		}
+	// Use the DSSE verifier if the payload is a DSSE with the In-Toto format.
+	// TODO: This verifier only supports verification of a single signer/signature on
+	// the envelope. Either have the verifier validate that only one signature exists,
+	// or use a multi-signature verifier.
+	if isIntotoDSSE(blobBytes) {
+		co.SigVerifier = dsse.WrapVerifier(co.SigVerifier)
+	}
 
-		co := &cosign.CheckOpts{
-			CertEmail:      certEmail,
-			CertOidcIssuer: certOidcIssuer,
-			EnforceSCT:     enforceSCT,
-		}
+	// 1. Verify the signature.
+	if err := co.SigVerifier.VerifySignature(strings.NewReader(sig), bytes.NewReader(blobBytes)); err != nil {
+		return err
+	}
 
-		co.RootCerts, err = fulcio.GetRoots()
-		if err != nil {
-			return fmt.Errorf("getting Fulcio roots: %w", err)
+	// This is the signature creation time. Without a transparency log entry timestamp,
+	// we can only use the current time as a bound.
+	var validityTime time.Time
+	// 2. Checks for transparency log entry presence:
+	switch {
+	// a. We have a local bundle.
+	case bundlePath != "":
+		var svBytes []byte
+		var err error
+		if cert != nil {
+			svBytes, err = cryptoutils.MarshalCertificateToPEM(cert)
+			if err != nil {
+				return fmt.Errorf("marshalling cert: %w", err)
+			}
+		} else {
+			svBytes, err = sigs.PublicKeyPem(co.SigVerifier, signatureoptions.WithContext(ctx))
+			if err != nil {
+				return fmt.Errorf("marshalling pubkey: %w", err)
+			}
 		}
-		co.IntermediateCerts, err = fulcio.GetIntermediates()
+		bundle, err := verifyRekorBundle(ctx, bundlePath, co.RekorClient, blobBytes, sig, svBytes)
 		if err != nil {
-			return fmt.Errorf("getting Fulcio intermediates: %w", err)
+			// Return when the provided bundle fails verification. (Do not fallback).
+			return err
+		}
+		validityTime = time.Unix(bundle.IntegratedTime, 0)
+		fmt.Fprintf(os.Stderr, "tlog entry verified offline\n")
+	// b. We can make an online lookup to the transparency log since we don't have an entry.
+	case co.RekorClient != nil && e == nil:
+		var tlogFindErr error
+		if cert == nil {
+			pub, err := co.SigVerifier.PublicKey(co.PKOpts...)
+			if err != nil {
+				return err
+			}
+			e, tlogFindErr = tlogFindPublicKey(ctx, co.RekorClient, blobBytes, sig, pub)
+		} else {
+			e, tlogFindErr = tlogFindCertificate(ctx, co.RekorClient, blobBytes, sig, cert)
+		}
+		if tlogFindErr != nil {
+			// TODO: Think about whether we should break here.
+			// This is COSIGN_EXPERIMENTAL mode, but in the case where someone
+			// provided a public key or still-valid cert,
+			/// they don't need TLOG lookup for the timestamp.
+			fmt.Fprintf(os.Stderr, "could not find entry in tlog: %s", tlogFindErr)
+			return tlogFindErr
+		}
+		// Fallthrough here to verify the TLOG entry and compute the integrated time.
+		fallthrough
+	// We are provided a log entry, possibly from above, or search.
+	case e != nil:
+		if err := cosign.VerifyTLogEntry(ctx, co.RekorClient, e); err != nil {
+			return err
 		}
 
-		cert := certs[0]
-		verifier, err := cosign.ValidateAndUnpackCert(cert, co)
+		uuid, err := cosign.ComputeLeafHash(e)
 		if err != nil {
-			continue
-		}
-		// Use the DSSE verifier if the payload is a DSSE with the In-Toto format.
-		if isIntotoDSSE(blobBytes) {
-			verifier = dsse.WrapVerifier(verifier)
-		}
-		// verify the signature
-		if err := verifier.VerifySignature(bytes.NewReader([]byte(sig)), bytes.NewReader(blobBytes)); err != nil {
-			continue
+			return err
 		}
 
-		// verify the rekor entry
-		if err := verifyRekorEntry(ctx, ko, tlogEntry, verifier, cert, b64sig, blobBytes); err != nil {
-			continue
-		}
-		validSigExists = true
+		validityTime = time.Unix(*e.IntegratedTime, 0)
+		fmt.Fprintf(os.Stderr, "tlog entry verified with uuid: %s index: %d\n", hex.EncodeToString(uuid), *e.LogIndex)
+	// If we do not have access to a bundle, a Rekor entry, or the access to lookup,
+	// then we can only use the current time as the signature creation time to verify
+	// the signature was created when the certificate was valid.
+	default:
+		validityTime = time.Now()
 	}
-	if !validSigExists {
-		fmt.Fprintln(os.Stderr, `WARNING: No valid entries were found in rekor to verify this blob.
 
-Transparency log support for blobs is experimental, and occasionally an entry isn't found even if one exists.
+	// 3. If a certificate is provided, check it's expiration.
+	if cert == nil {
+		return nil
+	}
 
-We recommend requesting the certificate/signature from the original signer of this blob and manually verifying with cosign verify-blob --cert [cert] --signature [signature].`)
-		return fmt.Errorf("could not find a valid tlog entry for provided blob, found %d invalid entries", len(uuids))
+	return cosign.CheckExpiry(cert, validityTime)
+}
+
+func tlogFindPublicKey(ctx context.Context, rekorClient *client.Rekor,
+	blobBytes []byte, sig string, pub crypto.PublicKey) (*models.LogEntryAnon, error) {
+	pemBytes, err := cryptoutils.MarshalPublicKeyToPEM(pub)
+	if err != nil {
+		return nil, err
 	}
-	fmt.Fprintln(os.Stderr, "Verified OK")
-	return nil
+	return tlogFindEntry(ctx, rekorClient, blobBytes, sig, pemBytes)
+}
+
+func tlogFindCertificate(ctx context.Context, rekorClient *client.Rekor,
+	blobBytes []byte, sig string, cert *x509.Certificate) (*models.LogEntryAnon, error) {
+	pemBytes, err := cryptoutils.MarshalCertificateToPEM(cert)
+	if err != nil {
+		return nil, err
+	}
+	return tlogFindEntry(ctx, rekorClient, blobBytes, sig, pemBytes)
 }
 
-// signatures returns the raw signature and the base64 encoded signature
-func signatures(sigRef string, bundlePath string) (string, string, error) {
+func tlogFindEntry(ctx context.Context, client *client.Rekor,
+	blobBytes []byte, sig string, pem []byte) (*models.LogEntryAnon, error) {
+	b64sig := base64.StdEncoding.EncodeToString([]byte(sig))
+	return cosign.FindTlogEntry(ctx, client, b64sig, blobBytes, pem)
+}
+
+// signatures returns the raw signature
+func signatures(sigRef string, bundlePath string) (string, error) {
 	var targetSig []byte
 	var err error
 	switch {
@@ -273,18 +392,18 @@ func signatures(sigRef string, bundlePath string) (string, string, error) {
 		if err != nil {
 			if !os.IsNotExist(err) {
 				// ignore if file does not exist, it can be a base64 encoded string as well
-				return "", "", err
+				return "", err
 			}
 			targetSig = []byte(sigRef)
 		}
 	case bundlePath != "":
 		b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
 		if err != nil {
-			return "", "", err
+			return "", err
 		}
 		targetSig = []byte(b.Base64Signature)
 	default:
-		return "", "", fmt.Errorf("missing flag '--signature'")
+		return "", fmt.Errorf("missing flag '--signature'")
 	}
 
 	var sig, b64sig string
@@ -296,7 +415,7 @@ func signatures(sigRef string, bundlePath string) (string, string, error) {
 		sig = string(targetSig)
 		b64sig = base64.StdEncoding.EncodeToString(targetSig)
 	}
-	return sig, b64sig, nil
+	return sig, nil
 }
 
 func payloadBytes(blobRef string) ([]byte, error) {
@@ -313,119 +432,176 @@ func payloadBytes(blobRef string) ([]byte, error) {
 	return blobBytes, nil
 }
 
-func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error {
-	// TODO: This can be moved below offline bundle verification when SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY
-	// is removed.
-	rekorClient, err := rekor.NewClient(ko.RekorURL)
+// TODO: RekorClient can be removed when SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY
+// is removed.
+func verifyRekorBundle(ctx context.Context, bundlePath string, rekorClient *client.Rekor,
+	blobBytes []byte, sig string, pubKeyBytes []byte) (*bundle.RekorPayload, error) {
+	b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
 	if err != nil {
-		return err
-	}
-
-	// If we have a bundle with a rekor entry, let's first try to verify offline
-	if ko.BundlePath != "" {
-		if err := verifyRekorBundle(ctx, ko.BundlePath, cert, rekorClient); err == nil {
-			fmt.Fprintf(os.Stderr, "tlog entry verified offline\n")
-			return nil
-		}
+		return nil, err
 	}
-	if !options.EnableExperimental() {
-		return nil
+	if b.Bundle == nil {
+		return nil, fmt.Errorf("rekor entry could not be extracted from local bundle")
 	}
 
-	// Only fetch from rekor tlog if we don't already have the entry.
-	if e == nil {
-		var pubBytes []byte
-		if pubKey != nil {
-			pubBytes, err = sigs.PublicKeyPem(pubKey, signatureoptions.WithContext(ctx))
-			if err != nil {
-				return err
-			}
-		}
-		if cert != nil {
-			pubBytes, err = cryptoutils.MarshalCertificateToPEM(cert)
-			if err != nil {
-				return err
-			}
-		}
-		e, err = cosign.FindTlogEntry(ctx, rekorClient, b64sig, blobBytes, pubBytes)
-		if err != nil {
-			return err
-		}
+	if err := verifyBundleMatchesData(ctx, b.Bundle, blobBytes, pubKeyBytes, []byte(sig)); err != nil {
+		return nil, err
 	}
 
-	if err := cosign.VerifyTLogEntry(ctx, rekorClient, e); err != nil {
-		return nil
+	publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient)
+	if err != nil {
+		return nil, fmt.Errorf("retrieving rekor public key: %w", err)
 	}
 
-	uuid, err := cosign.ComputeLeafHash(e)
+	pubKey, ok := publicKeys[b.Bundle.Payload.LogID]
+	if !ok {
+		return nil, errors.New("rekor log public key not found for payload")
+	}
+	err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey)
 	if err != nil {
-		return err
+		return nil, err
 	}
-
-	fmt.Fprintf(os.Stderr, "tlog entry verified with uuid: %s index: %d\n", hex.EncodeToString(uuid), *e.LogIndex)
-	if cert == nil {
-		return nil
+	if pubKey.Status != tuf.Active {
+		fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n")
 	}
-	// if we have a cert, we should check expiry
-	return cosign.CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0))
+
+	return &b.Bundle.Payload, nil
 }
 
-func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate, rekorClient *client.Rekor) error {
-	b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
+func verifyBundleMatchesData(ctx context.Context, bundle *bundle.RekorBundle, blobBytes, certBytes, sigBytes []byte) error {
+	eimpl, kind, apiVersion, err := unmarshalEntryImpl(bundle.Payload.Body.(string))
 	if err != nil {
 		return err
 	}
-	if b.Bundle == nil {
-		return fmt.Errorf("rekor entry is not available")
-	}
-	publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient)
+
+	targetImpl, err := reconstructCanonicalizedEntry(ctx, kind, apiVersion, blobBytes, certBytes, sigBytes)
 	if err != nil {
-		return fmt.Errorf("retrieving rekor public key: %w", err)
+		return fmt.Errorf("recontructing rekorEntry for bundle comparison: %w", err)
 	}
 
-	pubKey, ok := publicKeys[b.Bundle.Payload.LogID]
-	if !ok {
-		return errors.New("rekor log public key not found for payload")
+	switch e := eimpl.(type) {
+	case *rekord_v001.V001Entry:
+		t := targetImpl.(*rekord_v001.V001Entry)
+		data, err := e.RekordObj.Data.Content.MarshalText()
+		if err != nil {
+			return fmt.Errorf("invalid rekord data: %w", err)
+		}
+		tData, err := t.RekordObj.Data.Content.MarshalText()
+		if err != nil {
+			return fmt.Errorf("invalid rekord data: %w", err)
+		}
+		if !bytes.Equal(data, tData) {
+			return fmt.Errorf("rekord data does not match blob")
+		}
+		if err := compareBase64Strings(e.RekordObj.Signature.Content.String(),
+			t.RekordObj.Signature.Content.String()); err != nil {
+			return fmt.Errorf("rekord signature does not match bundle %s", err)
+		}
+		if err := compareBase64Strings(e.RekordObj.Signature.PublicKey.Content.String(),
+			t.RekordObj.Signature.PublicKey.Content.String()); err != nil {
+			return fmt.Errorf("rekord public key does not match bundle")
+		}
+	case *hashedrekord_v001.V001Entry:
+		t := targetImpl.(*hashedrekord_v001.V001Entry)
+		if *e.HashedRekordObj.Data.Hash.Value != *t.HashedRekordObj.Data.Hash.Value {
+			return fmt.Errorf("hashedRekord data does not match blob")
+		}
+		if err := compareBase64Strings(e.HashedRekordObj.Signature.Content.String(),
+			t.HashedRekordObj.Signature.Content.String()); err != nil {
+			return fmt.Errorf("hashedRekord signature does not match bundle %s", err)
+		}
+		if err := compareBase64Strings(e.HashedRekordObj.Signature.PublicKey.Content.String(),
+			t.HashedRekordObj.Signature.PublicKey.Content.String()); err != nil {
+			return fmt.Errorf("hashedRekord public key does not match bundle")
+		}
+	case *intoto_v001.V001Entry:
+		t := targetImpl.(*intoto_v001.V001Entry)
+		if *e.IntotoObj.Content.Hash.Value != *t.IntotoObj.Content.Hash.Value {
+			return fmt.Errorf("intoto content hash does not match attestation")
+		}
+		if *e.IntotoObj.Content.PayloadHash.Value != *t.IntotoObj.Content.PayloadHash.Value {
+			return fmt.Errorf("intoto payload hash does not match attestation")
+		}
+		if err := compareBase64Strings(e.IntotoObj.PublicKey.String(),
+			t.IntotoObj.PublicKey.String()); err != nil {
+			return fmt.Errorf("intoto public key does not match bundle")
+		}
+	default:
+		return errors.New("unexpected tlog entry type")
 	}
-	err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey)
+	return nil
+}
+
+func reconstructCanonicalizedEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) {
+	props := types.ArtifactProperties{
+		PublicKeyBytes: certBytes,
+		PKIFormat:      string(pki.X509),
+	}
+	switch kind {
+	case rekord.KIND:
+		props.ArtifactBytes = blobBytes
+		props.SignatureBytes = sigBytes
+	case hashedrekord.KIND:
+		blobHash := sha256.Sum256(blobBytes)
+		props.ArtifactHash = strings.ToLower(hex.EncodeToString(blobHash[:]))
+		props.SignatureBytes = sigBytes
+	case intoto.KIND:
+		props.ArtifactBytes = blobBytes
+	default:
+		return nil, fmt.Errorf("unexpected entry kind: %s", kind)
+	}
+	proposedEntry, err := types.NewProposedEntry(ctx, kind, apiVersion, props)
 	if err != nil {
-		return err
+		return nil, err
 	}
-	if pubKey.Status != tuf.Active {
-		fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n")
+	entry, err := types.NewEntry(proposedEntry)
+	if err != nil {
+		return nil, err
 	}
-
-	if cert == nil {
-		return nil
+	can, err := entry.Canonicalize(ctx)
+	if err != nil {
+		return nil, err
+	}
+	proposedEntryCan, err := models.UnmarshalProposedEntry(bytes.NewReader(can), runtime.JSONConsumer())
+	if err != nil {
+		return nil, err
 	}
-	it := time.Unix(b.Bundle.Payload.IntegratedTime, 0)
-	return cosign.CheckExpiry(cert, it)
+	return types.NewEntry(proposedEntryCan)
 }
 
-func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) {
-	b, err := base64.StdEncoding.DecodeString(e.Body.(string))
+// unmarshalEntryImpl decodes the base64-encoded entry to a specific entry type (types.EntryImpl).
+func unmarshalEntryImpl(e string) (types.EntryImpl, string, string, error) {
+	b, err := base64.StdEncoding.DecodeString(e)
 	if err != nil {
-		return nil, err
+		return nil, "", "", err
 	}
 
 	pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer())
 	if err != nil {
-		return nil, err
+		return nil, "", "", err
+	}
+
+	entry, err := types.NewEntry(pe)
+	if err != nil {
+		return nil, "", "", err
 	}
+	return entry, pe.Kind(), entry.APIVersion(), nil
+}
 
-	eimpl, err := types.NewEntry(pe)
+func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) {
+	eimpl, _, _, err := unmarshalEntryImpl(e.Body.(string))
 	if err != nil {
 		return nil, err
 	}
 
 	var publicKeyB64 []byte
 	switch e := eimpl.(type) {
-	case *rekord.V001Entry:
+	case *rekord_v001.V001Entry:
 		publicKeyB64, err = e.RekordObj.Signature.PublicKey.Content.MarshalText()
 		if err != nil {
 			return nil, err
 		}
-	case *hashedrekord.V001Entry:
+	case *hashedrekord_v001.V001Entry:
 		publicKeyB64, err = e.HashedRekordObj.Signature.PublicKey.Content.MarshalText()
 		if err != nil {
 			return nil, err
@@ -463,3 +639,19 @@ func isIntotoDSSE(blobBytes []byte) bool {
 
 	return true
 }
+
+// TODO: Use this function to compare bundle signatures in OCI.
+func compareBase64Strings(got string, expected string) error {
+	decodeFirst, err := base64.StdEncoding.DecodeString(got)
+	if err != nil {
+		return fmt.Errorf("decoding base64 string %s", got)
+	}
+	decodeSecond, err := base64.StdEncoding.DecodeString(expected)
+	if err != nil {
+		return fmt.Errorf("decoding base64 string %s", expected)
+	}
+	if !bytes.Equal(decodeFirst, decodeSecond) {
+		return fmt.Errorf("comparing base64 strings, expected %s, got %s", expected, got)
+	}
+	return nil
+}

programming_language: Go
filename: verify_blob_test.go
change_type: ModificationType.MODIFY
diff: @@ -15,14 +15,46 @@
 package verify
 
 import (
+	"bytes"
+	"context"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/sha256"
+	"crypto/x509"
 	"encoding/base64"
+	"encoding/hex"
 	"encoding/json"
+	"fmt"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
+	"time"
 
-	"github.com/secure-systems-lab/go-securesystemslib/dsse"
+	"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
+	"github.com/go-openapi/swag"
+	ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse"
+	"github.com/sigstore/cosign/cmd/cosign/cli/options"
+	"github.com/sigstore/cosign/internal/pkg/cosign/rekor/mock"
 	"github.com/sigstore/cosign/pkg/cosign"
+	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	sigs "github.com/sigstore/cosign/pkg/signature"
+	ctypes "github.com/sigstore/cosign/pkg/types"
+	"github.com/sigstore/cosign/test"
+	"github.com/sigstore/rekor/pkg/generated/client"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/pki"
+	"github.com/sigstore/rekor/pkg/types"
+	"github.com/sigstore/rekor/pkg/types/hashedrekord"
+	hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
+	"github.com/sigstore/rekor/pkg/types/intoto"
+	"github.com/sigstore/rekor/pkg/types/rekord"
+	"github.com/sigstore/sigstore/pkg/cryptoutils"
+	"github.com/sigstore/sigstore/pkg/signature"
+	"github.com/sigstore/sigstore/pkg/signature/dsse"
+	signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
 )
 
 func TestSignaturesRef(t *testing.T) {
@@ -48,7 +80,7 @@ func TestSignaturesRef(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.description, func(t *testing.T) {
-			gotSig, gotb64Sig, err := signatures(test.sigRef, "")
+			gotSig, err := signatures(test.sigRef, "")
 			if test.shouldErr && err != nil {
 				return
 			}
@@ -58,9 +90,6 @@ func TestSignaturesRef(t *testing.T) {
 			if gotSig != sig {
 				t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig)
 			}
-			if gotb64Sig != b64sig {
-				t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig)
-			}
 		})
 	}
 }
@@ -84,28 +113,25 @@ func TestSignaturesBundle(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	gotSig, gotb64Sig, err := signatures("", fp)
+	gotSig, err := signatures("", fp)
 	if err != nil {
 		t.Fatal(err)
 	}
 	if gotSig != sig {
 		t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig)
 	}
-	if gotb64Sig != b64sig {
-		t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig)
-	}
 }
 
 func TestIsIntotoDSSEWithEnvelopes(t *testing.T) {
 	tts := []struct {
-		envelope     dsse.Envelope
+		envelope     ssldsse.Envelope
 		isIntotoDSSE bool
 	}{
 		{
-			envelope: dsse.Envelope{
+			envelope: ssldsse.Envelope{
 				PayloadType: "application/vnd.in-toto+json",
 				Payload:     base64.StdEncoding.EncodeToString([]byte("This is a test")),
-				Signatures:  []dsse.Signature{},
+				Signatures:  []ssldsse.Signature{},
 			},
 			isIntotoDSSE: true,
 		},
@@ -141,3 +167,950 @@ func TestIsIntotoDSSEWithBytes(t *testing.T) {
 		}
 	}
 }
+
+// Does not test identity options, only blob verification with different
+// options.
+func TestVerifyBlob(t *testing.T) {
+	ctx := context.Background()
+	td := t.TempDir()
+
+	leafPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	signer, err := signature.LoadECDSASignerVerifier(leafPriv, crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	pubKeyBytes, err := sigs.PublicKeyPem(signer, signatureoptions.WithContext(ctx))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Generate expired and unexpired certificates
+	identity := "hello@foo.com"
+	issuer := "issuer"
+	rootCert, rootPriv, _ := test.GenerateRootCa()
+	rootPool := x509.NewCertPool()
+	rootPool.AddCert(rootCert)
+	unexpiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer,
+		time.Now(), leafPriv, rootCert, rootPriv)
+	unexpiredCertPem, _ := cryptoutils.MarshalCertificateToPEM(unexpiredLeafCert)
+
+	expiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer,
+		time.Now().Add(-time.Hour), leafPriv, rootCert, rootPriv)
+	expiredLeafPem, _ := cryptoutils.MarshalCertificateToPEM(expiredLeafCert)
+
+	// Make rekor signer
+	rekorPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rekorSigner, err := signature.LoadECDSASignerVerifier(rekorPriv, crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	pemRekor, err := cryptoutils.MarshalPublicKeyToPEM(rekorSigner.Public())
+	if err != nil {
+		t.Fatal(err)
+	}
+	tmpRekorPubFile, err := os.CreateTemp(td, "cosign_rekor_pub_*.key")
+	if err != nil {
+		t.Fatalf("failed to create temp rekor pub file: %v", err)
+	}
+	defer tmpRekorPubFile.Close()
+	if _, err := tmpRekorPubFile.Write(pemRekor); err != nil {
+		t.Fatalf("failed to write rekor pub file: %v", err)
+	}
+	t.Setenv("SIGSTORE_REKOR_PUBLIC_KEY", tmpRekorPubFile.Name())
+
+	var makeSignature = func(blob []byte) string {
+		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
+		if err != nil {
+			t.Fatal(err)
+		}
+		return string(sig)
+	}
+	blobBytes := []byte("foo")
+	blobSignature := makeSignature(blobBytes)
+
+	otherBytes := []byte("bar")
+	otherSignature := makeSignature(otherBytes)
+
+	tts := []struct {
+		name        string
+		blob        []byte
+		signature   string
+		sigVerifier signature.Verifier
+		cert        *x509.Certificate
+		bundlePath  string
+		// If online lookups to Rekor are enabled
+		experimental bool
+		// The rekor entry response when Rekor is enabled
+		rekorEntry *models.LogEntry
+		shouldErr  bool
+	}{
+		{
+			name:         "valid signature with public key",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			shouldErr:    false,
+		},
+		{
+			name:         "valid signature with public key - experimental no rekor fail",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: true,
+			rekorEntry:   nil,
+			shouldErr:    true,
+		},
+		{
+			name:         "valid signature with public key - experimental rekor entry success",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: true,
+			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				pubKeyBytes, true),
+			shouldErr: false,
+		},
+		{
+			name:         "valid signature with public key - good bundle provided",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				pubKeyBytes, true),
+			shouldErr: false,
+		},
+		{
+			name:         "valid signature with public key - bad bundle SET",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature),
+				unexpiredCertPem, true),
+			shouldErr: true,
+		},
+		{
+			name:         "valid signature with public key - bad bundle cert mismatch",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				unexpiredCertPem, true),
+			shouldErr: true,
+		},
+		{
+			name:         "valid signature with public key - bad bundle signature mismatch",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)),
+				pubKeyBytes, true),
+			shouldErr: true,
+		},
+		{
+			name:         "valid signature with public key - bad bundle msg & signature mismatch",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature),
+				pubKeyBytes, true),
+			shouldErr: true,
+		},
+		{
+			name:         "invalid signature with public key",
+			blob:         blobBytes,
+			signature:    otherSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			shouldErr:    true,
+		},
+		{
+			name:         "invalid signature with public key - experimental",
+			blob:         blobBytes,
+			signature:    otherSignature,
+			sigVerifier:  signer,
+			experimental: true,
+			shouldErr:    true,
+		},
+		{
+			name:         "valid signature with unexpired certificate",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			cert:         unexpiredLeafCert,
+			experimental: false,
+			shouldErr:    false,
+		},
+		{
+			name:         "valid signature with unexpired certificate - bad bundle cert mismatch",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			cert:         unexpiredLeafCert,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				pubKeyBytes, true),
+			shouldErr: true,
+		},
+		{
+			name:         "valid signature with unexpired certificate - bad bundle signature mismatch",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			cert:         unexpiredLeafCert,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)),
+				unexpiredCertPem, true),
+			shouldErr: true,
+		},
+		{
+			name:         "valid signature with unexpired certificate - bad bundle msg & signature mismatch",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			experimental: false,
+			cert:         unexpiredLeafCert,
+			bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature),
+				unexpiredCertPem, true),
+			shouldErr: true,
+		},
+		{
+			name:         "invalid signature with unexpired certificate",
+			blob:         blobBytes,
+			signature:    otherSignature,
+			sigVerifier:  signer,
+			cert:         unexpiredLeafCert,
+			experimental: false,
+			shouldErr:    true,
+		},
+		{
+			name:         "valid signature with unexpired certificate - experimental",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			cert:         unexpiredLeafCert,
+			sigVerifier:  signer,
+			experimental: true,
+			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				unexpiredCertPem, true),
+			shouldErr: false,
+		},
+
+		{
+			name:         "valid signature with unexpired certificate - experimental & rekor entry found",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			cert:         unexpiredLeafCert,
+			experimental: true,
+			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				unexpiredCertPem, true),
+			shouldErr: false,
+		},
+		{
+			name:         "valid signature with expired certificate",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			cert:         expiredLeafCert,
+			sigVerifier:  signer,
+			experimental: false,
+			shouldErr:    true,
+		},
+
+		{
+			name:         "valid signature with expired certificate - experimental good rekor lookup",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			cert:         expiredLeafCert,
+			experimental: true,
+			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				expiredLeafPem, true),
+			shouldErr: false,
+		},
+
+		{
+			name:         "valid signature with expired certificate - experimental bad rekor integrated time",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			cert:         expiredLeafCert,
+			sigVerifier:  signer,
+			experimental: true,
+			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				expiredLeafPem, false),
+			shouldErr: true,
+		},
+
+		{
+			name:         "valid signature with unexpired certificate - good bundle, nonexperimental",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			cert:         unexpiredLeafCert,
+			sigVerifier:  signer,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				unexpiredCertPem, true),
+			shouldErr: false,
+		},
+		{
+			name:         "valid signature with expired certificate - good bundle, nonexperimental",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			cert:         expiredLeafCert,
+			sigVerifier:  signer,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				expiredLeafPem, true),
+			shouldErr: false,
+		},
+		{
+			name:         "valid signature with expired certificate - bundle with bad expiration",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			cert:         expiredLeafCert,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				expiredLeafPem, false),
+			shouldErr: true,
+		},
+		{
+			name:         "valid signature with expired certificate - bundle with bad SET",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			cert:         expiredLeafCert,
+			experimental: false,
+			bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature),
+				expiredLeafPem, true),
+			shouldErr: true,
+		},
+		{
+			name:         "valid signature with expired certificate - experimental good bundle",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			cert:         expiredLeafCert,
+			experimental: true,
+			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
+				expiredLeafPem, true),
+			shouldErr: false,
+		},
+		{
+			name:         "valid signature with expired certificate - experimental bad rekor entry",
+			blob:         blobBytes,
+			signature:    blobSignature,
+			sigVerifier:  signer,
+			cert:         expiredLeafCert,
+			experimental: true,
+			// This is the wrong signer for the SET!
+			rekorEntry: makeRekorEntry(t, *signer, blobBytes, []byte(blobSignature),
+				expiredLeafPem, true),
+			shouldErr: true,
+		},
+	}
+	for _, tt := range tts {
+		t.Run(tt.name, func(t *testing.T) {
+			tt := tt
+			var mClient client.Rekor
+			mClient.Entries = &mock.EntriesClient{Entries: tt.rekorEntry}
+			co := &cosign.CheckOpts{
+				SigVerifier: tt.sigVerifier,
+				RootCerts:   rootPool,
+			}
+			// if expermental is enabled, add RekorClient to co.
+			if tt.experimental {
+				co.RekorClient = &mClient
+			}
+
+			err := verifyBlob(ctx, co, tt.blob, tt.signature, tt.cert, tt.bundlePath, nil)
+			if (err != nil) != tt.shouldErr {
+				t.Fatalf("verifyBlob()= %s, expected shouldErr=%t ", err, tt.shouldErr)
+			}
+		})
+	}
+}
+
+func makeRekorEntry(t *testing.T, rekorSigner signature.ECDSASignerVerifier,
+	pyld, sig, svBytes []byte, expiryValid bool) *models.LogEntry {
+	ctx := context.Background()
+	// Calculate log ID, the digest of the Rekor public key
+	logID, err := getLogID(rekorSigner.Public())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	hashedrekord := &hashedrekord_v001.V001Entry{}
+	h := sha256.Sum256(pyld)
+	pe, err := hashedrekord.CreateFromArtifactProperties(ctx, types.ArtifactProperties{
+		ArtifactHash:   hex.EncodeToString(h[:]),
+		SignatureBytes: sig,
+		PublicKeyBytes: svBytes,
+		PKIFormat:      "x509",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	entry, err := types.NewEntry(pe)
+	if err != nil {
+		t.Fatal(err)
+	}
+	leaf, err := entry.Canonicalize(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	integratedTime := time.Now()
+	certs, _ := cryptoutils.UnmarshalCertificatesFromPEM(svBytes)
+	if certs != nil && len(certs) > 0 {
+		if expiryValid {
+			integratedTime = certs[0].NotAfter.Add(-time.Second)
+		} else {
+			integratedTime = certs[0].NotAfter.Add(time.Second)
+		}
+	}
+	e := models.LogEntryAnon{
+		Body:           base64.StdEncoding.EncodeToString(leaf),
+		IntegratedTime: swag.Int64(integratedTime.Unix()),
+		LogIndex:       swag.Int64(0),
+		LogID:          swag.String(logID),
+	}
+	// Marshal payload, sign, and set SET in Bundle
+	jsonPayload, err := json.Marshal(e)
+	if err != nil {
+		t.Fatal(err)
+	}
+	canonicalized, err := jsoncanonicalizer.Transform(jsonPayload)
+	if err != nil {
+		t.Fatal(err)
+	}
+	bundleSig, err := rekorSigner.SignMessage(bytes.NewReader(canonicalized))
+	if err != nil {
+		t.Fatal(err)
+	}
+	uuid, _ := cosign.ComputeLeafHash(&e)
+
+	e.Verification = &models.LogEntryAnonVerification{
+		SignedEntryTimestamp: bundleSig,
+		InclusionProof: &models.InclusionProof{
+			LogIndex: swag.Int64(0),
+			TreeSize: swag.Int64(1),
+			RootHash: swag.String(hex.EncodeToString(uuid)),
+			Hashes:   []string{},
+		},
+	}
+	return &models.LogEntry{hex.EncodeToString(uuid): e}
+}
+
+func makeLocalBundle(t *testing.T, rekorSigner signature.ECDSASignerVerifier,
+	pyld []byte, sig []byte, svBytes []byte, expiryValid bool) string {
+	td := t.TempDir()
+
+	// Create bundle.
+	entry := makeRekorEntry(t, rekorSigner, pyld, sig, svBytes, expiryValid)
+	var e models.LogEntryAnon
+	for _, v := range *entry {
+		e = v
+	}
+	b := cosign.LocalSignedPayload{
+		Base64Signature: base64.StdEncoding.EncodeToString(sig),
+		Cert:            string(svBytes),
+		Bundle: &bundle.RekorBundle{
+			Payload: bundle.RekorPayload{
+				Body:           e.Body,
+				IntegratedTime: *e.IntegratedTime,
+				LogIndex:       *e.LogIndex,
+				LogID:          *e.LogID,
+			},
+			SignedEntryTimestamp: e.Verification.SignedEntryTimestamp,
+		},
+	}
+
+	// Write bundle to disk
+	jsonBundle, err := json.Marshal(b)
+	if err != nil {
+		t.Fatal(err)
+	}
+	bundlePath := filepath.Join(td, "bundle.sig")
+	if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil {
+		t.Fatal(err)
+	}
+	return bundlePath
+}
+
+func TestVerifyBlobCmdWithBundle(t *testing.T) {
+	keyless := newKeylessStack(t)
+
+	t.Run("Normal verification", func(t *testing.T) {
+		identity := "hello@foo.com"
+		issuer := "issuer"
+		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
+
+		// Create blob
+		blob := "someblob"
+
+		// Sign blob with private key
+		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Create bundle
+		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
+		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
+		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
+		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
+		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
+
+		// Verify command
+		err = VerifyBlobCmd(context.Background(),
+			options.KeyOpts{BundlePath: bundlePath},
+			"",       /*certRef*/ // Cert is fetched from bundle
+			identity, /*certEmail*/
+			issuer,   /*certOidcIssuer*/
+			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
+			"",       /*sigRef*/    // Sig is fetched from bundle
+			blobPath, /*blobRef*/
+			// GitHub identity flags start
+			"", "", "", "", "",
+			// GitHub identity flags end
+			false /*enforceSCT*/)
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+	t.Run("Mismatched cert/sig", func(t *testing.T) {
+		// This test ensures that the signature and cert at the top level in the LocalSignedPayload must be identical to the ones in the RekorBundle.
+		identity := "hello@foo.com"
+		issuer := "issuer"
+		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
+		_, _, leafPemCert2, signer2 := keyless.genLeafCert(t, identity, issuer)
+
+		// Create blob
+		blob := "someblob"
+
+		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		sig2, err := signer2.SignMessage(bytes.NewReader([]byte(blob)))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Create bundle
+		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert2, sig2)
+		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
+		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
+		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
+		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
+
+		// Verify command
+		err = VerifyBlobCmd(context.Background(),
+			options.KeyOpts{BundlePath: bundlePath},
+			"",       /*certRef*/ // Cert is fetched from bundle
+			"",       /*certEmail*/
+			"",       /*certOidcIssuer*/
+			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
+			"",       /*sigRef*/    // Sig is fetched from bundle
+			blobPath, /*blobRef*/
+			// GitHub identity flags start
+			"", "", "", "", "",
+			// GitHub identity flags end
+			false /*enforceSCT*/)
+		if err == nil {
+			t.Fatal("expecting err due to mismatched signatures, got nil")
+		}
+	})
+	t.Run("Expired cert", func(t *testing.T) {
+		identity := "hello@foo.com"
+		issuer := "issuer"
+		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
+
+		// Create blob
+		blob := "someblob"
+
+		// Sign blob with private key
+		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Create bundle
+		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
+		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()-1, entry)
+		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
+		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
+		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
+
+		// Verify command
+		err = VerifyBlobCmd(context.Background(),
+			options.KeyOpts{BundlePath: bundlePath},
+			"",       /*certRef*/ // Cert is fetched from bundle
+			"",       /*certEmail*/
+			"",       /*certOidcIssuer*/
+			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
+			"",       /*sigRef*/    // Sig is fetched from bundle
+			blobPath, /*blobRef*/
+			// GitHub identity flags start
+			"", "", "", "", "",
+			// GitHub identity flags end
+			false /*enforceSCT*/)
+		if err == nil {
+			t.Fatal("expected error due to expired cert, received nil")
+		}
+	})
+	t.Run("Attestation", func(t *testing.T) {
+		identity := "hello@foo.com"
+		issuer := "issuer"
+		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
+
+		stmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}`
+		wrapped := dsse.WrapSigner(signer, ctypes.IntotoPayloadType)
+		signedPayload, err := wrapped.SignMessage(bytes.NewReader([]byte(stmt)), signatureoptions.WithContext(context.Background()))
+		if err != nil {
+			t.Fatal(err)
+		}
+		// intoto sig = json-serialized dsse envelope
+		sig := signedPayload
+
+		// Create bundle
+		entry := genRekorEntry(t, intoto.KIND, intoto.New().DefaultVersion(), signedPayload, leafPemCert, sig)
+		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
+		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
+		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
+		blobPath := writeBlobFile(t, keyless.td, string(signedPayload), "attestation.txt")
+
+		// Verify command
+		err = VerifyBlobCmd(context.Background(),
+			options.KeyOpts{BundlePath: bundlePath},
+			"",       /*certRef*/ // Cert is fetched from bundle
+			"",       /*certEmail*/
+			"",       /*certOidcIssuer*/
+			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
+			"",       /*sigRef*/    // Sig is fetched from bundle
+			blobPath, /*blobRef*/
+			// GitHub identity flags start
+			"", "", "", "", "",
+			// GitHub identity flags end
+			false /*enforceSCT*/)
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+	t.Run("Invalid blob signature", func(t *testing.T) {
+		identity := "hello@foo.com"
+		issuer := "issuer"
+		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
+
+		// Create blob
+		blob := "someblob"
+
+		// Sign blob with private key
+		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Create bundle
+		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
+		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
+		b.Bundle.SignedEntryTimestamp = []byte{'i', 'n', 'v', 'a', 'l', 'i', 'd'}
+		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
+		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
+
+		// Verify command
+		err = VerifyBlobCmd(context.Background(),
+			options.KeyOpts{BundlePath: bundlePath},
+			"",       /*certRef*/ // Cert is fetched from bundle
+			"",       /*certEmail*/
+			"",       /*certOidcIssuer*/
+			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
+			"",       /*sigRef*/    // Sig is fetched from bundle
+			blobPath, /*blobRef*/
+			// GitHub identity flags start
+			"", "", "", "", "",
+			// GitHub identity flags end
+			false /*enforceSCT*/)
+		if err == nil || !strings.Contains(err.Error(), "unable to verify SET") {
+			t.Fatalf("expected error verifying SET, got %v", err)
+		}
+	})
+	t.Run("Mismatched certificate email", func(t *testing.T) {
+		identity := "hello@foo.com"
+		issuer := "issuer"
+		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
+
+		// Create blob
+		blob := "someblob"
+
+		// Sign blob with private key
+		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Create bundle
+		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
+		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
+		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
+		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
+		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
+
+		// Verify command
+		err = VerifyBlobCmd(context.Background(),
+			options.KeyOpts{BundlePath: bundlePath},
+			"",                    /*certRef*/ // Cert is fetched from bundle
+			"invalid@example.com", /*certEmail*/
+			issuer,                /*certOidcIssuer*/
+			"",                    /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
+			"",                    /*sigRef*/    // Sig is fetched from bundle
+			blobPath,              /*blobRef*/
+			// GitHub identity flags start
+			"", "", "", "", "",
+			// GitHub identity flags end
+			false /*enforceSCT*/)
+		if err == nil || !strings.Contains(err.Error(), "expected email not found in certificate") {
+			t.Fatalf("expected error with mismatched identity, got %v", err)
+		}
+	})
+	t.Run("Mismatched certificate issuer", func(t *testing.T) {
+		identity := "hello@foo.com"
+		issuer := "issuer"
+		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
+
+		// Create blob
+		blob := "someblob"
+
+		// Sign blob with private key
+		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Create bundle
+		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
+		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
+		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
+		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
+		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
+
+		// Verify command
+		err = VerifyBlobCmd(context.Background(),
+			options.KeyOpts{BundlePath: bundlePath},
+			"",        /*certRef*/ // Cert is fetched from bundle
+			identity,  /*certEmail*/
+			"invalid", /*certOidcIssuer*/
+			"",        /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
+			"",        /*sigRef*/    // Sig is fetched from bundle
+			blobPath,  /*blobRef*/
+			// GitHub identity flags start
+			"", "", "", "", "",
+			// GitHub identity flags end
+			false /*enforceSCT*/)
+		if err == nil || !strings.Contains(err.Error(), "expected oidc issuer not found in certificate") {
+			t.Fatalf("expected error with mismatched issuer, got %v", err)
+		}
+	})
+}
+
+type keylessStack struct {
+	rootCert    *x509.Certificate
+	rootPriv    *ecdsa.PrivateKey
+	rootPemCert []byte
+	subCert     *x509.Certificate
+	subPriv     *ecdsa.PrivateKey
+	subPemCert  []byte
+	rekorSigner *signature.ECDSASignerVerifier
+	rekorLogID  string
+	td          string // temporary directory
+}
+
+func newKeylessStack(t *testing.T) *keylessStack {
+	stack := &keylessStack{td: t.TempDir()}
+	stack.rootCert, stack.rootPriv, _ = test.GenerateRootCa()
+	stack.rootPemCert, _ = cryptoutils.MarshalCertificateToPEM(stack.rootCert)
+	stack.subCert, stack.subPriv, _ = test.GenerateSubordinateCa(stack.rootCert, stack.rootPriv)
+	stack.subPemCert, _ = cryptoutils.MarshalCertificateToPEM(stack.subCert)
+
+	stack.genChainFile(t)
+	stack.genRekor(t)
+	return stack
+}
+
+func (s *keylessStack) genLeafCert(t *testing.T, subject string, issuer string) (*x509.Certificate, *ecdsa.PrivateKey, []byte, *signature.ECDSASignerVerifier) {
+	cert, priv, _ := test.GenerateLeafCert(subject, issuer, s.subCert, s.subPriv)
+	pemCert, _ := cryptoutils.MarshalCertificateToPEM(cert)
+	signer, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return cert, priv, pemCert, signer
+}
+
+func (s *keylessStack) genChainFile(t *testing.T) {
+	var chain []byte
+	chain = append(chain, s.subPemCert...)
+	chain = append(chain, s.rootPemCert...)
+	tmpChainFile, err := os.CreateTemp(s.td, "cosign_fulcio_chain_*.cert")
+	if err != nil {
+		t.Fatalf("failed to create temp chain file: %v", err)
+	}
+	defer tmpChainFile.Close()
+	if _, err := tmpChainFile.Write(chain); err != nil {
+		t.Fatalf("failed to write chain file: %v", err)
+	}
+	// Override for Fulcio root so it doesn't use TUF
+	t.Setenv("SIGSTORE_ROOT_FILE", tmpChainFile.Name())
+}
+
+func (s *keylessStack) genRekor(t *testing.T) {
+	// Create Rekor private key and write to disk
+	rekorPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	s.rekorSigner, err = signature.LoadECDSASignerVerifier(rekorPriv, crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rekorPub := s.rekorSigner.Public()
+	pemRekor, err := cryptoutils.MarshalPublicKeyToPEM(rekorPub)
+	if err != nil {
+		t.Fatal(err)
+	}
+	tmpRekorPubFile, err := os.CreateTemp(s.td, "cosign_rekor_pub_*.key")
+	if err != nil {
+		t.Fatalf("failed to create temp rekor pub file: %v", err)
+	}
+	defer tmpRekorPubFile.Close()
+	if _, err := tmpRekorPubFile.Write(pemRekor); err != nil {
+		t.Fatalf("failed to write rekor pub file: %v", err)
+	}
+
+	// Calculate log ID, the digest of the Rekor public key
+	s.rekorLogID, err = getLogID(rekorPub)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Override for Rekor public key so it doesn't use TUF
+	t.Setenv("SIGSTORE_REKOR_PUBLIC_KEY", tmpRekorPubFile.Name())
+}
+func (s *keylessStack) rekorSignPayload(t *testing.T, payload bundle.RekorPayload) []byte {
+	// Marshal payload, sign, and return SET
+	jsonPayload, err := json.Marshal(payload)
+	if err != nil {
+		t.Fatal(err)
+	}
+	canonicalized, err := jsoncanonicalizer.Transform(jsonPayload)
+	if err != nil {
+		t.Fatal(err)
+	}
+	bundleSig, err := s.rekorSigner.SignMessage(bytes.NewReader(canonicalized))
+	if err != nil {
+		t.Fatal(err)
+	}
+	return bundleSig
+}
+
+// getLogID calculates the digest of a PKIX-encoded public key
+func getLogID(pub crypto.PublicKey) (string, error) {
+	pubBytes, err := x509.MarshalPKIXPublicKey(pub)
+	if err != nil {
+		return "", err
+	}
+	digest := sha256.Sum256(pubBytes)
+	return hex.EncodeToString(digest[:]), nil
+}
+
+func genRekorEntry(t *testing.T, kind, version string, artifact []byte, cert []byte, sig []byte) string {
+	// Generate the Rekor Entry
+	entryImpl, err := createEntry(context.Background(), kind, version, artifact, cert, sig)
+	if err != nil {
+		t.Fatal(err)
+	}
+	entryBytes, err := entryImpl.Canonicalize(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	return base64.StdEncoding.EncodeToString(entryBytes)
+}
+
+func createBundle(t *testing.T, sig []byte, certPem []byte, logID string, integratedTime int64, rekorEntry string) *cosign.LocalSignedPayload {
+	// Create bundle with:
+	// * Blob signature
+	// * Signing certificate
+	// * Bundle with a payload and signature over the payload
+	b := &cosign.LocalSignedPayload{
+		Base64Signature: base64.StdEncoding.EncodeToString(sig),
+		Cert:            string(certPem),
+		Bundle: &bundle.RekorBundle{
+			SignedEntryTimestamp: []byte{},
+			Payload: bundle.RekorPayload{
+				LogID:          logID,
+				IntegratedTime: integratedTime,
+				LogIndex:       1,
+				Body:           rekorEntry,
+			},
+		},
+	}
+
+	return b
+}
+
+func createEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) {
+	props := types.ArtifactProperties{
+		PublicKeyBytes: certBytes,
+		PKIFormat:      string(pki.X509),
+	}
+	switch kind {
+	case rekord.KIND:
+		props.ArtifactBytes = blobBytes
+		props.SignatureBytes = sigBytes
+	case hashedrekord.KIND:
+		blobHash := sha256.Sum256(blobBytes)
+		props.ArtifactHash = strings.ToLower(hex.EncodeToString(blobHash[:]))
+		props.SignatureBytes = sigBytes
+	case intoto.KIND:
+		props.ArtifactBytes = blobBytes
+	default:
+		return nil, fmt.Errorf("unexpected entry kind: %s", kind)
+	}
+	proposedEntry, err := types.NewProposedEntry(ctx, kind, apiVersion, props)
+	if err != nil {
+		return nil, err
+	}
+	return types.NewEntry(proposedEntry)
+}
+
+func writeBundleFile(t *testing.T, td string, b *cosign.LocalSignedPayload, name string) string {
+	// Write bundle to disk
+	jsonBundle, err := json.Marshal(b)
+	if err != nil {
+		t.Fatal(err)
+	}
+	bundlePath := filepath.Join(td, name)
+	if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil {
+		t.Fatal(err)
+	}
+	return bundlePath
+}
+
+func writeBlobFile(t *testing.T, td string, blob string, name string) string {
+	// Write blob to disk
+	blobPath := filepath.Join(td, name)
+	if err := os.WriteFile(blobPath, []byte(blob), 0644); err != nil {
+		t.Fatal(err)
+	}
+	return blobPath
+}

programming_language: Go
filename: mock_rekor_client.go
change_type: ModificationType.MODIFY
diff: @@ -15,81 +15,58 @@
 package mock
 
 import (
-	"encoding/base64"
-	"encoding/hex"
+	"errors"
 
 	"github.com/go-openapi/runtime"
-	"github.com/transparency-dev/merkle/rfc6962"
 
 	"github.com/sigstore/rekor/pkg/generated/client/entries"
 	"github.com/sigstore/rekor/pkg/generated/models"
 )
 
-var (
-	lea = models.LogEntryAnon{
-		Attestation:    &models.LogEntryAnonAttestation{},
-		Body:           base64.StdEncoding.EncodeToString([]byte("asdf")),
-		IntegratedTime: new(int64),
-		LogID:          new(string),
-		LogIndex:       new(int64),
-		Verification: &models.LogEntryAnonVerification{
-			InclusionProof: &models.InclusionProof{
-				RootHash: new(string),
-				TreeSize: new(int64),
-				LogIndex: new(int64),
-			},
-		},
-	}
-	data = models.LogEntry{
-		uuid(lea): lea,
-	}
-)
-
-// uuid generates the UUID for the given LogEntry.
-// This is effectively a reimplementation of
-// pkg/cosign/tlog.go -> verifyUUID / ComputeLeafHash, but separated
-// to avoid a circular dependency.
-// TODO?: Perhaps we should refactor the tlog libraries into a separate
-// package?
-func uuid(e models.LogEntryAnon) string {
-	entryBytes, err := base64.StdEncoding.DecodeString(e.Body.(string))
-	if err != nil {
-		panic(err)
-	}
-	return hex.EncodeToString(rfc6962.DefaultHasher.HashLeaf(entryBytes))
-}
-
 // EntriesClient is a client that implements entries.ClientService for Rekor
 // To use:
 // var mClient client.Rekor
-// mClient.entries = &EntriesClient{}
+// mClient.Entries = &logEntry
 type EntriesClient struct {
-	Entries models.LogEntry
+	Entries *models.LogEntry
 }
 
 func (m *EntriesClient) CreateLogEntry(params *entries.CreateLogEntryParams, opts ...entries.ClientOption) (*entries.CreateLogEntryCreated, error) {
-	return &entries.CreateLogEntryCreated{
-		ETag:     "",
-		Location: "",
-		Payload:  data,
-	}, nil
+	if m.Entries != nil {
+		return &entries.CreateLogEntryCreated{
+			ETag:     "",
+			Location: "",
+			Payload:  *m.Entries,
+		}, nil
+	}
+	return nil, errors.New("entry not provided")
 }
 
 func (m *EntriesClient) GetLogEntryByIndex(params *entries.GetLogEntryByIndexParams, opts ...entries.ClientOption) (*entries.GetLogEntryByIndexOK, error) {
-	return &entries.GetLogEntryByIndexOK{
-		Payload: data,
-	}, nil
+	if m.Entries != nil {
+		return &entries.GetLogEntryByIndexOK{
+			Payload: *m.Entries,
+		}, nil
+	}
+	return nil, errors.New("entry not provided")
 }
 
 func (m *EntriesClient) GetLogEntryByUUID(params *entries.GetLogEntryByUUIDParams, opts ...entries.ClientOption) (*entries.GetLogEntryByUUIDOK, error) {
-	return &entries.GetLogEntryByUUIDOK{
-		Payload: data,
-	}, nil
+	if m.Entries != nil {
+		return &entries.GetLogEntryByUUIDOK{
+			Payload: *m.Entries,
+		}, nil
+	}
+	return nil, errors.New("entry not provided")
 }
 
 func (m *EntriesClient) SearchLogQuery(params *entries.SearchLogQueryParams, opts ...entries.ClientOption) (*entries.SearchLogQueryOK, error) {
+	resp := []models.LogEntry{}
+	if m.Entries != nil {
+		resp = append(resp, *m.Entries)
+	}
 	return &entries.SearchLogQueryOK{
-		Payload: []models.LogEntry{data},
+		Payload: resp,
 	}, nil
 }
 

programming_language: Go
filename: signer_test.go
change_type: ModificationType.MODIFY
diff: @@ -22,10 +22,12 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/go-openapi/swag"
 	"github.com/sigstore/cosign/internal/pkg/cosign/payload"
 	"github.com/sigstore/cosign/internal/pkg/cosign/rekor/mock"
 	"github.com/sigstore/cosign/pkg/cosign"
 	"github.com/sigstore/rekor/pkg/generated/client"
+	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/sigstore/pkg/signature"
 )
 
@@ -48,7 +50,12 @@ func TestSigner(t *testing.T) {
 
 	// Mock out Rekor client
 	var mClient client.Rekor
-	mClient.Entries = &mock.EntriesClient{}
+
+	mClient.Entries = &mock.EntriesClient{
+		Entries: &models.LogEntry{"123": models.LogEntryAnon{
+			LogIndex: swag.Int64(123),
+		}},
+	}
 
 	testSigner := NewSigner(payloadSigner, &mClient)
 

programming_language: Go
filename: verify.go
change_type: ModificationType.MODIFY
diff: @@ -992,7 +992,7 @@ func VerifySET(bundlePayload cbundle.RekorPayload, signature []byte, pub *ecdsa.
 	// verify the SET against the public key
 	hash := sha256.Sum256(canonicalized)
 	if !ecdsa.VerifyASN1(pub, hash[:], signature) {
-		return &VerificationError{"unable to verify"}
+		return &VerificationError{"unable to verify SET"}
 	}
 	return nil
 }

programming_language: Go
filename: verify_test.go
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,7 @@ import (
 	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
+	"encoding/hex"
 	"encoding/json"
 	"encoding/pem"
 	"errors"
@@ -46,11 +47,13 @@ import (
 	"github.com/sigstore/cosign/pkg/types"
 	"github.com/sigstore/cosign/test"
 	"github.com/sigstore/rekor/pkg/generated/client"
+	"github.com/sigstore/rekor/pkg/generated/models"
 	rtypes "github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 	"github.com/sigstore/sigstore/pkg/signature"
 	"github.com/sigstore/sigstore/pkg/signature/options"
 	"github.com/stretchr/testify/require"
+	"github.com/transparency-dev/merkle/rfc6962"
 )
 
 type mockVerifier struct {
@@ -343,6 +346,40 @@ func TestVerifyImageSignatureWithExistingSub(t *testing.T) {
 	}
 }
 
+var (
+	lea = models.LogEntryAnon{
+		Attestation:    &models.LogEntryAnonAttestation{},
+		Body:           base64.StdEncoding.EncodeToString([]byte("asdf")),
+		IntegratedTime: new(int64),
+		LogID:          new(string),
+		LogIndex:       new(int64),
+		Verification: &models.LogEntryAnonVerification{
+			InclusionProof: &models.InclusionProof{
+				RootHash: new(string),
+				TreeSize: new(int64),
+				LogIndex: new(int64),
+			},
+		},
+	}
+	data = models.LogEntry{
+		uuid(lea): lea,
+	}
+)
+
+// uuid generates the UUID for the given LogEntry.
+// This is effectively a reimplementation of
+// pkg/cosign/tlog.go -> verifyUUID / ComputeLeafHash, but separated
+// to avoid a circular dependency.
+// TODO?: Perhaps we should refactor the tlog libraries into a separate
+// package?
+func uuid(e models.LogEntryAnon) string {
+	entryBytes, err := base64.StdEncoding.DecodeString(e.Body.(string))
+	if err != nil {
+		panic(err)
+	}
+	return hex.EncodeToString(rfc6962.DefaultHasher.HashLeaf(entryBytes))
+}
+
 // This test ensures that image signature validation fails properly if we are
 // using a SigVerifier with Rekor.
 // See https://github.com/sigstore/cosign/issues/1816 for more details.
@@ -361,7 +398,9 @@ func TestVerifyImageSignatureWithSigVerifierAndRekor(t *testing.T) {
 	// tlog entry for the signature during validation (even though it does not
 	// match the underlying data / key)
 	mClient := new(client.Rekor)
-	mClient.Entries = &mock.EntriesClient{}
+	mClient.Entries = &mock.EntriesClient{
+		Entries: &data,
+	}
 
 	if _, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{
 		SigVerifier: sv,

programming_language: Go
filename: cert_utils.go
change_type: ModificationType.MODIFY
diff: @@ -69,7 +69,7 @@ func GenerateRootCa() (*x509.Certificate, *ecdsa.PrivateKey, error) {
 			CommonName:   "sigstore",
 			Organization: []string{"sigstore.dev"},
 		},
-		NotBefore:             time.Now().Add(-5 * time.Minute),
+		NotBefore:             time.Now().Add(-5 * time.Hour),
 		NotAfter:              time.Now().Add(5 * time.Hour),
 		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
 		BasicConstraintsValid: true,
@@ -117,6 +117,34 @@ func GenerateSubordinateCa(rootTemplate *x509.Certificate, rootPriv crypto.Signe
 	return cert, priv, nil
 }
 
+func GenerateLeafCertWithExpiration(subject string, oidcIssuer string, expiration time.Time,
+	priv *ecdsa.PrivateKey,
+	parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, error) {
+	certTemplate := &x509.Certificate{
+		SerialNumber:   big.NewInt(1),
+		EmailAddresses: []string{subject},
+		NotBefore:      expiration,
+		NotAfter:       expiration.Add(10 * time.Minute),
+		KeyUsage:       x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:    []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+		IsCA:           false,
+		ExtraExtensions: []pkix.Extension{{
+			// OID for OIDC Issuer extension
+			Id:       asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
+			Critical: false,
+			Value:    []byte(oidcIssuer),
+		},
+		},
+	}
+
+	cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv)
+	if err != nil {
+		return nil, err
+	}
+
+	return cert, nil
+}
+
 func GenerateLeafCert(subject string, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) {
 	certTemplate := &x509.Certificate{
 		SerialNumber:   big.NewInt(1),

programming_language: Go