VYPR
Moderate severityNVD Advisory· Published Sep 14, 2022· Updated Apr 22, 2025

Vulnerabilities with blob verification in sigstore cosign

CVE-2022-36056

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/sigstore/cosignGo
< 1.12.01.12.0

Affected products

1

Patches

1
80b79ed8b4d2

Merge pull request from GHSA-8gw7-4j42-w388

https://github.com/sigstore/cosignasraaSep 14, 2022via ghsa
7 files changed · +1452 236
  • cmd/cosign/cli/verify/verify_blob.go+361 169 modified
    @@ -19,7 +19,7 @@ import (
     	"bytes"
     	"context"
     	"crypto"
    -	_ "crypto/sha256" // for `crypto.SHA256`
    +	"crypto/sha256"
     	"crypto/x509"
     	"encoding/base64"
     	"encoding/hex"
    @@ -28,6 +28,7 @@ import (
     	"fmt"
     	"io"
     	"os"
    +	"strings"
     	"time"
     
     	"github.com/go-openapi/runtime"
    @@ -37,6 +38,7 @@ import (
     	"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
     	"github.com/sigstore/cosign/pkg/blob"
     	"github.com/sigstore/cosign/pkg/cosign"
    +	"github.com/sigstore/cosign/pkg/cosign/bundle"
     	"github.com/sigstore/cosign/pkg/cosign/pivkey"
     	"github.com/sigstore/cosign/pkg/cosign/pkcs11key"
     	sigs "github.com/sigstore/cosign/pkg/signature"
    @@ -45,11 +47,15 @@ import (
     	ctypes "github.com/sigstore/cosign/pkg/types"
     	"github.com/sigstore/rekor/pkg/generated/client"
     	"github.com/sigstore/rekor/pkg/generated/models"
    +	"github.com/sigstore/rekor/pkg/pki"
     	"github.com/sigstore/rekor/pkg/types"
    -	hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
    -	rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
    +	"github.com/sigstore/rekor/pkg/types/hashedrekord"
    +	hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
    +	"github.com/sigstore/rekor/pkg/types/intoto"
    +	intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
    +	"github.com/sigstore/rekor/pkg/types/rekord"
    +	rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
     	"github.com/sigstore/sigstore/pkg/cryptoutils"
    -	"github.com/sigstore/sigstore/pkg/signature"
     	"github.com/sigstore/sigstore/pkg/signature/dsse"
     	signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
     )
    @@ -65,14 +71,13 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
     	certGithubWorkflowName,
     	certGithubWorkflowRepository,
     	certGithubWorkflowRef string, enforceSCT bool) error {
    -	var verifier signature.Verifier
     	var cert *x509.Certificate
     
     	if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() && ko.BundlePath == "" {
     		return &options.PubKeyParseError{}
     	}
     
    -	sig, b64sig, err := signatures(sigRef, ko.BundlePath)
    +	sig, err := signatures(sigRef, ko.BundlePath)
     	if err != nil {
     		return err
     	}
    @@ -82,14 +87,44 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
     		return err
     	}
     
    +	co := &cosign.CheckOpts{
    +		CertEmail:                    certEmail,
    +		CertOidcIssuer:               certOidcIssuer,
    +		CertGithubWorkflowTrigger:    certGithubWorkflowTrigger,
    +		CertGithubWorkflowSha:        certGithubWorkflowSha,
    +		CertGithubWorkflowName:       certGithubWorkflowName,
    +		CertGithubWorkflowRepository: certGithubWorkflowRepository,
    +		CertGithubWorkflowRef:        certGithubWorkflowRef,
    +		EnforceSCT:                   enforceSCT,
    +	}
    +	if options.EnableExperimental() {
    +		if ko.RekorURL != "" {
    +			rekorClient, err := rekor.NewClient(ko.RekorURL)
    +			if err != nil {
    +				return fmt.Errorf("creating Rekor client: %w", err)
    +			}
    +			co.RekorClient = rekorClient
    +		}
    +	}
    +	if certRef == "" || options.EnableExperimental() {
    +		co.RootCerts, err = fulcio.GetRoots()
    +		if err != nil {
    +			return fmt.Errorf("getting Fulcio roots: %w", err)
    +		}
    +		co.IntermediateCerts, err = fulcio.GetIntermediates()
    +		if err != nil {
    +			return fmt.Errorf("getting Fulcio intermediates: %w", err)
    +		}
    +	}
    +
     	// Keys are optional!
     	switch {
     	case ko.KeyRef != "":
    -		verifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef)
    +		co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef)
     		if err != nil {
     			return fmt.Errorf("loading public key: %w", err)
     		}
    -		pkcs11Key, ok := verifier.(*pkcs11key.Key)
    +		pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key)
     		if ok {
     			defer pkcs11Key.Close()
     		}
    @@ -99,7 +134,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
     			return fmt.Errorf("opening piv token: %w", err)
     		}
     		defer sk.Close()
    -		verifier, err = sk.Verifier()
    +		co.SigVerifier, err = sk.Verifier()
     		if err != nil {
     			return fmt.Errorf("loading public key from token: %w", err)
     		}
    @@ -108,27 +143,8 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
     		if err != nil {
     			return err
     		}
    -		co := &cosign.CheckOpts{
    -			CertEmail:                    certEmail,
    -			CertOidcIssuer:               certOidcIssuer,
    -			CertGithubWorkflowTrigger:    certGithubWorkflowTrigger,
    -			CertGithubWorkflowSha:        certGithubWorkflowSha,
    -			CertGithubWorkflowName:       certGithubWorkflowName,
    -			CertGithubWorkflowRepository: certGithubWorkflowRepository,
    -			CertGithubWorkflowRef:        certGithubWorkflowRef,
    -			EnforceSCT:                   enforceSCT,
    -		}
     		if certChain == "" {
    -			// If no certChain is passed, the Fulcio root certificate will be used
    -			co.RootCerts, err = fulcio.GetRoots()
    -			if err != nil {
    -				return fmt.Errorf("getting Fulcio roots: %w", err)
    -			}
    -			co.IntermediateCerts, err = fulcio.GetIntermediates()
    -			if err != nil {
    -				return fmt.Errorf("getting Fulcio intermediates: %w", err)
    -			}
    -			verifier, err = cosign.ValidateAndUnpackCert(cert, co)
    +			co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co)
     			if err != nil {
     				return err
     			}
    @@ -138,7 +154,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
     			if err != nil {
     				return err
     			}
    -			verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
    +			co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
     			if err != nil {
     				return err
     			}
    @@ -151,120 +167,223 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail,
     		if b.Cert == "" {
     			return fmt.Errorf("bundle does not contain cert for verification, please provide public key")
     		}
    -		// cert can either be a cert or public key
    +		// b.Cert can either be a certificate or public key
     		certBytes := []byte(b.Cert)
     		if isb64(certBytes) {
     			certBytes, _ = base64.StdEncoding.DecodeString(b.Cert)
     		}
     		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 {
    @@ -273,18 +392,18 @@ func signatures(sigRef string, bundlePath string) (string, string, error) {
     		if err != nil {
     			if !os.IsNotExist(err) {
     				// ignore if file does not exist, it can be a base64 encoded string as well
    -				return "", "", err
    +				return "", err
     			}
     			targetSig = []byte(sigRef)
     		}
     	case bundlePath != "":
     		b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
     		if err != nil {
    -			return "", "", err
    +			return "", err
     		}
     		targetSig = []byte(b.Base64Signature)
     	default:
    -		return "", "", fmt.Errorf("missing flag '--signature'")
    +		return "", fmt.Errorf("missing flag '--signature'")
     	}
     
     	var sig, b64sig string
    @@ -296,7 +415,7 @@ func signatures(sigRef string, bundlePath string) (string, string, error) {
     		sig = string(targetSig)
     		b64sig = base64.StdEncoding.EncodeToString(targetSig)
     	}
    -	return sig, b64sig, nil
    +	return sig, nil
     }
     
     func payloadBytes(blobRef string) ([]byte, error) {
    @@ -313,119 +432,176 @@ func payloadBytes(blobRef string) ([]byte, error) {
     	return blobBytes, nil
     }
     
    -func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error {
    -	// TODO: This can be moved below offline bundle verification when SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY
    -	// is removed.
    -	rekorClient, err := rekor.NewClient(ko.RekorURL)
    +// TODO: RekorClient can be removed when SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY
    +// is removed.
    +func verifyRekorBundle(ctx context.Context, bundlePath string, rekorClient *client.Rekor,
    +	blobBytes []byte, sig string, pubKeyBytes []byte) (*bundle.RekorPayload, error) {
    +	b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
     	if err != nil {
    -		return err
    -	}
    -
    -	// If we have a bundle with a rekor entry, let's first try to verify offline
    -	if ko.BundlePath != "" {
    -		if err := verifyRekorBundle(ctx, ko.BundlePath, cert, rekorClient); err == nil {
    -			fmt.Fprintf(os.Stderr, "tlog entry verified offline\n")
    -			return nil
    -		}
    +		return nil, err
     	}
    -	if !options.EnableExperimental() {
    -		return nil
    +	if b.Bundle == nil {
    +		return nil, fmt.Errorf("rekor entry could not be extracted from local bundle")
     	}
     
    -	// Only fetch from rekor tlog if we don't already have the entry.
    -	if e == nil {
    -		var pubBytes []byte
    -		if pubKey != nil {
    -			pubBytes, err = sigs.PublicKeyPem(pubKey, signatureoptions.WithContext(ctx))
    -			if err != nil {
    -				return err
    -			}
    -		}
    -		if cert != nil {
    -			pubBytes, err = cryptoutils.MarshalCertificateToPEM(cert)
    -			if err != nil {
    -				return err
    -			}
    -		}
    -		e, err = cosign.FindTlogEntry(ctx, rekorClient, b64sig, blobBytes, pubBytes)
    -		if err != nil {
    -			return err
    -		}
    +	if err := verifyBundleMatchesData(ctx, b.Bundle, blobBytes, pubKeyBytes, []byte(sig)); err != nil {
    +		return nil, err
     	}
     
    -	if err := cosign.VerifyTLogEntry(ctx, rekorClient, e); err != nil {
    -		return nil
    +	publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient)
    +	if err != nil {
    +		return nil, fmt.Errorf("retrieving rekor public key: %w", err)
     	}
     
    -	uuid, err := cosign.ComputeLeafHash(e)
    +	pubKey, ok := publicKeys[b.Bundle.Payload.LogID]
    +	if !ok {
    +		return nil, errors.New("rekor log public key not found for payload")
    +	}
    +	err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey)
     	if err != nil {
    -		return err
    +		return nil, err
     	}
    -
    -	fmt.Fprintf(os.Stderr, "tlog entry verified with uuid: %s index: %d\n", hex.EncodeToString(uuid), *e.LogIndex)
    -	if cert == nil {
    -		return nil
    +	if pubKey.Status != tuf.Active {
    +		fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n")
     	}
    -	// if we have a cert, we should check expiry
    -	return cosign.CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0))
    +
    +	return &b.Bundle.Payload, nil
     }
     
    -func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate, rekorClient *client.Rekor) error {
    -	b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
    +func verifyBundleMatchesData(ctx context.Context, bundle *bundle.RekorBundle, blobBytes, certBytes, sigBytes []byte) error {
    +	eimpl, kind, apiVersion, err := unmarshalEntryImpl(bundle.Payload.Body.(string))
     	if err != nil {
     		return err
     	}
    -	if b.Bundle == nil {
    -		return fmt.Errorf("rekor entry is not available")
    -	}
    -	publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient)
    +
    +	targetImpl, err := reconstructCanonicalizedEntry(ctx, kind, apiVersion, blobBytes, certBytes, sigBytes)
     	if err != nil {
    -		return fmt.Errorf("retrieving rekor public key: %w", err)
    +		return fmt.Errorf("recontructing rekorEntry for bundle comparison: %w", err)
     	}
     
    -	pubKey, ok := publicKeys[b.Bundle.Payload.LogID]
    -	if !ok {
    -		return errors.New("rekor log public key not found for payload")
    +	switch e := eimpl.(type) {
    +	case *rekord_v001.V001Entry:
    +		t := targetImpl.(*rekord_v001.V001Entry)
    +		data, err := e.RekordObj.Data.Content.MarshalText()
    +		if err != nil {
    +			return fmt.Errorf("invalid rekord data: %w", err)
    +		}
    +		tData, err := t.RekordObj.Data.Content.MarshalText()
    +		if err != nil {
    +			return fmt.Errorf("invalid rekord data: %w", err)
    +		}
    +		if !bytes.Equal(data, tData) {
    +			return fmt.Errorf("rekord data does not match blob")
    +		}
    +		if err := compareBase64Strings(e.RekordObj.Signature.Content.String(),
    +			t.RekordObj.Signature.Content.String()); err != nil {
    +			return fmt.Errorf("rekord signature does not match bundle %s", err)
    +		}
    +		if err := compareBase64Strings(e.RekordObj.Signature.PublicKey.Content.String(),
    +			t.RekordObj.Signature.PublicKey.Content.String()); err != nil {
    +			return fmt.Errorf("rekord public key does not match bundle")
    +		}
    +	case *hashedrekord_v001.V001Entry:
    +		t := targetImpl.(*hashedrekord_v001.V001Entry)
    +		if *e.HashedRekordObj.Data.Hash.Value != *t.HashedRekordObj.Data.Hash.Value {
    +			return fmt.Errorf("hashedRekord data does not match blob")
    +		}
    +		if err := compareBase64Strings(e.HashedRekordObj.Signature.Content.String(),
    +			t.HashedRekordObj.Signature.Content.String()); err != nil {
    +			return fmt.Errorf("hashedRekord signature does not match bundle %s", err)
    +		}
    +		if err := compareBase64Strings(e.HashedRekordObj.Signature.PublicKey.Content.String(),
    +			t.HashedRekordObj.Signature.PublicKey.Content.String()); err != nil {
    +			return fmt.Errorf("hashedRekord public key does not match bundle")
    +		}
    +	case *intoto_v001.V001Entry:
    +		t := targetImpl.(*intoto_v001.V001Entry)
    +		if *e.IntotoObj.Content.Hash.Value != *t.IntotoObj.Content.Hash.Value {
    +			return fmt.Errorf("intoto content hash does not match attestation")
    +		}
    +		if *e.IntotoObj.Content.PayloadHash.Value != *t.IntotoObj.Content.PayloadHash.Value {
    +			return fmt.Errorf("intoto payload hash does not match attestation")
    +		}
    +		if err := compareBase64Strings(e.IntotoObj.PublicKey.String(),
    +			t.IntotoObj.PublicKey.String()); err != nil {
    +			return fmt.Errorf("intoto public key does not match bundle")
    +		}
    +	default:
    +		return errors.New("unexpected tlog entry type")
     	}
    -	err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey)
    +	return nil
    +}
    +
    +func reconstructCanonicalizedEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) {
    +	props := types.ArtifactProperties{
    +		PublicKeyBytes: certBytes,
    +		PKIFormat:      string(pki.X509),
    +	}
    +	switch kind {
    +	case rekord.KIND:
    +		props.ArtifactBytes = blobBytes
    +		props.SignatureBytes = sigBytes
    +	case hashedrekord.KIND:
    +		blobHash := sha256.Sum256(blobBytes)
    +		props.ArtifactHash = strings.ToLower(hex.EncodeToString(blobHash[:]))
    +		props.SignatureBytes = sigBytes
    +	case intoto.KIND:
    +		props.ArtifactBytes = blobBytes
    +	default:
    +		return nil, fmt.Errorf("unexpected entry kind: %s", kind)
    +	}
    +	proposedEntry, err := types.NewProposedEntry(ctx, kind, apiVersion, props)
     	if err != nil {
    -		return err
    +		return nil, err
     	}
    -	if pubKey.Status != tuf.Active {
    -		fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n")
    +	entry, err := types.NewEntry(proposedEntry)
    +	if err != nil {
    +		return nil, err
     	}
    -
    -	if cert == nil {
    -		return nil
    +	can, err := entry.Canonicalize(ctx)
    +	if err != nil {
    +		return nil, err
    +	}
    +	proposedEntryCan, err := models.UnmarshalProposedEntry(bytes.NewReader(can), runtime.JSONConsumer())
    +	if err != nil {
    +		return nil, err
     	}
    -	it := time.Unix(b.Bundle.Payload.IntegratedTime, 0)
    -	return cosign.CheckExpiry(cert, it)
    +	return types.NewEntry(proposedEntryCan)
     }
     
    -func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) {
    -	b, err := base64.StdEncoding.DecodeString(e.Body.(string))
    +// unmarshalEntryImpl decodes the base64-encoded entry to a specific entry type (types.EntryImpl).
    +func unmarshalEntryImpl(e string) (types.EntryImpl, string, string, error) {
    +	b, err := base64.StdEncoding.DecodeString(e)
     	if err != nil {
    -		return nil, err
    +		return nil, "", "", err
     	}
     
     	pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer())
     	if err != nil {
    -		return nil, err
    +		return nil, "", "", err
    +	}
    +
    +	entry, err := types.NewEntry(pe)
    +	if err != nil {
    +		return nil, "", "", err
     	}
    +	return entry, pe.Kind(), entry.APIVersion(), nil
    +}
     
    -	eimpl, err := types.NewEntry(pe)
    +func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) {
    +	eimpl, _, _, err := unmarshalEntryImpl(e.Body.(string))
     	if err != nil {
     		return nil, err
     	}
     
     	var publicKeyB64 []byte
     	switch e := eimpl.(type) {
    -	case *rekord.V001Entry:
    +	case *rekord_v001.V001Entry:
     		publicKeyB64, err = e.RekordObj.Signature.PublicKey.Content.MarshalText()
     		if err != nil {
     			return nil, err
     		}
    -	case *hashedrekord.V001Entry:
    +	case *hashedrekord_v001.V001Entry:
     		publicKeyB64, err = e.HashedRekordObj.Signature.PublicKey.Content.MarshalText()
     		if err != nil {
     			return nil, err
    @@ -463,3 +639,19 @@ func isIntotoDSSE(blobBytes []byte) bool {
     
     	return true
     }
    +
    +// TODO: Use this function to compare bundle signatures in OCI.
    +func compareBase64Strings(got string, expected string) error {
    +	decodeFirst, err := base64.StdEncoding.DecodeString(got)
    +	if err != nil {
    +		return fmt.Errorf("decoding base64 string %s", got)
    +	}
    +	decodeSecond, err := base64.StdEncoding.DecodeString(expected)
    +	if err != nil {
    +		return fmt.Errorf("decoding base64 string %s", expected)
    +	}
    +	if !bytes.Equal(decodeFirst, decodeSecond) {
    +		return fmt.Errorf("comparing base64 strings, expected %s, got %s", expected, got)
    +	}
    +	return nil
    +}
    
  • cmd/cosign/cli/verify/verify_blob_test.go+985 12 modified
    @@ -15,14 +15,46 @@
     package verify
     
     import (
    +	"bytes"
    +	"context"
    +	"crypto"
    +	"crypto/ecdsa"
    +	"crypto/elliptic"
    +	"crypto/rand"
    +	"crypto/sha256"
    +	"crypto/x509"
     	"encoding/base64"
    +	"encoding/hex"
     	"encoding/json"
    +	"fmt"
     	"os"
     	"path/filepath"
    +	"strings"
     	"testing"
    +	"time"
     
    -	"github.com/secure-systems-lab/go-securesystemslib/dsse"
    +	"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
    +	"github.com/go-openapi/swag"
    +	ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse"
    +	"github.com/sigstore/cosign/cmd/cosign/cli/options"
    +	"github.com/sigstore/cosign/internal/pkg/cosign/rekor/mock"
     	"github.com/sigstore/cosign/pkg/cosign"
    +	"github.com/sigstore/cosign/pkg/cosign/bundle"
    +	sigs "github.com/sigstore/cosign/pkg/signature"
    +	ctypes "github.com/sigstore/cosign/pkg/types"
    +	"github.com/sigstore/cosign/test"
    +	"github.com/sigstore/rekor/pkg/generated/client"
    +	"github.com/sigstore/rekor/pkg/generated/models"
    +	"github.com/sigstore/rekor/pkg/pki"
    +	"github.com/sigstore/rekor/pkg/types"
    +	"github.com/sigstore/rekor/pkg/types/hashedrekord"
    +	hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
    +	"github.com/sigstore/rekor/pkg/types/intoto"
    +	"github.com/sigstore/rekor/pkg/types/rekord"
    +	"github.com/sigstore/sigstore/pkg/cryptoutils"
    +	"github.com/sigstore/sigstore/pkg/signature"
    +	"github.com/sigstore/sigstore/pkg/signature/dsse"
    +	signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
     )
     
     func TestSignaturesRef(t *testing.T) {
    @@ -48,7 +80,7 @@ func TestSignaturesRef(t *testing.T) {
     
     	for _, test := range tests {
     		t.Run(test.description, func(t *testing.T) {
    -			gotSig, gotb64Sig, err := signatures(test.sigRef, "")
    +			gotSig, err := signatures(test.sigRef, "")
     			if test.shouldErr && err != nil {
     				return
     			}
    @@ -58,9 +90,6 @@ func TestSignaturesRef(t *testing.T) {
     			if gotSig != sig {
     				t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig)
     			}
    -			if gotb64Sig != b64sig {
    -				t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig)
    -			}
     		})
     	}
     }
    @@ -84,28 +113,25 @@ func TestSignaturesBundle(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	gotSig, gotb64Sig, err := signatures("", fp)
    +	gotSig, err := signatures("", fp)
     	if err != nil {
     		t.Fatal(err)
     	}
     	if gotSig != sig {
     		t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig)
     	}
    -	if gotb64Sig != b64sig {
    -		t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig)
    -	}
     }
     
     func TestIsIntotoDSSEWithEnvelopes(t *testing.T) {
     	tts := []struct {
    -		envelope     dsse.Envelope
    +		envelope     ssldsse.Envelope
     		isIntotoDSSE bool
     	}{
     		{
    -			envelope: dsse.Envelope{
    +			envelope: ssldsse.Envelope{
     				PayloadType: "application/vnd.in-toto+json",
     				Payload:     base64.StdEncoding.EncodeToString([]byte("This is a test")),
    -				Signatures:  []dsse.Signature{},
    +				Signatures:  []ssldsse.Signature{},
     			},
     			isIntotoDSSE: true,
     		},
    @@ -141,3 +167,950 @@ func TestIsIntotoDSSEWithBytes(t *testing.T) {
     		}
     	}
     }
    +
    +// Does not test identity options, only blob verification with different
    +// options.
    +func TestVerifyBlob(t *testing.T) {
    +	ctx := context.Background()
    +	td := t.TempDir()
    +
    +	leafPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	signer, err := signature.LoadECDSASignerVerifier(leafPriv, crypto.SHA256)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	pubKeyBytes, err := sigs.PublicKeyPem(signer, signatureoptions.WithContext(ctx))
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	// Generate expired and unexpired certificates
    +	identity := "hello@foo.com"
    +	issuer := "issuer"
    +	rootCert, rootPriv, _ := test.GenerateRootCa()
    +	rootPool := x509.NewCertPool()
    +	rootPool.AddCert(rootCert)
    +	unexpiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer,
    +		time.Now(), leafPriv, rootCert, rootPriv)
    +	unexpiredCertPem, _ := cryptoutils.MarshalCertificateToPEM(unexpiredLeafCert)
    +
    +	expiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer,
    +		time.Now().Add(-time.Hour), leafPriv, rootCert, rootPriv)
    +	expiredLeafPem, _ := cryptoutils.MarshalCertificateToPEM(expiredLeafCert)
    +
    +	// Make rekor signer
    +	rekorPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	rekorSigner, err := signature.LoadECDSASignerVerifier(rekorPriv, crypto.SHA256)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	pemRekor, err := cryptoutils.MarshalPublicKeyToPEM(rekorSigner.Public())
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	tmpRekorPubFile, err := os.CreateTemp(td, "cosign_rekor_pub_*.key")
    +	if err != nil {
    +		t.Fatalf("failed to create temp rekor pub file: %v", err)
    +	}
    +	defer tmpRekorPubFile.Close()
    +	if _, err := tmpRekorPubFile.Write(pemRekor); err != nil {
    +		t.Fatalf("failed to write rekor pub file: %v", err)
    +	}
    +	t.Setenv("SIGSTORE_REKOR_PUBLIC_KEY", tmpRekorPubFile.Name())
    +
    +	var makeSignature = func(blob []byte) string {
    +		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +		return string(sig)
    +	}
    +	blobBytes := []byte("foo")
    +	blobSignature := makeSignature(blobBytes)
    +
    +	otherBytes := []byte("bar")
    +	otherSignature := makeSignature(otherBytes)
    +
    +	tts := []struct {
    +		name        string
    +		blob        []byte
    +		signature   string
    +		sigVerifier signature.Verifier
    +		cert        *x509.Certificate
    +		bundlePath  string
    +		// If online lookups to Rekor are enabled
    +		experimental bool
    +		// The rekor entry response when Rekor is enabled
    +		rekorEntry *models.LogEntry
    +		shouldErr  bool
    +	}{
    +		{
    +			name:         "valid signature with public key",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			shouldErr:    false,
    +		},
    +		{
    +			name:         "valid signature with public key - experimental no rekor fail",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: true,
    +			rekorEntry:   nil,
    +			shouldErr:    true,
    +		},
    +		{
    +			name:         "valid signature with public key - experimental rekor entry success",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: true,
    +			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				pubKeyBytes, true),
    +			shouldErr: false,
    +		},
    +		{
    +			name:         "valid signature with public key - good bundle provided",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				pubKeyBytes, true),
    +			shouldErr: false,
    +		},
    +		{
    +			name:         "valid signature with public key - bad bundle SET",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature),
    +				unexpiredCertPem, true),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "valid signature with public key - bad bundle cert mismatch",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				unexpiredCertPem, true),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "valid signature with public key - bad bundle signature mismatch",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)),
    +				pubKeyBytes, true),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "valid signature with public key - bad bundle msg & signature mismatch",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature),
    +				pubKeyBytes, true),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "invalid signature with public key",
    +			blob:         blobBytes,
    +			signature:    otherSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			shouldErr:    true,
    +		},
    +		{
    +			name:         "invalid signature with public key - experimental",
    +			blob:         blobBytes,
    +			signature:    otherSignature,
    +			sigVerifier:  signer,
    +			experimental: true,
    +			shouldErr:    true,
    +		},
    +		{
    +			name:         "valid signature with unexpired certificate",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			cert:         unexpiredLeafCert,
    +			experimental: false,
    +			shouldErr:    false,
    +		},
    +		{
    +			name:         "valid signature with unexpired certificate - bad bundle cert mismatch",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			cert:         unexpiredLeafCert,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				pubKeyBytes, true),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "valid signature with unexpired certificate - bad bundle signature mismatch",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			cert:         unexpiredLeafCert,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)),
    +				unexpiredCertPem, true),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "valid signature with unexpired certificate - bad bundle msg & signature mismatch",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			cert:         unexpiredLeafCert,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature),
    +				unexpiredCertPem, true),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "invalid signature with unexpired certificate",
    +			blob:         blobBytes,
    +			signature:    otherSignature,
    +			sigVerifier:  signer,
    +			cert:         unexpiredLeafCert,
    +			experimental: false,
    +			shouldErr:    true,
    +		},
    +		{
    +			name:         "valid signature with unexpired certificate - experimental",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			cert:         unexpiredLeafCert,
    +			sigVerifier:  signer,
    +			experimental: true,
    +			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				unexpiredCertPem, true),
    +			shouldErr: false,
    +		},
    +
    +		{
    +			name:         "valid signature with unexpired certificate - experimental & rekor entry found",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			cert:         unexpiredLeafCert,
    +			experimental: true,
    +			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				unexpiredCertPem, true),
    +			shouldErr: false,
    +		},
    +		{
    +			name:         "valid signature with expired certificate",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			cert:         expiredLeafCert,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			shouldErr:    true,
    +		},
    +
    +		{
    +			name:         "valid signature with expired certificate - experimental good rekor lookup",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			cert:         expiredLeafCert,
    +			experimental: true,
    +			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				expiredLeafPem, true),
    +			shouldErr: false,
    +		},
    +
    +		{
    +			name:         "valid signature with expired certificate - experimental bad rekor integrated time",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			cert:         expiredLeafCert,
    +			sigVerifier:  signer,
    +			experimental: true,
    +			rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				expiredLeafPem, false),
    +			shouldErr: true,
    +		},
    +
    +		{
    +			name:         "valid signature with unexpired certificate - good bundle, nonexperimental",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			cert:         unexpiredLeafCert,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				unexpiredCertPem, true),
    +			shouldErr: false,
    +		},
    +		{
    +			name:         "valid signature with expired certificate - good bundle, nonexperimental",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			cert:         expiredLeafCert,
    +			sigVerifier:  signer,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				expiredLeafPem, true),
    +			shouldErr: false,
    +		},
    +		{
    +			name:         "valid signature with expired certificate - bundle with bad expiration",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			cert:         expiredLeafCert,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				expiredLeafPem, false),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "valid signature with expired certificate - bundle with bad SET",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			cert:         expiredLeafCert,
    +			experimental: false,
    +			bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature),
    +				expiredLeafPem, true),
    +			shouldErr: true,
    +		},
    +		{
    +			name:         "valid signature with expired certificate - experimental good bundle",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			cert:         expiredLeafCert,
    +			experimental: true,
    +			bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature),
    +				expiredLeafPem, true),
    +			shouldErr: false,
    +		},
    +		{
    +			name:         "valid signature with expired certificate - experimental bad rekor entry",
    +			blob:         blobBytes,
    +			signature:    blobSignature,
    +			sigVerifier:  signer,
    +			cert:         expiredLeafCert,
    +			experimental: true,
    +			// This is the wrong signer for the SET!
    +			rekorEntry: makeRekorEntry(t, *signer, blobBytes, []byte(blobSignature),
    +				expiredLeafPem, true),
    +			shouldErr: true,
    +		},
    +	}
    +	for _, tt := range tts {
    +		t.Run(tt.name, func(t *testing.T) {
    +			tt := tt
    +			var mClient client.Rekor
    +			mClient.Entries = &mock.EntriesClient{Entries: tt.rekorEntry}
    +			co := &cosign.CheckOpts{
    +				SigVerifier: tt.sigVerifier,
    +				RootCerts:   rootPool,
    +			}
    +			// if expermental is enabled, add RekorClient to co.
    +			if tt.experimental {
    +				co.RekorClient = &mClient
    +			}
    +
    +			err := verifyBlob(ctx, co, tt.blob, tt.signature, tt.cert, tt.bundlePath, nil)
    +			if (err != nil) != tt.shouldErr {
    +				t.Fatalf("verifyBlob()= %s, expected shouldErr=%t ", err, tt.shouldErr)
    +			}
    +		})
    +	}
    +}
    +
    +func makeRekorEntry(t *testing.T, rekorSigner signature.ECDSASignerVerifier,
    +	pyld, sig, svBytes []byte, expiryValid bool) *models.LogEntry {
    +	ctx := context.Background()
    +	// Calculate log ID, the digest of the Rekor public key
    +	logID, err := getLogID(rekorSigner.Public())
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	hashedrekord := &hashedrekord_v001.V001Entry{}
    +	h := sha256.Sum256(pyld)
    +	pe, err := hashedrekord.CreateFromArtifactProperties(ctx, types.ArtifactProperties{
    +		ArtifactHash:   hex.EncodeToString(h[:]),
    +		SignatureBytes: sig,
    +		PublicKeyBytes: svBytes,
    +		PKIFormat:      "x509",
    +	})
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	entry, err := types.NewEntry(pe)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	leaf, err := entry.Canonicalize(ctx)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	integratedTime := time.Now()
    +	certs, _ := cryptoutils.UnmarshalCertificatesFromPEM(svBytes)
    +	if certs != nil && len(certs) > 0 {
    +		if expiryValid {
    +			integratedTime = certs[0].NotAfter.Add(-time.Second)
    +		} else {
    +			integratedTime = certs[0].NotAfter.Add(time.Second)
    +		}
    +	}
    +	e := models.LogEntryAnon{
    +		Body:           base64.StdEncoding.EncodeToString(leaf),
    +		IntegratedTime: swag.Int64(integratedTime.Unix()),
    +		LogIndex:       swag.Int64(0),
    +		LogID:          swag.String(logID),
    +	}
    +	// Marshal payload, sign, and set SET in Bundle
    +	jsonPayload, err := json.Marshal(e)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	canonicalized, err := jsoncanonicalizer.Transform(jsonPayload)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	bundleSig, err := rekorSigner.SignMessage(bytes.NewReader(canonicalized))
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	uuid, _ := cosign.ComputeLeafHash(&e)
    +
    +	e.Verification = &models.LogEntryAnonVerification{
    +		SignedEntryTimestamp: bundleSig,
    +		InclusionProof: &models.InclusionProof{
    +			LogIndex: swag.Int64(0),
    +			TreeSize: swag.Int64(1),
    +			RootHash: swag.String(hex.EncodeToString(uuid)),
    +			Hashes:   []string{},
    +		},
    +	}
    +	return &models.LogEntry{hex.EncodeToString(uuid): e}
    +}
    +
    +func makeLocalBundle(t *testing.T, rekorSigner signature.ECDSASignerVerifier,
    +	pyld []byte, sig []byte, svBytes []byte, expiryValid bool) string {
    +	td := t.TempDir()
    +
    +	// Create bundle.
    +	entry := makeRekorEntry(t, rekorSigner, pyld, sig, svBytes, expiryValid)
    +	var e models.LogEntryAnon
    +	for _, v := range *entry {
    +		e = v
    +	}
    +	b := cosign.LocalSignedPayload{
    +		Base64Signature: base64.StdEncoding.EncodeToString(sig),
    +		Cert:            string(svBytes),
    +		Bundle: &bundle.RekorBundle{
    +			Payload: bundle.RekorPayload{
    +				Body:           e.Body,
    +				IntegratedTime: *e.IntegratedTime,
    +				LogIndex:       *e.LogIndex,
    +				LogID:          *e.LogID,
    +			},
    +			SignedEntryTimestamp: e.Verification.SignedEntryTimestamp,
    +		},
    +	}
    +
    +	// Write bundle to disk
    +	jsonBundle, err := json.Marshal(b)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	bundlePath := filepath.Join(td, "bundle.sig")
    +	if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil {
    +		t.Fatal(err)
    +	}
    +	return bundlePath
    +}
    +
    +func TestVerifyBlobCmdWithBundle(t *testing.T) {
    +	keyless := newKeylessStack(t)
    +
    +	t.Run("Normal verification", func(t *testing.T) {
    +		identity := "hello@foo.com"
    +		issuer := "issuer"
    +		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
    +
    +		// Create blob
    +		blob := "someblob"
    +
    +		// Sign blob with private key
    +		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		// Create bundle
    +		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
    +		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
    +		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
    +		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
    +		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
    +
    +		// Verify command
    +		err = VerifyBlobCmd(context.Background(),
    +			options.KeyOpts{BundlePath: bundlePath},
    +			"",       /*certRef*/ // Cert is fetched from bundle
    +			identity, /*certEmail*/
    +			issuer,   /*certOidcIssuer*/
    +			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
    +			"",       /*sigRef*/    // Sig is fetched from bundle
    +			blobPath, /*blobRef*/
    +			// GitHub identity flags start
    +			"", "", "", "", "",
    +			// GitHub identity flags end
    +			false /*enforceSCT*/)
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +	})
    +	t.Run("Mismatched cert/sig", func(t *testing.T) {
    +		// This test ensures that the signature and cert at the top level in the LocalSignedPayload must be identical to the ones in the RekorBundle.
    +		identity := "hello@foo.com"
    +		issuer := "issuer"
    +		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
    +		_, _, leafPemCert2, signer2 := keyless.genLeafCert(t, identity, issuer)
    +
    +		// Create blob
    +		blob := "someblob"
    +
    +		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		sig2, err := signer2.SignMessage(bytes.NewReader([]byte(blob)))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		// Create bundle
    +		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert2, sig2)
    +		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
    +		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
    +		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
    +		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
    +
    +		// Verify command
    +		err = VerifyBlobCmd(context.Background(),
    +			options.KeyOpts{BundlePath: bundlePath},
    +			"",       /*certRef*/ // Cert is fetched from bundle
    +			"",       /*certEmail*/
    +			"",       /*certOidcIssuer*/
    +			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
    +			"",       /*sigRef*/    // Sig is fetched from bundle
    +			blobPath, /*blobRef*/
    +			// GitHub identity flags start
    +			"", "", "", "", "",
    +			// GitHub identity flags end
    +			false /*enforceSCT*/)
    +		if err == nil {
    +			t.Fatal("expecting err due to mismatched signatures, got nil")
    +		}
    +	})
    +	t.Run("Expired cert", func(t *testing.T) {
    +		identity := "hello@foo.com"
    +		issuer := "issuer"
    +		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
    +
    +		// Create blob
    +		blob := "someblob"
    +
    +		// Sign blob with private key
    +		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		// Create bundle
    +		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
    +		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()-1, entry)
    +		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
    +		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
    +		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
    +
    +		// Verify command
    +		err = VerifyBlobCmd(context.Background(),
    +			options.KeyOpts{BundlePath: bundlePath},
    +			"",       /*certRef*/ // Cert is fetched from bundle
    +			"",       /*certEmail*/
    +			"",       /*certOidcIssuer*/
    +			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
    +			"",       /*sigRef*/    // Sig is fetched from bundle
    +			blobPath, /*blobRef*/
    +			// GitHub identity flags start
    +			"", "", "", "", "",
    +			// GitHub identity flags end
    +			false /*enforceSCT*/)
    +		if err == nil {
    +			t.Fatal("expected error due to expired cert, received nil")
    +		}
    +	})
    +	t.Run("Attestation", func(t *testing.T) {
    +		identity := "hello@foo.com"
    +		issuer := "issuer"
    +		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
    +
    +		stmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}`
    +		wrapped := dsse.WrapSigner(signer, ctypes.IntotoPayloadType)
    +		signedPayload, err := wrapped.SignMessage(bytes.NewReader([]byte(stmt)), signatureoptions.WithContext(context.Background()))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +		// intoto sig = json-serialized dsse envelope
    +		sig := signedPayload
    +
    +		// Create bundle
    +		entry := genRekorEntry(t, intoto.KIND, intoto.New().DefaultVersion(), signedPayload, leafPemCert, sig)
    +		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
    +		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
    +		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
    +		blobPath := writeBlobFile(t, keyless.td, string(signedPayload), "attestation.txt")
    +
    +		// Verify command
    +		err = VerifyBlobCmd(context.Background(),
    +			options.KeyOpts{BundlePath: bundlePath},
    +			"",       /*certRef*/ // Cert is fetched from bundle
    +			"",       /*certEmail*/
    +			"",       /*certOidcIssuer*/
    +			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
    +			"",       /*sigRef*/    // Sig is fetched from bundle
    +			blobPath, /*blobRef*/
    +			// GitHub identity flags start
    +			"", "", "", "", "",
    +			// GitHub identity flags end
    +			false /*enforceSCT*/)
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +	})
    +	t.Run("Invalid blob signature", func(t *testing.T) {
    +		identity := "hello@foo.com"
    +		issuer := "issuer"
    +		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
    +
    +		// Create blob
    +		blob := "someblob"
    +
    +		// Sign blob with private key
    +		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		// Create bundle
    +		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
    +		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
    +		b.Bundle.SignedEntryTimestamp = []byte{'i', 'n', 'v', 'a', 'l', 'i', 'd'}
    +		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
    +		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
    +
    +		// Verify command
    +		err = VerifyBlobCmd(context.Background(),
    +			options.KeyOpts{BundlePath: bundlePath},
    +			"",       /*certRef*/ // Cert is fetched from bundle
    +			"",       /*certEmail*/
    +			"",       /*certOidcIssuer*/
    +			"",       /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
    +			"",       /*sigRef*/    // Sig is fetched from bundle
    +			blobPath, /*blobRef*/
    +			// GitHub identity flags start
    +			"", "", "", "", "",
    +			// GitHub identity flags end
    +			false /*enforceSCT*/)
    +		if err == nil || !strings.Contains(err.Error(), "unable to verify SET") {
    +			t.Fatalf("expected error verifying SET, got %v", err)
    +		}
    +	})
    +	t.Run("Mismatched certificate email", func(t *testing.T) {
    +		identity := "hello@foo.com"
    +		issuer := "issuer"
    +		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
    +
    +		// Create blob
    +		blob := "someblob"
    +
    +		// Sign blob with private key
    +		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		// Create bundle
    +		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
    +		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
    +		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
    +		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
    +		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
    +
    +		// Verify command
    +		err = VerifyBlobCmd(context.Background(),
    +			options.KeyOpts{BundlePath: bundlePath},
    +			"",                    /*certRef*/ // Cert is fetched from bundle
    +			"invalid@example.com", /*certEmail*/
    +			issuer,                /*certOidcIssuer*/
    +			"",                    /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
    +			"",                    /*sigRef*/    // Sig is fetched from bundle
    +			blobPath,              /*blobRef*/
    +			// GitHub identity flags start
    +			"", "", "", "", "",
    +			// GitHub identity flags end
    +			false /*enforceSCT*/)
    +		if err == nil || !strings.Contains(err.Error(), "expected email not found in certificate") {
    +			t.Fatalf("expected error with mismatched identity, got %v", err)
    +		}
    +	})
    +	t.Run("Mismatched certificate issuer", func(t *testing.T) {
    +		identity := "hello@foo.com"
    +		issuer := "issuer"
    +		leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer)
    +
    +		// Create blob
    +		blob := "someblob"
    +
    +		// Sign blob with private key
    +		sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		// Create bundle
    +		entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
    +		b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
    +		b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
    +		bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
    +		blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
    +
    +		// Verify command
    +		err = VerifyBlobCmd(context.Background(),
    +			options.KeyOpts{BundlePath: bundlePath},
    +			"",        /*certRef*/ // Cert is fetched from bundle
    +			identity,  /*certEmail*/
    +			"invalid", /*certOidcIssuer*/
    +			"",        /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE
    +			"",        /*sigRef*/    // Sig is fetched from bundle
    +			blobPath,  /*blobRef*/
    +			// GitHub identity flags start
    +			"", "", "", "", "",
    +			// GitHub identity flags end
    +			false /*enforceSCT*/)
    +		if err == nil || !strings.Contains(err.Error(), "expected oidc issuer not found in certificate") {
    +			t.Fatalf("expected error with mismatched issuer, got %v", err)
    +		}
    +	})
    +}
    +
    +type keylessStack struct {
    +	rootCert    *x509.Certificate
    +	rootPriv    *ecdsa.PrivateKey
    +	rootPemCert []byte
    +	subCert     *x509.Certificate
    +	subPriv     *ecdsa.PrivateKey
    +	subPemCert  []byte
    +	rekorSigner *signature.ECDSASignerVerifier
    +	rekorLogID  string
    +	td          string // temporary directory
    +}
    +
    +func newKeylessStack(t *testing.T) *keylessStack {
    +	stack := &keylessStack{td: t.TempDir()}
    +	stack.rootCert, stack.rootPriv, _ = test.GenerateRootCa()
    +	stack.rootPemCert, _ = cryptoutils.MarshalCertificateToPEM(stack.rootCert)
    +	stack.subCert, stack.subPriv, _ = test.GenerateSubordinateCa(stack.rootCert, stack.rootPriv)
    +	stack.subPemCert, _ = cryptoutils.MarshalCertificateToPEM(stack.subCert)
    +
    +	stack.genChainFile(t)
    +	stack.genRekor(t)
    +	return stack
    +}
    +
    +func (s *keylessStack) genLeafCert(t *testing.T, subject string, issuer string) (*x509.Certificate, *ecdsa.PrivateKey, []byte, *signature.ECDSASignerVerifier) {
    +	cert, priv, _ := test.GenerateLeafCert(subject, issuer, s.subCert, s.subPriv)
    +	pemCert, _ := cryptoutils.MarshalCertificateToPEM(cert)
    +	signer, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	return cert, priv, pemCert, signer
    +}
    +
    +func (s *keylessStack) genChainFile(t *testing.T) {
    +	var chain []byte
    +	chain = append(chain, s.subPemCert...)
    +	chain = append(chain, s.rootPemCert...)
    +	tmpChainFile, err := os.CreateTemp(s.td, "cosign_fulcio_chain_*.cert")
    +	if err != nil {
    +		t.Fatalf("failed to create temp chain file: %v", err)
    +	}
    +	defer tmpChainFile.Close()
    +	if _, err := tmpChainFile.Write(chain); err != nil {
    +		t.Fatalf("failed to write chain file: %v", err)
    +	}
    +	// Override for Fulcio root so it doesn't use TUF
    +	t.Setenv("SIGSTORE_ROOT_FILE", tmpChainFile.Name())
    +}
    +
    +func (s *keylessStack) genRekor(t *testing.T) {
    +	// Create Rekor private key and write to disk
    +	rekorPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	s.rekorSigner, err = signature.LoadECDSASignerVerifier(rekorPriv, crypto.SHA256)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	rekorPub := s.rekorSigner.Public()
    +	pemRekor, err := cryptoutils.MarshalPublicKeyToPEM(rekorPub)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	tmpRekorPubFile, err := os.CreateTemp(s.td, "cosign_rekor_pub_*.key")
    +	if err != nil {
    +		t.Fatalf("failed to create temp rekor pub file: %v", err)
    +	}
    +	defer tmpRekorPubFile.Close()
    +	if _, err := tmpRekorPubFile.Write(pemRekor); err != nil {
    +		t.Fatalf("failed to write rekor pub file: %v", err)
    +	}
    +
    +	// Calculate log ID, the digest of the Rekor public key
    +	s.rekorLogID, err = getLogID(rekorPub)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	// Override for Rekor public key so it doesn't use TUF
    +	t.Setenv("SIGSTORE_REKOR_PUBLIC_KEY", tmpRekorPubFile.Name())
    +}
    +func (s *keylessStack) rekorSignPayload(t *testing.T, payload bundle.RekorPayload) []byte {
    +	// Marshal payload, sign, and return SET
    +	jsonPayload, err := json.Marshal(payload)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	canonicalized, err := jsoncanonicalizer.Transform(jsonPayload)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	bundleSig, err := s.rekorSigner.SignMessage(bytes.NewReader(canonicalized))
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	return bundleSig
    +}
    +
    +// getLogID calculates the digest of a PKIX-encoded public key
    +func getLogID(pub crypto.PublicKey) (string, error) {
    +	pubBytes, err := x509.MarshalPKIXPublicKey(pub)
    +	if err != nil {
    +		return "", err
    +	}
    +	digest := sha256.Sum256(pubBytes)
    +	return hex.EncodeToString(digest[:]), nil
    +}
    +
    +func genRekorEntry(t *testing.T, kind, version string, artifact []byte, cert []byte, sig []byte) string {
    +	// Generate the Rekor Entry
    +	entryImpl, err := createEntry(context.Background(), kind, version, artifact, cert, sig)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	entryBytes, err := entryImpl.Canonicalize(context.Background())
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	return base64.StdEncoding.EncodeToString(entryBytes)
    +}
    +
    +func createBundle(t *testing.T, sig []byte, certPem []byte, logID string, integratedTime int64, rekorEntry string) *cosign.LocalSignedPayload {
    +	// Create bundle with:
    +	// * Blob signature
    +	// * Signing certificate
    +	// * Bundle with a payload and signature over the payload
    +	b := &cosign.LocalSignedPayload{
    +		Base64Signature: base64.StdEncoding.EncodeToString(sig),
    +		Cert:            string(certPem),
    +		Bundle: &bundle.RekorBundle{
    +			SignedEntryTimestamp: []byte{},
    +			Payload: bundle.RekorPayload{
    +				LogID:          logID,
    +				IntegratedTime: integratedTime,
    +				LogIndex:       1,
    +				Body:           rekorEntry,
    +			},
    +		},
    +	}
    +
    +	return b
    +}
    +
    +func createEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) {
    +	props := types.ArtifactProperties{
    +		PublicKeyBytes: certBytes,
    +		PKIFormat:      string(pki.X509),
    +	}
    +	switch kind {
    +	case rekord.KIND:
    +		props.ArtifactBytes = blobBytes
    +		props.SignatureBytes = sigBytes
    +	case hashedrekord.KIND:
    +		blobHash := sha256.Sum256(blobBytes)
    +		props.ArtifactHash = strings.ToLower(hex.EncodeToString(blobHash[:]))
    +		props.SignatureBytes = sigBytes
    +	case intoto.KIND:
    +		props.ArtifactBytes = blobBytes
    +	default:
    +		return nil, fmt.Errorf("unexpected entry kind: %s", kind)
    +	}
    +	proposedEntry, err := types.NewProposedEntry(ctx, kind, apiVersion, props)
    +	if err != nil {
    +		return nil, err
    +	}
    +	return types.NewEntry(proposedEntry)
    +}
    +
    +func writeBundleFile(t *testing.T, td string, b *cosign.LocalSignedPayload, name string) string {
    +	// Write bundle to disk
    +	jsonBundle, err := json.Marshal(b)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	bundlePath := filepath.Join(td, name)
    +	if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil {
    +		t.Fatal(err)
    +	}
    +	return bundlePath
    +}
    +
    +func writeBlobFile(t *testing.T, td string, blob string, name string) string {
    +	// Write blob to disk
    +	blobPath := filepath.Join(td, name)
    +	if err := os.WriteFile(blobPath, []byte(blob), 0644); err != nil {
    +		t.Fatal(err)
    +	}
    +	return blobPath
    +}
    
  • internal/pkg/cosign/rekor/mock/mock_rekor_client.go+28 51 modified
    @@ -15,81 +15,58 @@
     package mock
     
     import (
    -	"encoding/base64"
    -	"encoding/hex"
    +	"errors"
     
     	"github.com/go-openapi/runtime"
    -	"github.com/transparency-dev/merkle/rfc6962"
     
     	"github.com/sigstore/rekor/pkg/generated/client/entries"
     	"github.com/sigstore/rekor/pkg/generated/models"
     )
     
    -var (
    -	lea = models.LogEntryAnon{
    -		Attestation:    &models.LogEntryAnonAttestation{},
    -		Body:           base64.StdEncoding.EncodeToString([]byte("asdf")),
    -		IntegratedTime: new(int64),
    -		LogID:          new(string),
    -		LogIndex:       new(int64),
    -		Verification: &models.LogEntryAnonVerification{
    -			InclusionProof: &models.InclusionProof{
    -				RootHash: new(string),
    -				TreeSize: new(int64),
    -				LogIndex: new(int64),
    -			},
    -		},
    -	}
    -	data = models.LogEntry{
    -		uuid(lea): lea,
    -	}
    -)
    -
    -// uuid generates the UUID for the given LogEntry.
    -// This is effectively a reimplementation of
    -// pkg/cosign/tlog.go -> verifyUUID / ComputeLeafHash, but separated
    -// to avoid a circular dependency.
    -// TODO?: Perhaps we should refactor the tlog libraries into a separate
    -// package?
    -func uuid(e models.LogEntryAnon) string {
    -	entryBytes, err := base64.StdEncoding.DecodeString(e.Body.(string))
    -	if err != nil {
    -		panic(err)
    -	}
    -	return hex.EncodeToString(rfc6962.DefaultHasher.HashLeaf(entryBytes))
    -}
    -
     // EntriesClient is a client that implements entries.ClientService for Rekor
     // To use:
     // var mClient client.Rekor
    -// mClient.entries = &EntriesClient{}
    +// mClient.Entries = &logEntry
     type EntriesClient struct {
    -	Entries models.LogEntry
    +	Entries *models.LogEntry
     }
     
     func (m *EntriesClient) CreateLogEntry(params *entries.CreateLogEntryParams, opts ...entries.ClientOption) (*entries.CreateLogEntryCreated, error) {
    -	return &entries.CreateLogEntryCreated{
    -		ETag:     "",
    -		Location: "",
    -		Payload:  data,
    -	}, nil
    +	if m.Entries != nil {
    +		return &entries.CreateLogEntryCreated{
    +			ETag:     "",
    +			Location: "",
    +			Payload:  *m.Entries,
    +		}, nil
    +	}
    +	return nil, errors.New("entry not provided")
     }
     
     func (m *EntriesClient) GetLogEntryByIndex(params *entries.GetLogEntryByIndexParams, opts ...entries.ClientOption) (*entries.GetLogEntryByIndexOK, error) {
    -	return &entries.GetLogEntryByIndexOK{
    -		Payload: data,
    -	}, nil
    +	if m.Entries != nil {
    +		return &entries.GetLogEntryByIndexOK{
    +			Payload: *m.Entries,
    +		}, nil
    +	}
    +	return nil, errors.New("entry not provided")
     }
     
     func (m *EntriesClient) GetLogEntryByUUID(params *entries.GetLogEntryByUUIDParams, opts ...entries.ClientOption) (*entries.GetLogEntryByUUIDOK, error) {
    -	return &entries.GetLogEntryByUUIDOK{
    -		Payload: data,
    -	}, nil
    +	if m.Entries != nil {
    +		return &entries.GetLogEntryByUUIDOK{
    +			Payload: *m.Entries,
    +		}, nil
    +	}
    +	return nil, errors.New("entry not provided")
     }
     
     func (m *EntriesClient) SearchLogQuery(params *entries.SearchLogQueryParams, opts ...entries.ClientOption) (*entries.SearchLogQueryOK, error) {
    +	resp := []models.LogEntry{}
    +	if m.Entries != nil {
    +		resp = append(resp, *m.Entries)
    +	}
     	return &entries.SearchLogQueryOK{
    -		Payload: []models.LogEntry{data},
    +		Payload: resp,
     	}, nil
     }
     
    
  • internal/pkg/cosign/rekor/signer_test.go+8 1 modified
    @@ -22,10 +22,12 @@ import (
     	"strings"
     	"testing"
     
    +	"github.com/go-openapi/swag"
     	"github.com/sigstore/cosign/internal/pkg/cosign/payload"
     	"github.com/sigstore/cosign/internal/pkg/cosign/rekor/mock"
     	"github.com/sigstore/cosign/pkg/cosign"
     	"github.com/sigstore/rekor/pkg/generated/client"
    +	"github.com/sigstore/rekor/pkg/generated/models"
     	"github.com/sigstore/sigstore/pkg/signature"
     )
     
    @@ -48,7 +50,12 @@ func TestSigner(t *testing.T) {
     
     	// Mock out Rekor client
     	var mClient client.Rekor
    -	mClient.Entries = &mock.EntriesClient{}
    +
    +	mClient.Entries = &mock.EntriesClient{
    +		Entries: &models.LogEntry{"123": models.LogEntryAnon{
    +			LogIndex: swag.Int64(123),
    +		}},
    +	}
     
     	testSigner := NewSigner(payloadSigner, &mClient)
     
    
  • pkg/cosign/verify.go+1 1 modified
    @@ -992,7 +992,7 @@ func VerifySET(bundlePayload cbundle.RekorPayload, signature []byte, pub *ecdsa.
     	// verify the SET against the public key
     	hash := sha256.Sum256(canonicalized)
     	if !ecdsa.VerifyASN1(pub, hash[:], signature) {
    -		return &VerificationError{"unable to verify"}
    +		return &VerificationError{"unable to verify SET"}
     	}
     	return nil
     }
    
  • pkg/cosign/verify_test.go+40 1 modified
    @@ -23,6 +23,7 @@ import (
     	"crypto/sha256"
     	"crypto/x509"
     	"encoding/base64"
    +	"encoding/hex"
     	"encoding/json"
     	"encoding/pem"
     	"errors"
    @@ -46,11 +47,13 @@ import (
     	"github.com/sigstore/cosign/pkg/types"
     	"github.com/sigstore/cosign/test"
     	"github.com/sigstore/rekor/pkg/generated/client"
    +	"github.com/sigstore/rekor/pkg/generated/models"
     	rtypes "github.com/sigstore/rekor/pkg/types"
     	"github.com/sigstore/sigstore/pkg/cryptoutils"
     	"github.com/sigstore/sigstore/pkg/signature"
     	"github.com/sigstore/sigstore/pkg/signature/options"
     	"github.com/stretchr/testify/require"
    +	"github.com/transparency-dev/merkle/rfc6962"
     )
     
     type mockVerifier struct {
    @@ -343,6 +346,40 @@ func TestVerifyImageSignatureWithExistingSub(t *testing.T) {
     	}
     }
     
    +var (
    +	lea = models.LogEntryAnon{
    +		Attestation:    &models.LogEntryAnonAttestation{},
    +		Body:           base64.StdEncoding.EncodeToString([]byte("asdf")),
    +		IntegratedTime: new(int64),
    +		LogID:          new(string),
    +		LogIndex:       new(int64),
    +		Verification: &models.LogEntryAnonVerification{
    +			InclusionProof: &models.InclusionProof{
    +				RootHash: new(string),
    +				TreeSize: new(int64),
    +				LogIndex: new(int64),
    +			},
    +		},
    +	}
    +	data = models.LogEntry{
    +		uuid(lea): lea,
    +	}
    +)
    +
    +// uuid generates the UUID for the given LogEntry.
    +// This is effectively a reimplementation of
    +// pkg/cosign/tlog.go -> verifyUUID / ComputeLeafHash, but separated
    +// to avoid a circular dependency.
    +// TODO?: Perhaps we should refactor the tlog libraries into a separate
    +// package?
    +func uuid(e models.LogEntryAnon) string {
    +	entryBytes, err := base64.StdEncoding.DecodeString(e.Body.(string))
    +	if err != nil {
    +		panic(err)
    +	}
    +	return hex.EncodeToString(rfc6962.DefaultHasher.HashLeaf(entryBytes))
    +}
    +
     // This test ensures that image signature validation fails properly if we are
     // using a SigVerifier with Rekor.
     // See https://github.com/sigstore/cosign/issues/1816 for more details.
    @@ -361,7 +398,9 @@ func TestVerifyImageSignatureWithSigVerifierAndRekor(t *testing.T) {
     	// tlog entry for the signature during validation (even though it does not
     	// match the underlying data / key)
     	mClient := new(client.Rekor)
    -	mClient.Entries = &mock.EntriesClient{}
    +	mClient.Entries = &mock.EntriesClient{
    +		Entries: &data,
    +	}
     
     	if _, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{
     		SigVerifier: sv,
    
  • test/cert_utils.go+29 1 modified
    @@ -69,7 +69,7 @@ func GenerateRootCa() (*x509.Certificate, *ecdsa.PrivateKey, error) {
     			CommonName:   "sigstore",
     			Organization: []string{"sigstore.dev"},
     		},
    -		NotBefore:             time.Now().Add(-5 * time.Minute),
    +		NotBefore:             time.Now().Add(-5 * time.Hour),
     		NotAfter:              time.Now().Add(5 * time.Hour),
     		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
     		BasicConstraintsValid: true,
    @@ -117,6 +117,34 @@ func GenerateSubordinateCa(rootTemplate *x509.Certificate, rootPriv crypto.Signe
     	return cert, priv, nil
     }
     
    +func GenerateLeafCertWithExpiration(subject string, oidcIssuer string, expiration time.Time,
    +	priv *ecdsa.PrivateKey,
    +	parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, error) {
    +	certTemplate := &x509.Certificate{
    +		SerialNumber:   big.NewInt(1),
    +		EmailAddresses: []string{subject},
    +		NotBefore:      expiration,
    +		NotAfter:       expiration.Add(10 * time.Minute),
    +		KeyUsage:       x509.KeyUsageDigitalSignature,
    +		ExtKeyUsage:    []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
    +		IsCA:           false,
    +		ExtraExtensions: []pkix.Extension{{
    +			// OID for OIDC Issuer extension
    +			Id:       asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
    +			Critical: false,
    +			Value:    []byte(oidcIssuer),
    +		},
    +		},
    +	}
    +
    +	cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	return cert, nil
    +}
    +
     func GenerateLeafCert(subject string, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) {
     	certTemplate := &x509.Certificate{
     		SerialNumber:   big.NewInt(1),
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.