CVE-2022-36056: Improper Verification of Cryptographic Signature (CWE-347)
value: 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.
CWE description: The product does not verify, or incorrectly verifies, the cryptographic signature for data.
CVSS v3 base score: 5.5
Published: 2022-09-14T20:15Z
Repository: https://github.com/sigstore/cosign
Language: Go
Files changed: cmd/cosign/cli/verify/verify_blob.go, cmd/cosign/cli/verify/verify_blob_test.go, internal/pkg/cosign/rekor/mock/mock_rekor_client.go, internal/pkg/cosign/rekor/signer_test.go, pkg/cosign/verify.go, pkg/cosign/verify_test.go, test/cert_utils.go
Commit date: 2022-09-14 09:37:41 +0100
Commit message: 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>
Diff: diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go
index 4754072c..b3123b99 100644
--- a/cmd/cosign/cli/verify/verify_blob.go
+++ b/cmd/cosign/cli/verify/verify_blob.go
@@ -17,41 +17,47 @@ package verify
 
 import (
 	"bytes"
 	"context"
 	"crypto"
-	_ "crypto/sha256" // for `crypto.SHA256`
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/go-openapi/runtime"
 	ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse"
 	"github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
 	"github.com/sigstore/cosign/cmd/cosign/cli/options"
 	"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"
 	"github.com/sigstore/sigstore/pkg/tuf"
 
 	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"
 )
 
 func isb64(data []byte) bool {
@@ -63,84 +69,94 @@ func isb64(data []byte) bool {
 func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 	certOidcIssuer, certChain, sigRef, blobRef, certGithubWorkflowTrigger, certGithubWorkflowSha,
 	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
 	}
 
 	blobBytes, err := payloadBytes(blobRef)
 	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 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()
 		}
 	case ko.Sk:
 		sk, err := pivkey.GetKeyWithSlot(ko.Slot)
 		if err != nil {
 			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)
 		}
 	case certRef != "":
 		cert, err = loadCertFromFileOrURL(certRef)
 		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
 			}
 		} else {
 			// Verify certificate with chain
 			chain, err := loadCertChainFromFileOrURL(certChain)
 			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
 			}
 		}
 	case ko.BundlePath != "":
@@ -149,144 +165,247 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
 			return err
 		}
 		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)
 		}
 		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
 		}
 
 		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
 	}
 
 	fmt.Fprintln(os.Stderr, "Verified OK")
 	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 {
 	case sigRef != "":
 		targetSig, err = blob.LoadFileOrURL(sigRef)
 		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
 	if isb64(targetSig) {
 		b64sig = string(targetSig)
@@ -294,11 +413,11 @@ func signatures(sigRef string, bundlePath string) (string, string, error) {
 		sig = string(sigBytes)
 	} else {
 		sig = string(targetSig)
 		b64sig = base64.StdEncoding.EncodeToString(targetSig)
 	}
-	return sig, b64sig, nil
+	return sig, nil
 }
 
 func payloadBytes(blobRef string) ([]byte, error) {
 	var blobBytes []byte
 	var err error
@@ -311,123 +430,180 @@ func payloadBytes(blobRef string) ([]byte, error) {
 		return nil, err
 	}
 	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
 		}
 	default:
@@ -461,5 +637,21 @@ func isIntotoDSSE(blobBytes []byte) bool {
 		return false
 	}
 
 	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
+}
diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go
index b6aa05f3..47872e60 100644
--- a/cmd/cosign/cli/verify/verify_blob_test.go
+++ b/cmd/cosign/cli/verify/verify_blob_test.go
@@ -13,18 +13,50 @@
 // limitations under the License.
 
 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) {
 	sig := "a=="
 	b64sig := "YT09"
@@ -46,23 +78,20 @@ 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
 			}
 			if test.shouldErr {
 				t.Fatal("should have received an error")
 			}
 			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 TestSignaturesBundle(t *testing.T) {
@@ -82,32 +111,29 @@ func TestSignaturesBundle(t *testing.T) {
 	}
 	if err := os.WriteFile(fp, contents, 0644); err != nil {
 		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,
 		},
 	}
 	for _, tt := range tts {
@@ -139,5 +165,952 @@ func TestIsIntotoDSSEWithBytes(t *testing.T) {
 		if got != tt.isIntotoDSSE {
 			t.Fatalf("unexpected envelope content")
 		}
 	}
 }
+
+// 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
+}
diff --git a/internal/pkg/cosign/rekor/mock/mock_rekor_client.go b/internal/pkg/cosign/rekor/mock/mock_rekor_client.go
index 2574dfb0..af229242 100644
--- a/internal/pkg/cosign/rekor/mock/mock_rekor_client.go
+++ b/internal/pkg/cosign/rekor/mock/mock_rekor_client.go
@@ -13,85 +13,62 @@
 // limitations under the License.
 
 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
 }
 
 // TODO: Implement mock
 func (m *EntriesClient) SetTransport(transport runtime.ClientTransport) {
diff --git a/internal/pkg/cosign/rekor/signer_test.go b/internal/pkg/cosign/rekor/signer_test.go
index cb705718..95bdc57d 100644
--- a/internal/pkg/cosign/rekor/signer_test.go
+++ b/internal/pkg/cosign/rekor/signer_test.go
@@ -20,14 +20,16 @@ import (
 	"crypto"
 	"encoding/base64"
 	"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"
 )
 
 func mustGetNewSigner(t *testing.T) signature.Signer {
 	t.Helper()
@@ -46,11 +48,16 @@ func TestSigner(t *testing.T) {
 	// Need real cert and chain
 	payloadSigner := payload.NewSigner(mustGetNewSigner(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)
 
 	testPayload := "test payload"
 
diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go
index 7efe8a11..ddba36db 100644
--- a/pkg/cosign/verify.go
+++ b/pkg/cosign/verify.go
@@ -990,11 +990,11 @@ 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
 }
 
 func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) ([][]*x509.Certificate, error) {
diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go
index bc6fdc3f..fbf1692c 100644
--- a/pkg/cosign/verify_test.go
+++ b/pkg/cosign/verify_test.go
@@ -21,10 +21,11 @@ import (
 	"crypto/elliptic"
 	"crypto/rand"
 	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
+	"encoding/hex"
 	"encoding/json"
 	"encoding/pem"
 	"errors"
 	"io"
 	"net"
@@ -44,15 +45,17 @@ import (
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
 	"github.com/sigstore/cosign/pkg/oci/static"
 	"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 {
 	shouldErr bool
 }
@@ -341,10 +344,44 @@ func TestVerifyImageSignatureWithExistingSub(t *testing.T) {
 	if verified == true {
 		t.Fatalf("expected verified=false, got verified=true")
 	}
 }
 
+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.
 func TestVerifyImageSignatureWithSigVerifierAndRekor(t *testing.T) {
 	sv, privKey, err := signature.NewDefaultECDSASignerVerifier()
@@ -359,11 +396,13 @@ func TestVerifyImageSignatureWithSigVerifierAndRekor(t *testing.T) {
 
 	// Add a fake rekor client - this makes it look like there's a matching
 	// 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,
 		RekorClient: mClient,
 	}); err == nil || !strings.Contains(err.Error(), "verifying inclusion proof") {
diff --git a/test/cert_utils.go b/test/cert_utils.go
index 6908a5f6..03fd5fa6 100644
--- a/test/cert_utils.go
+++ b/test/cert_utils.go
@@ -67,11 +67,11 @@ func GenerateRootCa() (*x509.Certificate, *ecdsa.PrivateKey, error) {
 		SerialNumber: big.NewInt(1),
 		Subject: pkix.Name{
 			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,
 		IsCA:                  true,
 	}
@@ -115,10 +115,38 @@ 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),
 		EmailAddresses: []string{subject},
 		NotBefore:      time.Now().Add(-1 * time.Minute),

Vulnerable code: 	_ "crypto/sha256" // for `crypto.SHA256`
	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/sigstore/pkg/signature"
	var verifier signature.Verifier
	sig, b64sig, err := signatures(sigRef, ko.BundlePath)
		verifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef)
		pkcs11Key, ok := verifier.(*pkcs11key.Key)
		verifier, err = sk.Verifier()
		co := &cosign.CheckOpts{
			CertEmail:                    certEmail,
			CertOidcIssuer:               certOidcIssuer,
			CertGithubWorkflowTrigger:    certGithubWorkflowTrigger,
			CertGithubWorkflowSha:        certGithubWorkflowSha,
			CertGithubWorkflowName:       certGithubWorkflowName,
			CertGithubWorkflowRepository: certGithubWorkflowRepository,
			CertGithubWorkflowRef:        certGithubWorkflowRef,
			EnforceSCT:                   enforceSCT,
		}
			// 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)
			verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
		// cert can either be a cert or public key
			verifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256)
			verifier, err = signature.LoadVerifier(cert.PublicKey, crypto.SHA256)
		rClient, err := rekor.NewClient(ko.RekorURL)
		if err != nil {
			return err
		}

		uuids, err := cosign.FindTLogEntriesByPayload(ctx, rClient, blobBytes)
		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)
	}
	// 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 {
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)
			continue
		certs, err := extractCerts(tlogEntry)
		if err != nil {
			continue
		}
		co := &cosign.CheckOpts{
			CertEmail:      certEmail,
			CertOidcIssuer: certOidcIssuer,
			EnforceSCT:     enforceSCT,
		}
		co.RootCerts, err = fulcio.GetRoots()
		if err != nil {
			return fmt.Errorf("getting Fulcio roots: %w", err)
		co.IntermediateCerts, err = fulcio.GetIntermediates()
			return fmt.Errorf("getting Fulcio intermediates: %w", err)
		cert := certs[0]
		verifier, err := cosign.ValidateAndUnpackCert(cert, co)
			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
		// verify the rekor entry
		if err := verifyRekorEntry(ctx, ko, tlogEntry, verifier, cert, b64sig, blobBytes); err != nil {
			continue
		}
		validSigExists = true
	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.
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))
	fmt.Fprintln(os.Stderr, "Verified OK")
	return nil
// signatures returns the raw signature and the base64 encoded signature
func signatures(sigRef string, bundlePath string) (string, string, error) {
				return "", "", err
			return "", "", err
		return "", "", fmt.Errorf("missing flag '--signature'")
	return sig, b64sig, 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)
		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
		}
	if !options.EnableExperimental() {
		return nil
	// 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 := cosign.VerifyTLogEntry(ctx, rekorClient, e); err != nil {
		return nil
	uuid, err := cosign.ComputeLeafHash(e)
		return 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 we have a cert, we should check expiry
	return cosign.CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0))
func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate, rekorClient *client.Rekor) error {
	b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
	if b.Bundle == nil {
		return fmt.Errorf("rekor entry is not available")
	}
	publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient)
		return fmt.Errorf("retrieving rekor public key: %w", err)
	pubKey, ok := publicKeys[b.Bundle.Payload.LogID]
	if !ok {
		return errors.New("rekor log public key not found for payload")
	err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey)
		return err
	if pubKey.Status != tuf.Active {
		fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n")

	if cert == nil {
		return nil
	it := time.Unix(b.Bundle.Payload.IntegratedTime, 0)
	return cosign.CheckExpiry(cert, it)
func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) {
	b, err := base64.StdEncoding.DecodeString(e.Body.(string))
		return nil, err
		return nil, err
	eimpl, err := types.NewEntry(pe)
	case *rekord.V001Entry:
	case *hashedrekord.V001Entry:
	"github.com/secure-systems-lab/go-securesystemslib/dsse"
			gotSig, gotb64Sig, err := signatures(test.sigRef, "")
			if gotb64Sig != b64sig {
				t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig)
			}
	gotSig, gotb64Sig, err := signatures("", fp)
	if gotb64Sig != b64sig {
		t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig)
	}
		envelope     dsse.Envelope
			envelope: dsse.Envelope{
				Signatures:  []dsse.Signature{},
	"encoding/base64"
	"encoding/hex"
	"github.com/transparency-dev/merkle/rfc6962"
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))
}

// mClient.entries = &EntriesClient{}
	Entries models.LogEntry
	return &entries.CreateLogEntryCreated{
		ETag:     "",
		Location: "",
		Payload:  data,
	}, nil
	return &entries.GetLogEntryByIndexOK{
		Payload: data,
	}, nil
	return &entries.GetLogEntryByUUIDOK{
		Payload: data,
	}, nil
		Payload: []models.LogEntry{data},
	mClient.Entries = &mock.EntriesClient{}
		return &VerificationError{"unable to verify"}
	mClient.Entries = &mock.EntriesClient{}
		NotBefore:             time.Now().Add(-5 * time.Minute),
Fixed code: 	"crypto/sha256"
	"strings"
	"github.com/sigstore/cosign/pkg/cosign/bundle"
	"github.com/sigstore/rekor/pkg/pki"
	"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"
	sig, err := signatures(sigRef, ko.BundlePath)
	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)
		}
	}

		co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef)
		pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key)
		co.SigVerifier, err = sk.Verifier()
			co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co)
			co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
		// b.Cert can either be a certificate or public key
			co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256)
			co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co)
			if err != nil {
				return err
			}
	// No certificate is provided: search by artifact sha in the TLOG.
		uuids, err := cosign.FindTLogEntriesByPayload(ctx, co.RekorClient, blobBytes)
		// 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))
	// Performs all blob verification.
	if err := verifyBlob(ctx, co, blobBytes, sig, cert, ko.BundlePath, nil); err != nil {
/* 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)
			return fmt.Errorf("validating cert: %w", err)
	}
	// 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)
	}
	// 1. Verify the signature.
	if err := co.SigVerifier.VerifySignature(strings.NewReader(sig), bytes.NewReader(blobBytes)); err != nil {
		return 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)
			}
		bundle, err := verifyRekorBundle(ctx, bundlePath, co.RekorClient, blobBytes, sig, svBytes)
			// 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
		uuid, err := cosign.ComputeLeafHash(e)
			return err
		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()
	// 3. If a certificate is provided, check it's expiration.
	if cert == nil {
		return nil
	}
	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
	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)
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) {
				return "", err
			return "", err
		return "", fmt.Errorf("missing flag '--signature'")
	return sig, nil
// 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)
		return nil, err
	if b.Bundle == nil {
		return nil, fmt.Errorf("rekor entry could not be extracted from local bundle")
	if err := verifyBundleMatchesData(ctx, b.Bundle, blobBytes, pubKeyBytes, []byte(sig)); err != nil {
		return nil, err
	publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient)
	if err != nil {
		return nil, fmt.Errorf("retrieving rekor public key: %w", err)
	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)
		return nil, err
	if pubKey.Status != tuf.Active {
		fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n")

	return &b.Bundle.Payload, nil
func verifyBundleMatchesData(ctx context.Context, bundle *bundle.RekorBundle, blobBytes, certBytes, sigBytes []byte) error {
	eimpl, kind, apiVersion, err := unmarshalEntryImpl(bundle.Payload.Body.(string))

	targetImpl, err := reconstructCanonicalizedEntry(ctx, kind, apiVersion, blobBytes, certBytes, sigBytes)
		return fmt.Errorf("recontructing rekorEntry for bundle comparison: %w", err)
	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")
	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)
		return nil, err
	entry, err := types.NewEntry(proposedEntry)
	if err != nil {
		return nil, err
	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
	return types.NewEntry(proposedEntryCan)
// 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)
		return nil, "", "", err
		return nil, "", "", err
	}

	entry, err := types.NewEntry(pe)
	if err != nil {
		return nil, "", "", err
	return entry, pe.Kind(), entry.APIVersion(), nil
}
func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) {
	eimpl, _, _, err := unmarshalEntryImpl(e.Body.(string))
	case *rekord_v001.V001Entry:
	case *hashedrekord_v001.V001Entry:

// 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
}
	"bytes"
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"encoding/hex"
	"fmt"
	"strings"
	"time"
	"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/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"
			gotSig, err := signatures(test.sigRef, "")
	gotSig, err := signatures("", fp)
		envelope     ssldsse.Envelope
			envelope: ssldsse.Envelope{
				Signatures:  []ssldsse.Signature{},

// 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
}
	"errors"
// mClient.Entries = &logEntry
	Entries *models.LogEntry
	if m.Entries != nil {
		return &entries.CreateLogEntryCreated{
			ETag:     "",
			Location: "",
			Payload:  *m.Entries,
		}, nil
	}
	return nil, errors.New("entry not provided")
	if m.Entries != nil {
		return &entries.GetLogEntryByIndexOK{
			Payload: *m.Entries,
		}, nil
	}
	return nil, errors.New("entry not provided")
	if m.Entries != nil {
		return &entries.GetLogEntryByUUIDOK{
			Payload: *m.Entries,
		}, nil
	}
	return nil, errors.New("entry not provided")
	resp := []models.LogEntry{}
	if m.Entries != nil {
		resp = append(resp, *m.Entries)
	}
		Payload: resp,
	"github.com/go-openapi/swag"
	"github.com/sigstore/rekor/pkg/generated/models"

	mClient.Entries = &mock.EntriesClient{
		Entries: &models.LogEntry{"123": models.LogEntryAnon{
			LogIndex: swag.Int64(123),
		}},
	}
		return &VerificationError{"unable to verify SET"}
	"encoding/hex"
	"github.com/sigstore/rekor/pkg/generated/models"
	"github.com/transparency-dev/merkle/rfc6962"
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))
}

	mClient.Entries = &mock.EntriesClient{
		Entries: &data,
	}
		NotBefore:             time.Now().Add(-5 * time.Hour),
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
}