From fd8773e770b08c3d9cd0ac31ce010e482513d33d Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Wed, 1 Oct 2025 16:30:39 -0700 Subject: [PATCH 01/12] Deduplicate key/token handling in sign commands Move the nearly identical code for parsing key options and creating a key pair and token out of attest, attest-blob, sign, and sign-blob, and into a common helper package. Move functions that had been shared out of sign.go into the helper package too so that other commands do not have to import the sign command package. Signed-off-by: Colleen Murphy --- cmd/cosign/cli/attest/attest.go | 58 +--- cmd/cosign/cli/attest/attest_blob.go | 56 +--- cmd/cosign/cli/sign/sign.go | 352 +-------------------- cmd/cosign/cli/sign/sign_blob.go | 60 +--- cmd/cosign/cli/sign/sign_test.go | 161 ---------- cmd/cosign/cli/signcommon/common.go | 378 +++++++++++++++++++++++ cmd/cosign/cli/signcommon/common_test.go | 182 +++++++++++ 7 files changed, 596 insertions(+), 651 deletions(-) create mode 100644 cmd/cosign/cli/signcommon/common.go create mode 100644 cmd/cosign/cli/signcommon/common_test.go diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index 6cf7c0fcbdd..b4bc50cd94a 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -29,9 +29,7 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" - cosign_sign "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" - "github.com/sigstore/cosign/v3/internal/auth" - "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" tsaclient "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" @@ -52,7 +50,7 @@ import ( type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error) -func uploadToTlog(ctx context.Context, sv *cosign_sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*models.LogEntryAnon, error) { +func uploadToTlog(ctx context.Context, sv *signcommon.SignerVerifier, rekorURL string, upload tlogUploadFn) (*models.LogEntryAnon, error) { rekorBytes, err := sv.Bytes(ctx) if err != nil { return nil, err @@ -157,49 +155,9 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { } if c.SigningConfig != nil { - var keypair sign.Keypair - var ephemeralKeypair bool - var idToken string - var sv *cosign_sign.SignerVerifier - var err error - - if c.Sk || c.Slot != "" || c.KeyRef != "" || c.CertPath != "" { - sv, _, err = cosign_sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - keypair, err = key.NewSignerVerifierKeypair(sv, c.DefaultLoadOptions) - if err != nil { - return fmt.Errorf("creating signerverifier keypair: %w", err) - } - } else { - keypair, err = sign.NewEphemeralKeypair(nil) - if err != nil { - return fmt.Errorf("generating keypair: %w", err) - } - ephemeralKeypair = true - } - defer func() { - if sv != nil { - sv.Close() - } - }() - - if ephemeralKeypair || c.IssueCertificateForExistingKey { - idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ - TokenOrPath: c.IDToken, - DisableProviders: c.OIDCDisableProviders, - Provider: c.OIDCProvider, - AuthFlow: c.FulcioAuthFlow, - SkipConfirm: c.SkipConfirmation, - OIDCServices: c.SigningConfig.OIDCProviderURLs(), - ClientID: c.OIDCClientID, - ClientSecret: c.OIDCClientSecret, - RedirectURL: c.OIDCRedirectURL, - }) - if err != nil { - return fmt.Errorf("retrieving ID token: %w", err) - } + keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, c.KeyOpts, c.CertPath, c.CertChainPath) + if err != nil { + return fmt.Errorf("getting keypair and token: %w", err) } content := &sign.DSSEData{ @@ -218,12 +176,12 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { return ociremote.WriteAttestationNewBundleFormat(digest, bundle, types.CosignSignPredicateType, ociremoteOpts...) } - sv, genKey, err := cosign_sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) if err != nil { return fmt.Errorf("getting signer: %w", err) } if genKey || c.IssueCertificateForExistingKey { - sv, err = cosign_sign.KeylessSigner(ctx, c.KeyOpts, sv) + sv, err = signcommon.KeylessSigner(ctx, c.KeyOpts, sv) if err != nil { return fmt.Errorf("getting Fulcio signer: %w", err) } @@ -293,7 +251,7 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { opts = append(opts, static.WithAnnotations(predicateTypeAnnotation)) // Check whether we should be uploading to the transparency log - shouldUpload, err := cosign_sign.ShouldUploadToTlog(ctx, c.KeyOpts, digest, c.TlogUpload) + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, digest, c.TlogUpload) if err != nil { return fmt.Errorf("should upload to tlog: %w", err) } diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index cc1c76aee91..187d692029d 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -33,9 +33,7 @@ import ( intotov1 "github.com/in-toto/attestation/go/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" - cosign_sign "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" - "github.com/sigstore/cosign/v3/internal/auth" - "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" tsaclient "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" @@ -158,49 +156,9 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } if c.SigningConfig != nil { - var keypair sign.Keypair - var ephemeralKeypair bool - var idToken string - var sv *cosign_sign.SignerVerifier - var err error - - if c.Sk || c.Slot != "" || c.KeyRef != "" || c.CertPath != "" { - sv, _, err = cosign_sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - keypair, err = key.NewSignerVerifierKeypair(sv, c.DefaultLoadOptions) - if err != nil { - return fmt.Errorf("creating signerverifier keypair: %w", err) - } - } else { - keypair, err = sign.NewEphemeralKeypair(nil) - if err != nil { - return fmt.Errorf("generating keypair: %w", err) - } - ephemeralKeypair = true - } - defer func() { - if sv != nil { - sv.Close() - } - }() - - if ephemeralKeypair || c.IssueCertificateForExistingKey { - idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ - TokenOrPath: c.IDToken, - DisableProviders: c.OIDCDisableProviders, - Provider: c.OIDCProvider, - AuthFlow: c.FulcioAuthFlow, - SkipConfirm: c.SkipConfirmation, - OIDCServices: c.SigningConfig.OIDCProviderURLs(), - ClientID: c.OIDCClientID, - ClientSecret: c.OIDCClientSecret, - RedirectURL: c.OIDCRedirectURL, - }) - if err != nil { - return fmt.Errorf("retrieving ID token: %w", err) - } + keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, c.KeyOpts, c.CertPath, c.CertChainPath) + if err != nil { + return fmt.Errorf("getting keypair and token: %w", err) } content := &sign.DSSEData{ @@ -218,12 +176,12 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error return nil } - sv, genKey, err := cosign_sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) if err != nil { return fmt.Errorf("getting signer: %w", err) } if genKey || c.IssueCertificateForExistingKey { - sv, err = cosign_sign.KeylessSigner(ctx, c.KeyOpts, sv) + sv, err = signcommon.KeylessSigner(ctx, c.KeyOpts, sv) if err != nil { return fmt.Errorf("getting Fulcio signer: %w", err) } @@ -292,7 +250,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error if err != nil { return err } - shouldUpload, err := cosign_sign.ShouldUploadToTlog(ctx, c.KeyOpts, nil, c.TlogUpload) + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, nil, c.TlogUpload) if err != nil { return fmt.Errorf("upload to tlog: %w", err) } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 43e060745fb..0e405966938 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -18,12 +18,8 @@ package sign import ( "bytes" "context" - "crypto" - "crypto/x509" "encoding/base64" "encoding/json" - "encoding/pem" - "errors" "fmt" "os" "path/filepath" @@ -31,15 +27,10 @@ import ( "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" intotov1 "github.com/in-toto/attestation/go/v1" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio/fulcioverifier" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign/privacy" - "github.com/sigstore/cosign/v3/internal/auth" - "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" icos "github.com/sigstore/cosign/v3/internal/pkg/cosign" ifulcio "github.com/sigstore/cosign/v3/internal/pkg/cosign/fulcio" ipayload "github.com/sigstore/cosign/v3/internal/pkg/cosign/payload" @@ -49,19 +40,14 @@ import ( "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/walk" - sigs "github.com/sigstore/cosign/v3/pkg/signature" "github.com/sigstore/cosign/v3/pkg/types" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore-go/pkg/sign" - "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" sigPayload "github.com/sigstore/sigstore/pkg/signature/payload" @@ -71,49 +57,6 @@ import ( _ "github.com/sigstore/cosign/v3/pkg/providers/all" ) -func ShouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool) (bool, error) { - upload := shouldUploadToTlog(ctx, ko, ref, tlogUpload) - var statementErr error - if upload { - privacy.StatementOnce.Do(func() { - ui.Infof(ctx, privacy.Statement) - ui.Infof(ctx, privacy.StatementConfirmation) - if !ko.SkipConfirmation { - if err := ui.ConfirmContinue(ctx); err != nil { - statementErr = err - } - } - }) - } - return upload, statementErr -} - -func shouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool) bool { - // return false if not uploading to the tlog has been requested - if !tlogUpload { - return false - } - - if ko.SkipConfirmation { - return true - } - - // We don't need to validate the ref, just return true - if ref == nil { - return true - } - - // Check if the image is public (no auth in Get) - if _, err := remote.Get(ref, remote.WithContext(ctx)); err != nil { - ui.Warnf(ctx, "%q appears to be a private repository, please confirm uploading to the transparency log at %q", ref.Context().String(), ko.RekorURL) - if ui.ConfirmContinue(ctx) != nil { - ui.Infof(ctx, "not uploading to transparency log") - return false - } - } - return true -} - func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremote.Option) (name.Reference, error) { if attachment == "" { return ref, nil @@ -249,55 +192,16 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt } if ko.SigningConfig != nil { - var keypair sign.Keypair - var ephemeralKeypair bool - var idToken string - var sv *SignerVerifier - var err error - - if ko.Sk || ko.Slot != "" || ko.KeyRef != "" || signOpts.Cert != "" { - sv, _, err = SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions) - if err != nil { - return fmt.Errorf("creating signerverifier keypair: %w", err) - } - } else { - keypair, err = sign.NewEphemeralKeypair(nil) - if err != nil { - return fmt.Errorf("generating keypair: %w", err) - } - ephemeralKeypair = true - } - defer func() { - if sv != nil { - sv.Close() - } - }() - - if ephemeralKeypair || ko.IssueCertificateForExistingKey { - idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ - TokenOrPath: ko.IDToken, - DisableProviders: ko.OIDCDisableProviders, - Provider: ko.OIDCProvider, - AuthFlow: ko.FulcioAuthFlow, - SkipConfirm: ko.SkipConfirmation, - OIDCServices: ko.SigningConfig.OIDCProviderURLs(), - ClientID: ko.OIDCClientID, - ClientSecret: ko.OIDCClientSecret, - RedirectURL: ko.OIDCRedirectURL, - }) - if err != nil { - return fmt.Errorf("retrieving ID token: %w", err) - } + keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, signOpts.Cert, signOpts.CertChain) + if err != nil { + return fmt.Errorf("getting keypair and token: %w", err) } content := &sign.DSSEData{ Data: payload, PayloadType: "application/vnd.in-toto+json", } + bundle, err := cbundle.SignData(ctx, content, keypair, idToken, ko.SigningConfig, ko.TrustedMaterial) if err != nil { return fmt.Errorf("signing bundle: %w", err) @@ -311,12 +215,12 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt return ociremote.WriteAttestationNewBundleFormat(digest, bundle, types.CosignSignPredicateType, ociremoteOpts...) } - sv, genKey, err := SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) + sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) if err != nil { return fmt.Errorf("getting signer: %w", err) } if genKey || ko.IssueCertificateForExistingKey { - sv, err = KeylessSigner(ctx, ko, sv) + sv, err = signcommon.KeylessSigner(ctx, ko, sv) if err != nil { return fmt.Errorf("getting Fulcio signer: %w", err) } @@ -356,7 +260,7 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt } var rekorEntry *models.LogEntryAnon - shouldUpload, err := ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) if err != nil { return fmt.Errorf("should upload to tlog: %w", err) } @@ -404,12 +308,12 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti } } - sv, genKey, err := SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) + sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) if err != nil { return fmt.Errorf("getting signer: %w", err) } if genKey || ko.IssueCertificateForExistingKey { - sv, err = KeylessSigner(ctx, ko, sv) + sv, err = signcommon.KeylessSigner(ctx, ko, sv) if err != nil { return fmt.Errorf("getting Fulcio signer: %w", err) } @@ -435,7 +339,7 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti )) } } - shouldUpload, err := ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) if err != nil { return fmt.Errorf("should upload to tlog: %w", err) } @@ -540,240 +444,6 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti return ociremote.WriteSignatures(digest.Repository, newSE, walkOpts...) } -func signerFromSecurityKey(ctx context.Context, keySlot string) (*SignerVerifier, error) { - sk, err := pivkey.GetKeyWithSlot(keySlot) - if err != nil { - return nil, err - } - sv, err := sk.SignerVerifier() - if err != nil { - sk.Close() - return nil, err - } - - // Handle the -cert flag. - // With PIV, we assume the certificate is in the same slot on the PIV - // token as the private key. If it's not there, show a warning to the - // user. - certFromPIV, err := sk.Certificate() - var pemBytes []byte - if err != nil { - ui.Warnf(ctx, "no x509 certificate retrieved from the PIV token") - } else { - pemBytes, err = cryptoutils.MarshalCertificateToPEM(certFromPIV) - if err != nil { - sk.Close() - return nil, err - } - } - - return &SignerVerifier{ - Cert: pemBytes, - SignerVerifier: sv, - close: sk.Close, - }, nil -} - -func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc, defaultLoadOptions *[]signature.LoadOption) (*SignerVerifier, error) { - k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc, defaultLoadOptions) - if err != nil { - return nil, fmt.Errorf("reading key: %w", err) - } - certSigner := &SignerVerifier{ - SignerVerifier: k, - } - - var leafCert *x509.Certificate - - // Attempt to extract certificate from PKCS11 token - // With PKCS11, we assume the certificate is in the same slot on the PKCS11 - // token as the private key. If it's not there, show a warning to the - // user. - if pkcs11Key, ok := k.(*pkcs11key.Key); ok { - certFromPKCS11, _ := pkcs11Key.Certificate() - certSigner.close = pkcs11Key.Close - - if certFromPKCS11 == nil { - ui.Warnf(ctx, "no x509 certificate retrieved from the PKCS11 token") - } else { - pemBytes, err := cryptoutils.MarshalCertificateToPEM(certFromPKCS11) - if err != nil { - pkcs11Key.Close() - return nil, err - } - // Check that the provided public key and certificate key match - pubKey, err := k.PublicKey() - if err != nil { - pkcs11Key.Close() - return nil, err - } - if cryptoutils.EqualKeys(pubKey, certFromPKCS11.PublicKey) != nil { - pkcs11Key.Close() - return nil, errors.New("pkcs11 key and certificate do not match") - } - leafCert = certFromPKCS11 - certSigner.Cert = pemBytes - } - } - - // Handle --cert flag - if certPath != "" { - // Allow both DER and PEM encoding - certBytes, err := os.ReadFile(certPath) - if err != nil { - return nil, fmt.Errorf("read certificate: %w", err) - } - // Handle PEM - if bytes.HasPrefix(certBytes, []byte("-----")) { - decoded, _ := pem.Decode(certBytes) - if decoded.Type != "CERTIFICATE" { - return nil, fmt.Errorf("supplied PEM file is not a certificate: %s", certPath) - } - certBytes = decoded.Bytes - } - parsedCert, err := x509.ParseCertificate(certBytes) - if err != nil { - return nil, fmt.Errorf("parse x509 certificate: %w", err) - } - pk, err := k.PublicKey() - if err != nil { - return nil, fmt.Errorf("get public key: %w", err) - } - if cryptoutils.EqualKeys(pk, parsedCert.PublicKey) != nil { - return nil, errors.New("public key in certificate does not match the provided public key") - } - pemBytes, err := cryptoutils.MarshalCertificateToPEM(parsedCert) - if err != nil { - return nil, fmt.Errorf("marshaling certificate to PEM: %w", err) - } - if certSigner.Cert != nil { - ui.Warnf(ctx, "overriding x509 certificate retrieved from the PKCS11 token") - } - leafCert = parsedCert - certSigner.Cert = pemBytes - } - - if certChainPath == "" { - return certSigner, nil - } else if certSigner.Cert == nil { - return nil, errors.New("no leaf certificate found or provided while specifying chain") - } - - // Handle --cert-chain flag - // Accept only PEM encoded certificate chain - certChainBytes, err := os.ReadFile(certChainPath) - if err != nil { - return nil, fmt.Errorf("reading certificate chain from path: %w", err) - } - certChain, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(certChainBytes)) - if err != nil { - return nil, fmt.Errorf("loading certificate chain: %w", err) - } - if len(certChain) == 0 { - return nil, errors.New("no certificates in certificate chain") - } - // Verify certificate chain is valid - rootPool := x509.NewCertPool() - rootPool.AddCert(certChain[len(certChain)-1]) - subPool := x509.NewCertPool() - for _, c := range certChain[:len(certChain)-1] { - subPool.AddCert(c) - } - if _, err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { - return nil, fmt.Errorf("unable to validate certificate chain: %w", err) - } - certSigner.Chain = certChainBytes - - return certSigner, nil -} - -func signerFromNewKey() (*SignerVerifier, error) { - privKey, err := cosign.GeneratePrivateKey() - if err != nil { - return nil, fmt.Errorf("generating cert: %w", err) - } - sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) - if err != nil { - return nil, err - } - - return &SignerVerifier{ - SignerVerifier: sv, - }, nil -} - -func KeylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) (*SignerVerifier, error) { - var ( - k *fulcio.Signer - err error - ) - - if _, ok := sv.SignerVerifier.(*signature.ED25519phSignerVerifier); ok { - return nil, fmt.Errorf("ed25519ph unsupported by Fulcio") - } - - if ko.InsecureSkipFulcioVerify { - if k, err = fulcio.NewSigner(ctx, ko, sv); err != nil { - return nil, fmt.Errorf("getting key from Fulcio: %w", err) - } - } else { - if k, err = fulcioverifier.NewSigner(ctx, ko, sv); err != nil { - return nil, fmt.Errorf("getting key from Fulcio: %w", err) - } - } - - return &SignerVerifier{ - Cert: k.Cert, - Chain: k.Chain, - SignerVerifier: k, - }, nil -} - -func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, bool, error) { - var sv *SignerVerifier - var err error - genKey := false - switch { - case ko.Sk: - sv, err = signerFromSecurityKey(ctx, ko.Slot) - case ko.KeyRef != "": - sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc, ko.DefaultLoadOptions) - default: - genKey = true - ui.Infof(ctx, "Generating ephemeral keys...") - sv, err = signerFromNewKey() - } - if err != nil { - return nil, false, err - } - return sv, genKey, nil -} - -type SignerVerifier struct { - Cert []byte - Chain []byte - signature.SignerVerifier - close func() -} - -func (c *SignerVerifier) Close() { - if c.close != nil { - c.close() - } -} - -func (c *SignerVerifier) Bytes(ctx context.Context) ([]byte, error) { - if c.Cert != nil { - return c.Cert, nil - } - - pemBytes, err := sigs.PublicKeyPem(c, signatureoptions.WithContext(ctx)) - if err != nil { - return nil, err - } - return pemBytes, nil -} - func fetchLocalSignedPayload(sig oci.Signature) (*cosign.LocalSignedPayload, error) { signedPayload := &cosign.LocalSignedPayload{} var err error diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index cba9af705ac..2d3cda17ca9 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -31,8 +31,7 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" - "github.com/sigstore/cosign/v3/internal/auth" - "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" internal "github.com/sigstore/cosign/v3/internal/pkg/cosign" "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" @@ -67,7 +66,7 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string ctx, cancel := context.WithTimeout(context.Background(), ro.Timeout) defer cancel() - shouldUpload, err := ShouldUploadToTlog(ctx, ko, nil, tlogUpload) + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, nil, tlogUpload) if err != nil { return nil, fmt.Errorf("upload to tlog: %w", err) } @@ -80,49 +79,9 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string } if ko.SigningConfig != nil { - var keypair sign.Keypair - var ephemeralKeypair bool - var idToken string - var sv *SignerVerifier - var err error - - if ko.Sk || ko.Slot != "" || ko.KeyRef != "" { - sv, _, err = SignerFromKeyOpts(ctx, "", "", ko) - if err != nil { - return nil, fmt.Errorf("getting signer: %w", err) - } - keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions) - if err != nil { - return nil, fmt.Errorf("creating signerverifier keypair: %w", err) - } - } else { - keypair, err = sign.NewEphemeralKeypair(nil) - if err != nil { - return nil, fmt.Errorf("generating keypair: %w", err) - } - ephemeralKeypair = true - } - defer func() { - if sv != nil { - sv.Close() - } - }() - - if ephemeralKeypair || ko.IssueCertificateForExistingKey { - idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ - TokenOrPath: ko.IDToken, - DisableProviders: ko.OIDCDisableProviders, - Provider: ko.OIDCProvider, - AuthFlow: ko.FulcioAuthFlow, - SkipConfirm: ko.SkipConfirmation, - OIDCServices: ko.SigningConfig.OIDCProviderURLs(), - ClientID: ko.OIDCClientID, - ClientSecret: ko.OIDCClientSecret, - RedirectURL: ko.OIDCRedirectURL, - }) - if err != nil { - return nil, fmt.Errorf("retrieving ID token: %w", err) - } + keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, "", "") + if err != nil { + return nil, fmt.Errorf("getting keypair and token: %w", err) } payload, closePayload, err := getPayload(ctx, payloadPath, protoHashAlgoToHash(keypair.GetHashAlgorithm())) @@ -137,6 +96,7 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string content := &sign.PlainData{ Data: data, } + bundle, err := cbundle.SignData(ctx, content, keypair, idToken, ko.SigningConfig, ko.TrustedMaterial) if err != nil { return nil, fmt.Errorf("signing bundle: %w", err) @@ -148,12 +108,12 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string return bundle, nil } - sv, genKey, err := SignerFromKeyOpts(ctx, "", "", ko) + sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, "", "", ko) if err != nil { return nil, err } if genKey || ko.IssueCertificateForExistingKey { - sv, err = KeylessSigner(ctx, ko, sv) + sv, err = signcommon.KeylessSigner(ctx, ko, sv) if err != nil { return nil, fmt.Errorf("getting Fulcio signer: %w", err) } @@ -356,7 +316,7 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string } // Extract an encoded certificate from the SignerVerifier. Returns (nil, nil) if verifier is not a certificate. -func extractCertificate(ctx context.Context, sv *SignerVerifier) ([]byte, error) { +func extractCertificate(ctx context.Context, sv *signcommon.SignerVerifier) ([]byte, error) { signer, err := sv.Bytes(ctx) if err != nil { return nil, fmt.Errorf("error getting signer: %w", err) @@ -369,7 +329,7 @@ func extractCertificate(ctx context.Context, sv *SignerVerifier) ([]byte, error) return nil, nil } -func getHashFunction(sv *SignerVerifier, defaultLoadOptions *[]signature.LoadOption) (crypto.Hash, error) { +func getHashFunction(sv *signcommon.SignerVerifier, defaultLoadOptions *[]signature.LoadOption) (crypto.Hash, error) { pubKey, err := sv.PublicKey() if err != nil { return crypto.Hash(0), fmt.Errorf("error getting public key: %w", err) diff --git a/cmd/cosign/cli/sign/sign_test.go b/cmd/cosign/cli/sign/sign_test.go index 78ad209d7cc..cd7b73f256b 100644 --- a/cmd/cosign/cli/sign/sign_test.go +++ b/cmd/cosign/cli/sign/sign_test.go @@ -17,97 +17,16 @@ package sign import ( "context" - "crypto/ecdsa" - "crypto/x509" - "encoding/pem" "errors" - "os" - "reflect" - "strings" "testing" "github.com/stretchr/testify/assert" - "github.com/secure-systems-lab/go-securesystemslib/encrypted" "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/internal/test" "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/sigstore/pkg/cryptoutils" ) -func pass(s string) cosign.PassFunc { - return func(_ bool) ([]byte, error) { - return []byte(s), nil - } -} - -func generateCertificateFiles(t *testing.T, tmpDir string, pf cosign.PassFunc) (privFile, certFile, chainFile string, privKey *ecdsa.PrivateKey, cert *x509.Certificate, chain []*x509.Certificate) { - t.Helper() - - rootCert, rootKey, _ := test.GenerateRootCa() - subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) - leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - x509Encoded, err := x509.MarshalPKCS8PrivateKey(privKey) - if err != nil { - t.Fatalf("failed to encode private key: %v", err) - } - password := []byte{} - if pf != nil { - password, err = pf(true) - if err != nil { - t.Fatalf("failed to read password: %v", err) - } - } - - encBytes, err := encrypted.Encrypt(x509Encoded, password) - if err != nil { - t.Fatalf("failed to encrypt key: %v", err) - } - - // store in PEM format - privBytes := pem.EncodeToMemory(&pem.Block{ - Bytes: encBytes, - Type: cosign.CosignPrivateKeyPemType, - }) - - tmpPrivFile, err := os.CreateTemp(tmpDir, "cosign_test_*.key") - if err != nil { - t.Fatalf("failed to create temp key file: %v", err) - } - defer tmpPrivFile.Close() - if _, err := tmpPrivFile.Write(privBytes); err != nil { - t.Fatalf("failed to write key file: %v", err) - } - - tmpCertFile, err := os.CreateTemp(tmpDir, "cosign.crt") - if err != nil { - t.Fatalf("failed to create temp certificate file: %v", err) - } - defer tmpCertFile.Close() - if _, err := tmpCertFile.Write(pemLeaf); err != nil { - t.Fatalf("failed to write certificate file: %v", err) - } - - tmpChainFile, err := os.CreateTemp(tmpDir, "cosign_chain.crt") - if err != nil { - t.Fatalf("failed to create temp chain file: %v", err) - } - defer tmpChainFile.Close() - pemChain := pemSub - pemChain = append(pemChain, pemRoot...) - if _, err := tmpChainFile.Write(pemChain); err != nil { - t.Fatalf("failed to write chain file: %v", err) - } - - return tmpPrivFile.Name(), tmpCertFile.Name(), tmpChainFile.Name(), privKey, leafCert, []*x509.Certificate{subCert, rootCert} -} - // TestSignCmdLocalKeyAndSk verifies the SignCmd returns an error // if both a local key path and a sk are specified func TestSignCmdLocalKeyAndSk(t *testing.T) { @@ -129,86 +48,6 @@ func TestSignCmdLocalKeyAndSk(t *testing.T) { } } -func Test_signerFromKeyRefSuccess(t *testing.T) { - tmpDir := t.TempDir() - ctx := context.Background() - keyFile, certFile, chainFile, privKey, cert, chain := generateCertificateFiles(t, tmpDir, pass("foo")) - - signer, err := signerFromKeyRef(ctx, certFile, chainFile, keyFile, pass("foo"), nil) - if err != nil { - t.Fatalf("unexpected error generating signer: %v", err) - } - // Expect public key matches - pubKey, err := signer.PublicKey() - if err != nil { - t.Fatalf("unexpected error fetching pubkey: %v", err) - } - if !privKey.Public().(*ecdsa.PublicKey).Equal(pubKey) { - t.Fatalf("public keys must be equal") - } - // Expect certificate matches - expectedPemBytes, err := cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - t.Fatalf("unexpected error marshalling certificate: %v", err) - } - if !reflect.DeepEqual(signer.Cert, expectedPemBytes) { - t.Fatalf("certificates must match") - } - // Expect certificate chain matches - expectedPemBytesChain, err := cryptoutils.MarshalCertificatesToPEM(chain) - if err != nil { - t.Fatalf("unexpected error marshalling certificate chain: %v", err) - } - if !reflect.DeepEqual(signer.Chain, expectedPemBytesChain) { - t.Fatalf("certificate chains must match") - } -} - -func Test_signerFromKeyRefFailure(t *testing.T) { - tmpDir := t.TempDir() - ctx := context.Background() - keyFile, certFile, _, _, _, _ := generateCertificateFiles(t, tmpDir, pass("foo")) - // Second set of files - tmpDir2 := t.TempDir() - _, certFile2, chainFile2, _, _, _ := generateCertificateFiles(t, tmpDir2, pass("bar")) - - // Public keys don't match - _, err := signerFromKeyRef(ctx, certFile2, chainFile2, keyFile, pass("foo"), nil) - if err == nil || err.Error() != "public key in certificate does not match the provided public key" { - t.Fatalf("expected mismatched keys error, got %v", err) - } - // Certificate chain cannot be verified - _, err = signerFromKeyRef(ctx, certFile, chainFile2, keyFile, pass("foo"), nil) - if err == nil || !strings.Contains(err.Error(), "unable to validate certificate chain") { - t.Fatalf("expected chain verification error, got %v", err) - } - // Certificate chain specified without certificate - _, err = signerFromKeyRef(ctx, "", chainFile2, keyFile, pass("foo"), nil) - if err == nil || !strings.Contains(err.Error(), "no leaf certificate found or provided while specifying chain") { - t.Fatalf("expected no leaf error, got %v", err) - } -} - -func Test_signerFromKeyRefFailureEmptyChainFile(t *testing.T) { - tmpDir := t.TempDir() - ctx := context.Background() - keyFile, certFile, _, _, _, _ := generateCertificateFiles(t, tmpDir, pass("foo")) - - tmpChainFile, err := os.CreateTemp(tmpDir, "cosign_chain_empty.crt") - if err != nil { - t.Fatalf("failed to create temp chain file: %v", err) - } - defer tmpChainFile.Close() - if _, err := tmpChainFile.Write([]byte{}); err != nil { - t.Fatalf("failed to write chain file: %v", err) - } - - _, err = signerFromKeyRef(ctx, certFile, tmpChainFile.Name(), keyFile, pass("foo"), nil) - if err == nil || err.Error() != "no certificates in certificate chain" { - t.Fatalf("expected empty chain error, got %v", err) - } -} - func Test_ParseOCIReference(t *testing.T) { var tests = []struct { ref string diff --git a/cmd/cosign/cli/signcommon/common.go b/cmd/cosign/cli/signcommon/common.go new file mode 100644 index 00000000000..24924aada8c --- /dev/null +++ b/cmd/cosign/cli/signcommon/common.go @@ -0,0 +1,378 @@ +// Copyright 2025 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package signcommon + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio/fulcioverifier" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign/privacy" + "github.com/sigstore/cosign/v3/internal/auth" + "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/internal/ui" + "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" + "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" + sigs "github.com/sigstore/cosign/v3/pkg/signature" + "github.com/sigstore/sigstore-go/pkg/sign" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" +) + +// SignerVerifier contains keys or certs to sign and verify. +type SignerVerifier struct { + Cert []byte + Chain []byte + signature.SignerVerifier + close func() +} + +// Close closes the key context if there is one. +func (c *SignerVerifier) Close() { + if c.close != nil { + c.close() + } +} + +// Bytes returns the raw bytes of the cert or key. +func (c *SignerVerifier) Bytes(ctx context.Context) ([]byte, error) { + if c.Cert != nil { + return c.Cert, nil + } + + pemBytes, err := sigs.PublicKeyPem(c, signatureoptions.WithContext(ctx)) + if err != nil { + return nil, err + } + return pemBytes, nil +} + +// GetKeypairAndToken creates a keypair object from provided key or cert flags or generates an ephemeral key. +// For an ephemeral key, it also uses the key to fetch an OIDC token, the pair of which are later used to get a Fulcio cert. +func GetKeypairAndToken(ctx context.Context, ko options.KeyOpts, cert, certChain string) (sign.Keypair, string, error) { + var keypair sign.Keypair + var ephemeralKeypair bool + var idToken string + var sv *SignerVerifier + var err error + + if ko.Sk || ko.Slot != "" || ko.KeyRef != "" || cert != "" { + sv, _, err = SignerFromKeyOpts(ctx, cert, certChain, ko) + if err != nil { + return nil, "", fmt.Errorf("getting signer: %w", err) + } + keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions) + if err != nil { + return nil, "", fmt.Errorf("creating signerverifier keypair: %w", err) + } + } else { + keypair, err = sign.NewEphemeralKeypair(nil) + if err != nil { + return nil, "", fmt.Errorf("generating keypair: %w", err) + } + ephemeralKeypair = true + } + defer func() { + if sv != nil { + sv.Close() + } + }() + + if ephemeralKeypair || ko.IssueCertificateForExistingKey { + idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ + TokenOrPath: ko.IDToken, + DisableProviders: ko.OIDCDisableProviders, + Provider: ko.OIDCProvider, + AuthFlow: ko.FulcioAuthFlow, + SkipConfirm: ko.SkipConfirmation, + OIDCServices: ko.SigningConfig.OIDCProviderURLs(), + ClientID: ko.OIDCClientID, + ClientSecret: ko.OIDCClientSecret, + RedirectURL: ko.OIDCRedirectURL, + }) + if err != nil { + return nil, "", fmt.Errorf("retrieving ID token: %w", err) + } + } + + return keypair, idToken, nil +} + +// KeylessSigner fetches an identity certificate from Fulcio and returns a SignerVerifier with the returned signing material. +func KeylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) (*SignerVerifier, error) { + var ( + k *fulcio.Signer + err error + ) + + if _, ok := sv.SignerVerifier.(*signature.ED25519phSignerVerifier); ok { + return nil, fmt.Errorf("ed25519ph unsupported by Fulcio") + } + + if ko.InsecureSkipFulcioVerify { + if k, err = fulcio.NewSigner(ctx, ko, sv); err != nil { + return nil, fmt.Errorf("getting key from Fulcio: %w", err) + } + } else { + if k, err = fulcioverifier.NewSigner(ctx, ko, sv); err != nil { + return nil, fmt.Errorf("getting key from Fulcio: %w", err) + } + } + + return &SignerVerifier{ + Cert: k.Cert, + Chain: k.Chain, + SignerVerifier: k, + }, nil +} + +// ShouldUploadToTlog determines whether the user wants to upload the entry to Rekor. +func ShouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool) (bool, error) { + upload := shouldUploadToTlog(ctx, ko, ref, tlogUpload) + var statementErr error + if upload { + privacy.StatementOnce.Do(func() { + ui.Infof(ctx, privacy.Statement) + ui.Infof(ctx, privacy.StatementConfirmation) + if !ko.SkipConfirmation { + if err := ui.ConfirmContinue(ctx); err != nil { + statementErr = err + } + } + }) + } + return upload, statementErr +} + +func shouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool) bool { + // return false if not uploading to the tlog has been requested + if !tlogUpload { + return false + } + + if ko.SkipConfirmation { + return true + } + + // We don't need to validate the ref, just return true + if ref == nil { + return true + } + + // Check if the image is public (no auth in Get) + if _, err := remote.Get(ref, remote.WithContext(ctx)); err != nil { + ui.Warnf(ctx, "%q appears to be a private repository, please confirm uploading to the transparency log at %q", ref.Context().String(), ko.RekorURL) + if ui.ConfirmContinue(ctx) != nil { + ui.Infof(ctx, "not uploading to transparency log") + return false + } + } + return true +} + +// SignerFromKeyOpts generates a SignerVerifier from provided key flags. +func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, bool, error) { + var sv *SignerVerifier + var err error + genKey := false + switch { + case ko.Sk: + sv, err = signerFromSecurityKey(ctx, ko.Slot) + case ko.KeyRef != "": + sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc, ko.DefaultLoadOptions) + default: + genKey = true + ui.Infof(ctx, "Generating ephemeral keys...") + sv, err = signerFromNewKey() + } + if err != nil { + return nil, false, err + } + return sv, genKey, nil +} + +func signerFromSecurityKey(ctx context.Context, keySlot string) (*SignerVerifier, error) { + sk, err := pivkey.GetKeyWithSlot(keySlot) + if err != nil { + return nil, err + } + sv, err := sk.SignerVerifier() + if err != nil { + sk.Close() + return nil, err + } + + // Handle the -cert flag. + // With PIV, we assume the certificate is in the same slot on the PIV + // token as the private key. If it's not there, show a warning to the + // user. + certFromPIV, err := sk.Certificate() + var pemBytes []byte + if err != nil { + ui.Warnf(ctx, "no x509 certificate retrieved from the PIV token") + } else { + pemBytes, err = cryptoutils.MarshalCertificateToPEM(certFromPIV) + if err != nil { + sk.Close() + return nil, err + } + } + + return &SignerVerifier{ + Cert: pemBytes, + SignerVerifier: sv, + close: sk.Close, + }, nil +} + +func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc, defaultLoadOptions *[]signature.LoadOption) (*SignerVerifier, error) { + k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc, defaultLoadOptions) + if err != nil { + return nil, fmt.Errorf("reading key: %w", err) + } + certSigner := &SignerVerifier{ + SignerVerifier: k, + } + + var leafCert *x509.Certificate + + // Attempt to extract certificate from PKCS11 token + // With PKCS11, we assume the certificate is in the same slot on the PKCS11 + // token as the private key. If it's not there, show a warning to the + // user. + if pkcs11Key, ok := k.(*pkcs11key.Key); ok { + certFromPKCS11, _ := pkcs11Key.Certificate() + certSigner.close = pkcs11Key.Close + + if certFromPKCS11 == nil { + ui.Warnf(ctx, "no x509 certificate retrieved from the PKCS11 token") + } else { + pemBytes, err := cryptoutils.MarshalCertificateToPEM(certFromPKCS11) + if err != nil { + pkcs11Key.Close() + return nil, err + } + // Check that the provided public key and certificate key match + pubKey, err := k.PublicKey() + if err != nil { + pkcs11Key.Close() + return nil, err + } + if cryptoutils.EqualKeys(pubKey, certFromPKCS11.PublicKey) != nil { + pkcs11Key.Close() + return nil, errors.New("pkcs11 key and certificate do not match") + } + leafCert = certFromPKCS11 + certSigner.Cert = pemBytes + } + } + + // Handle --cert flag + if certPath != "" { + // Allow both DER and PEM encoding + certBytes, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf("read certificate: %w", err) + } + // Handle PEM + if bytes.HasPrefix(certBytes, []byte("-----")) { + decoded, _ := pem.Decode(certBytes) + if decoded.Type != "CERTIFICATE" { + return nil, fmt.Errorf("supplied PEM file is not a certificate: %s", certPath) + } + certBytes = decoded.Bytes + } + parsedCert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, fmt.Errorf("parse x509 certificate: %w", err) + } + pk, err := k.PublicKey() + if err != nil { + return nil, fmt.Errorf("get public key: %w", err) + } + if cryptoutils.EqualKeys(pk, parsedCert.PublicKey) != nil { + return nil, errors.New("public key in certificate does not match the provided public key") + } + pemBytes, err := cryptoutils.MarshalCertificateToPEM(parsedCert) + if err != nil { + return nil, fmt.Errorf("marshaling certificate to PEM: %w", err) + } + if certSigner.Cert != nil { + ui.Warnf(ctx, "overriding x509 certificate retrieved from the PKCS11 token") + } + leafCert = parsedCert + certSigner.Cert = pemBytes + } + + if certChainPath == "" { + return certSigner, nil + } else if certSigner.Cert == nil { + return nil, errors.New("no leaf certificate found or provided while specifying chain") + } + + // Handle --cert-chain flag + // Accept only PEM encoded certificate chain + certChainBytes, err := os.ReadFile(certChainPath) + if err != nil { + return nil, fmt.Errorf("reading certificate chain from path: %w", err) + } + certChain, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(certChainBytes)) + if err != nil { + return nil, fmt.Errorf("loading certificate chain: %w", err) + } + if len(certChain) == 0 { + return nil, errors.New("no certificates in certificate chain") + } + // Verify certificate chain is valid + rootPool := x509.NewCertPool() + rootPool.AddCert(certChain[len(certChain)-1]) + subPool := x509.NewCertPool() + for _, c := range certChain[:len(certChain)-1] { + subPool.AddCert(c) + } + if _, err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { + return nil, fmt.Errorf("unable to validate certificate chain: %w", err) + } + certSigner.Chain = certChainBytes + + return certSigner, nil +} + +func signerFromNewKey() (*SignerVerifier, error) { + privKey, err := cosign.GeneratePrivateKey() + if err != nil { + return nil, fmt.Errorf("generating cert: %w", err) + } + sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) + if err != nil { + return nil, err + } + + return &SignerVerifier{ + SignerVerifier: sv, + }, nil +} diff --git a/cmd/cosign/cli/signcommon/common_test.go b/cmd/cosign/cli/signcommon/common_test.go new file mode 100644 index 00000000000..43da4a128d2 --- /dev/null +++ b/cmd/cosign/cli/signcommon/common_test.go @@ -0,0 +1,182 @@ +// Copyright 2025 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package signcommon + +import ( + "context" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "os" + "reflect" + "strings" + "testing" + + "github.com/secure-systems-lab/go-securesystemslib/encrypted" + "github.com/sigstore/cosign/v3/internal/test" + "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +func pass(s string) cosign.PassFunc { + return func(_ bool) ([]byte, error) { + return []byte(s), nil + } +} + +func generateCertificateFiles(t *testing.T, tmpDir string, pf cosign.PassFunc) (privFile, certFile, chainFile string, privKey *ecdsa.PrivateKey, cert *x509.Certificate, chain []*x509.Certificate) { + t.Helper() + + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + x509Encoded, err := x509.MarshalPKCS8PrivateKey(privKey) + if err != nil { + t.Fatalf("failed to encode private key: %v", err) + } + password := []byte{} + if pf != nil { + password, err = pf(true) + if err != nil { + t.Fatalf("failed to read password: %v", err) + } + } + + encBytes, err := encrypted.Encrypt(x509Encoded, password) + if err != nil { + t.Fatalf("failed to encrypt key: %v", err) + } + + // store in PEM format + privBytes := pem.EncodeToMemory(&pem.Block{ + Bytes: encBytes, + Type: cosign.CosignPrivateKeyPemType, + }) + + tmpPrivFile, err := os.CreateTemp(tmpDir, "cosign_test_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + defer tmpPrivFile.Close() + if _, err := tmpPrivFile.Write(privBytes); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + + tmpCertFile, err := os.CreateTemp(tmpDir, "cosign.crt") + if err != nil { + t.Fatalf("failed to create temp certificate file: %v", err) + } + defer tmpCertFile.Close() + if _, err := tmpCertFile.Write(pemLeaf); err != nil { + t.Fatalf("failed to write certificate file: %v", err) + } + + tmpChainFile, err := os.CreateTemp(tmpDir, "cosign_chain.crt") + if err != nil { + t.Fatalf("failed to create temp chain file: %v", err) + } + defer tmpChainFile.Close() + pemChain := pemSub + pemChain = append(pemChain, pemRoot...) + if _, err := tmpChainFile.Write(pemChain); err != nil { + t.Fatalf("failed to write chain file: %v", err) + } + + return tmpPrivFile.Name(), tmpCertFile.Name(), tmpChainFile.Name(), privKey, leafCert, []*x509.Certificate{subCert, rootCert} +} + +func Test_signerFromKeyRefSuccess(t *testing.T) { + tmpDir := t.TempDir() + ctx := context.Background() + keyFile, certFile, chainFile, privKey, cert, chain := generateCertificateFiles(t, tmpDir, pass("foo")) + + signer, err := signerFromKeyRef(ctx, certFile, chainFile, keyFile, pass("foo"), nil) + if err != nil { + t.Fatalf("unexpected error generating signer: %v", err) + } + // Expect public key matches + pubKey, err := signer.PublicKey() + if err != nil { + t.Fatalf("unexpected error fetching pubkey: %v", err) + } + if !privKey.Public().(*ecdsa.PublicKey).Equal(pubKey) { + t.Fatalf("public keys must be equal") + } + // Expect certificate matches + expectedPemBytes, err := cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + t.Fatalf("unexpected error marshalling certificate: %v", err) + } + if !reflect.DeepEqual(signer.Cert, expectedPemBytes) { + t.Fatalf("certificates must match") + } + // Expect certificate chain matches + expectedPemBytesChain, err := cryptoutils.MarshalCertificatesToPEM(chain) + if err != nil { + t.Fatalf("unexpected error marshalling certificate chain: %v", err) + } + if !reflect.DeepEqual(signer.Chain, expectedPemBytesChain) { + t.Fatalf("certificate chains must match") + } +} + +func Test_signerFromKeyRefFailure(t *testing.T) { + tmpDir := t.TempDir() + ctx := context.Background() + keyFile, certFile, _, _, _, _ := generateCertificateFiles(t, tmpDir, pass("foo")) + // Second set of files + tmpDir2 := t.TempDir() + _, certFile2, chainFile2, _, _, _ := generateCertificateFiles(t, tmpDir2, pass("bar")) + + // Public keys don't match + _, err := signerFromKeyRef(ctx, certFile2, chainFile2, keyFile, pass("foo"), nil) + if err == nil || err.Error() != "public key in certificate does not match the provided public key" { + t.Fatalf("expected mismatched keys error, got %v", err) + } + // Certificate chain cannot be verified + _, err = signerFromKeyRef(ctx, certFile, chainFile2, keyFile, pass("foo"), nil) + if err == nil || !strings.Contains(err.Error(), "unable to validate certificate chain") { + t.Fatalf("expected chain verification error, got %v", err) + } + // Certificate chain specified without certificate + _, err = signerFromKeyRef(ctx, "", chainFile2, keyFile, pass("foo"), nil) + if err == nil || !strings.Contains(err.Error(), "no leaf certificate found or provided while specifying chain") { + t.Fatalf("expected no leaf error, got %v", err) + } +} + +func Test_signerFromKeyRefFailureEmptyChainFile(t *testing.T) { + tmpDir := t.TempDir() + ctx := context.Background() + keyFile, certFile, _, _, _, _ := generateCertificateFiles(t, tmpDir, pass("foo")) + + tmpChainFile, err := os.CreateTemp(tmpDir, "cosign_chain_empty.crt") + if err != nil { + t.Fatalf("failed to create temp chain file: %v", err) + } + defer tmpChainFile.Close() + if _, err := tmpChainFile.Write([]byte{}); err != nil { + t.Fatalf("failed to write chain file: %v", err) + } + + _, err = signerFromKeyRef(ctx, certFile, tmpChainFile.Name(), keyFile, pass("foo"), nil) + if err == nil || err.Error() != "no certificates in certificate chain" { + t.Fatalf("expected empty chain error, got %v", err) + } +} From 30bf9a71b2992489f7b5a1badcb90f3b8be30e9d Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Thu, 2 Oct 2025 13:41:01 -0700 Subject: [PATCH 02/12] Deduplicate signer-verifier creation Signed-off-by: Colleen Murphy --- cmd/cosign/cli/attest/attest.go | 11 +++-------- cmd/cosign/cli/attest/attest_blob.go | 11 +++-------- cmd/cosign/cli/sign/sign.go | 21 +++++---------------- cmd/cosign/cli/sign/sign_blob.go | 12 +++--------- cmd/cosign/cli/signcommon/common.go | 23 ++++++++++++++++++----- 5 files changed, 32 insertions(+), 46 deletions(-) diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index b4bc50cd94a..9dbf48841e9 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -176,17 +176,12 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { return ociremote.WriteAttestationNewBundleFormat(digest, bundle, types.CosignSignPredicateType, ociremoteOpts...) } - sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + sv, closeSV, err := signcommon.GetSignerVerifier(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) if err != nil { return fmt.Errorf("getting signer: %w", err) } - if genKey || c.IssueCertificateForExistingKey { - sv, err = signcommon.KeylessSigner(ctx, c.KeyOpts, sv) - if err != nil { - return fmt.Errorf("getting Fulcio signer: %w", err) - } - } - defer sv.Close() + defer closeSV() + wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) dd := cremote.NewDupeDetector(sv) diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index 187d692029d..155b9739582 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -176,17 +176,12 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error return nil } - sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + sv, closeSV, err := signcommon.GetSignerVerifier(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) if err != nil { return fmt.Errorf("getting signer: %w", err) } - if genKey || c.IssueCertificateForExistingKey { - sv, err = signcommon.KeylessSigner(ctx, c.KeyOpts, sv) - if err != nil { - return fmt.Errorf("getting Fulcio signer: %w", err) - } - } - defer sv.Close() + defer closeSV() + wrapped := sigstoredsse.WrapSigner(sv, types.IntotoPayloadType) sig, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 0e405966938..f6e959c0842 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -215,17 +215,11 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt return ociremote.WriteAttestationNewBundleFormat(digest, bundle, types.CosignSignPredicateType, ociremoteOpts...) } - sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) + sv, closeSV, err := signcommon.GetSignerVerifier(ctx, signOpts.Cert, signOpts.CertChain, ko) if err != nil { return fmt.Errorf("getting signer: %w", err) } - if genKey || ko.IssueCertificateForExistingKey { - sv, err = signcommon.KeylessSigner(ctx, ko, sv) - if err != nil { - return fmt.Errorf("getting Fulcio signer: %w", err) - } - } - defer sv.Close() + defer closeSV() wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) @@ -308,17 +302,12 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti } } - sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) + sv, closeSV, err := signcommon.GetSignerVerifier(ctx, signOpts.Cert, signOpts.CertChain, ko) if err != nil { return fmt.Errorf("getting signer: %w", err) } - if genKey || ko.IssueCertificateForExistingKey { - sv, err = signcommon.KeylessSigner(ctx, ko, sv) - if err != nil { - return fmt.Errorf("getting Fulcio signer: %w", err) - } - } - defer sv.Close() + defer closeSV() + dd := cremote.NewDupeDetector(sv) var s icos.Signer diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index 2d3cda17ca9..121f19689f7 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -108,17 +108,11 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string return bundle, nil } - sv, genKey, err := signcommon.SignerFromKeyOpts(ctx, "", "", ko) + sv, closeSV, err := signcommon.GetSignerVerifier(ctx, "", "", ko) if err != nil { - return nil, err - } - if genKey || ko.IssueCertificateForExistingKey { - sv, err = signcommon.KeylessSigner(ctx, ko, sv) - if err != nil { - return nil, fmt.Errorf("getting Fulcio signer: %w", err) - } + return nil, fmt.Errorf("getting signer: %w", err) } - defer sv.Close() + defer closeSV() hashFunction, err := getHashFunction(sv, ko.DefaultLoadOptions) if err != nil { diff --git a/cmd/cosign/cli/signcommon/common.go b/cmd/cosign/cli/signcommon/common.go index 24924aada8c..cbe20126ed8 100644 --- a/cmd/cosign/cli/signcommon/common.go +++ b/cmd/cosign/cli/signcommon/common.go @@ -81,7 +81,7 @@ func GetKeypairAndToken(ctx context.Context, ko options.KeyOpts, cert, certChain var err error if ko.Sk || ko.Slot != "" || ko.KeyRef != "" || cert != "" { - sv, _, err = SignerFromKeyOpts(ctx, cert, certChain, ko) + sv, _, err = signerFromKeyOpts(ctx, cert, certChain, ko) if err != nil { return nil, "", fmt.Errorf("getting signer: %w", err) } @@ -122,8 +122,7 @@ func GetKeypairAndToken(ctx context.Context, ko options.KeyOpts, cert, certChain return keypair, idToken, nil } -// KeylessSigner fetches an identity certificate from Fulcio and returns a SignerVerifier with the returned signing material. -func KeylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) (*SignerVerifier, error) { +func keylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) (*SignerVerifier, error) { var ( k *fulcio.Signer err error @@ -194,8 +193,22 @@ func shouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Refere return true } -// SignerFromKeyOpts generates a SignerVerifier from provided key flags. -func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, bool, error) { +// GetSignerVerifier generates a SignerVerifier from provided key flags. +func GetSignerVerifier(ctx context.Context, cert, certChain string, ko options.KeyOpts) (*SignerVerifier, func(), error) { + sv, genKey, err := signerFromKeyOpts(ctx, cert, certChain, ko) + if err != nil { + return nil, nil, fmt.Errorf("getting signer from opts: %w", err) + } + if genKey || ko.IssueCertificateForExistingKey { + sv, err = keylessSigner(ctx, ko, sv) + if err != nil { + return nil, nil, fmt.Errorf("getting Fulcio signer: %w", err) + } + } + return sv, sv.Close, nil +} + +func signerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, bool, error) { var sv *SignerVerifier var err error genKey := false From 99a2358b92bb3a3b9862ceada60326386e8b542b Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Thu, 2 Oct 2025 15:32:15 -0700 Subject: [PATCH 03/12] Deduplicate timestamp retrieval Signed-off-by: Colleen Murphy --- cmd/cosign/cli/attest/attest.go | 47 +++++++------------- cmd/cosign/cli/attest/attest_blob.go | 66 +++++----------------------- cmd/cosign/cli/sign/sign.go | 26 +++-------- cmd/cosign/cli/sign/sign_blob.go | 46 ++----------------- cmd/cosign/cli/signcommon/common.go | 46 +++++++++++++++++++ 5 files changed, 86 insertions(+), 145 deletions(-) diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index 9dbf48841e9..1321c48f64e 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -30,8 +30,6 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" - tsaclient "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" @@ -199,39 +197,28 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { if sv.Cert != nil { opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) } - var timestampBytes []byte var tsaPayload []byte - if c.KeyOpts.TSAServerURL != "" { - // We need to decide what signature to send to the timestamp authority. - // - // Historically, cosign sent `signedPayload`, which is the entire JSON DSSE - // Envelope. However, when sigstore clients are verifying a bundle they - // will use the DSSE Sig field, so we choose what signature to send to - // the timestamp authority based on our output format. - if c.KeyOpts.NewBundleFormat { - tsaPayload, err = cosign.GetDSSESigBytes(signedPayload) - if err != nil { - return err - } - } else { - tsaPayload = signedPayload - } - tc := tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL) - if c.KeyOpts.TSAClientCert != "" { - tc = tsaclient.NewTSAClientMTLS(c.KeyOpts.TSAServerURL, - c.KeyOpts.TSAClientCACert, - c.KeyOpts.TSAClientCert, - c.KeyOpts.TSAClientKey, - c.KeyOpts.TSAServerName, - ) - } - timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tc) + // We need to decide what signature to send to the timestamp authority. + // + // Historically, cosign sent `signedPayload`, which is the entire JSON DSSE + // Envelope. However, when sigstore clients are verifying a bundle they + // will use the DSSE Sig field, so we choose what signature to send to + // the timestamp authority based on our output format. + if c.KeyOpts.NewBundleFormat { + tsaPayload, err = cosign.GetDSSESigBytes(signedPayload) if err != nil { return err } - bundle := cbundle.TimestampToRFC3161Timestamp(timestampBytes) + } else { + tsaPayload = signedPayload + } + timestampBytes, rfc3161Timestamp, err := signcommon.GetRFC3161Timestamp(tsaPayload, c.KeyOpts) + if err != nil { + return fmt.Errorf("getting timestamp: %w", err) + } - opts = append(opts, static.WithRFC3161Timestamp(bundle)) + if rfc3161Timestamp != nil { + opts = append(opts, static.WithRFC3161Timestamp(rfc3161Timestamp)) } predicateType, err := options.ParsePredicateType(c.PredicateType) diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index 155b9739582..25998540a2c 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -21,7 +21,6 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" - "errors" "fmt" "io" "os" @@ -34,8 +33,6 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" - tsaclient "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" @@ -92,10 +89,6 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error defer cancelFn() } - if c.TSAServerURL != "" && c.RFC3161TimestampPath == "" && !c.NewBundleFormat { - return errors.New("expected either new bundle or an rfc3161-timestamp path when using a TSA server") - } - base := path.Base(artifactPath) var payload []byte @@ -189,57 +182,20 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error return fmt.Errorf("signing: %w", err) } - var rfc3161Timestamp *cbundle.RFC3161Timestamp - var timestampBytes []byte - var tsaPayload []byte - var rekorEntry *models.LogEntryAnon - - if c.KeyOpts.TSAServerURL != "" { - tc := tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL) - if c.TSAClientCert != "" { - tc = tsaclient.NewTSAClientMTLS(c.KeyOpts.TSAServerURL, - c.KeyOpts.TSAClientCACert, - c.KeyOpts.TSAClientCert, - c.KeyOpts.TSAClientKey, - c.KeyOpts.TSAServerName, - ) - } - // We need to decide what signature to send to the timestamp authority. - // - // Historically, cosign sent `sig`, which is the entire JSON DSSE - // Envelope. However, when sigstore clients are verifying a bundle they - // will use the DSSE Sig field, so we choose what signature to send to - // the timestamp authority based on our output format. - if c.NewBundleFormat { - tsaPayload, err = cosign.GetDSSESigBytes(sig) - if err != nil { - return err - } - } else { - tsaPayload = sig - } - timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tc) + // We need to decide what signature to send to the timestamp authority. + // + // Historically, cosign sent `sig`, which is the entire JSON DSSE + // Envelope. However, when sigstore clients are verifying a bundle they + // will use the DSSE Sig field, so we choose what signature to send to + // the timestamp authority based on our output format. + tsaPayload := sig + if c.NewBundleFormat { + tsaPayload, err = cosign.GetDSSESigBytes(sig) if err != nil { return err } - rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes) - // TODO: Consider uploading RFC3161 TS to Rekor - - if rfc3161Timestamp == nil { - return fmt.Errorf("rfc3161 timestamp is nil") - } - - if c.RFC3161TimestampPath != "" { - ts, err := json.Marshal(rfc3161Timestamp) - if err != nil { - return err - } - if err := os.WriteFile(c.RFC3161TimestampPath, ts, 0600); err != nil { - return fmt.Errorf("create RFC3161 timestamp file: %w", err) - } - fmt.Fprintln(os.Stderr, "RFC3161 timestamp bundle written to file ", c.RFC3161TimestampPath) - } } + timestampBytes, _, err := signcommon.GetRFC3161Timestamp(tsaPayload, c.KeyOpts) signer, err := sv.Bytes(ctx) if err != nil { @@ -250,6 +206,8 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error return fmt.Errorf("upload to tlog: %w", err) } signedPayload := cosign.LocalSignedPayload{} + + var rekorEntry *models.LogEntryAnon if shouldUpload { rekorClient, err := rekor.NewClient(c.RekorURL) if err != nil { diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index f6e959c0842..a1c7afb1ef4 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -227,25 +227,13 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt return fmt.Errorf("signing: %w", err) } - var timestampBytes []byte - if ko.TSAServerURL != "" { - tsaPayload, err := cosign.GetDSSESigBytes(signedPayload) - if err != nil { - return err - } - tc := client.NewTSAClient(ko.TSAServerURL) - if ko.TSAClientCert != "" { - tc = client.NewTSAClientMTLS(ko.TSAServerURL, - ko.TSAClientCACert, - ko.TSAClientCert, - ko.TSAClientKey, - ko.TSAServerName, - ) - } - timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tc) - if err != nil { - return err - } + tsaPayload, err := cosign.GetDSSESigBytes(signedPayload) + if err != nil { + return err + } + timestampBytes, _, err := signcommon.GetRFC3161Timestamp(tsaPayload, ko) + if err != nil { + return fmt.Errorf("getting timestamp: %w", err) } signerBytes, err := sv.Bytes(ctx) diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index 121f19689f7..5faa6836092 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -33,8 +33,6 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" internal "github.com/sigstore/cosign/v3/internal/pkg/cosign" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" @@ -143,48 +141,12 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string signedPayload := cosign.LocalSignedPayload{} var rekorEntry *models.LogEntryAnon - var rfc3161Timestamp *cbundle.RFC3161Timestamp - var timestampBytes []byte - if ko.TSAServerURL != "" { - if ko.RFC3161TimestampPath == "" && !ko.NewBundleFormat { - return nil, fmt.Errorf("must use protobuf bundle or set timestamp output path") - } - var err error - if ko.TSAClientCACert == "" && ko.TSAClientCert == "" { // no mTLS params or custom CA - timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClient(ko.TSAServerURL)) - if err != nil { - return nil, err - } - } else { - timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClientMTLS(ko.TSAServerURL, - ko.TSAClientCACert, - ko.TSAClientCert, - ko.TSAClientKey, - ko.TSAServerName, - )) - if err != nil { - return nil, err - } - } - - rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes) - - if rfc3161Timestamp == nil { - return nil, fmt.Errorf("rfc3161 timestamp is nil") - } - - if ko.RFC3161TimestampPath != "" { - ts, err := json.Marshal(rfc3161Timestamp) - if err != nil { - return nil, err - } - if err := os.WriteFile(ko.RFC3161TimestampPath, ts, 0600); err != nil { - return nil, fmt.Errorf("create RFC3161 timestamp file: %w", err) - } - ui.Infof(ctx, "RFC3161 timestamp written to file %s\n", ko.RFC3161TimestampPath) - } + timestampBytes, _, err := signcommon.GetRFC3161Timestamp(sig, ko) + if err != nil { + return nil, fmt.Errorf("getting timestamp: %w", err) } + if shouldUpload { rekorBytes, err := sv.Bytes(ctx) if err != nil { diff --git a/cmd/cosign/cli/signcommon/common.go b/cmd/cosign/cli/signcommon/common.go index cbe20126ed8..bf9279cbe81 100644 --- a/cmd/cosign/cli/signcommon/common.go +++ b/cmd/cosign/cli/signcommon/common.go @@ -19,6 +19,7 @@ import ( "context" "crypto" "crypto/x509" + "encoding/json" "encoding/pem" "errors" "fmt" @@ -32,8 +33,11 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign/privacy" "github.com/sigstore/cosign/v3/internal/auth" "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" + "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" + cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" sigs "github.com/sigstore/cosign/v3/pkg/signature" @@ -389,3 +393,45 @@ func signerFromNewKey() (*SignerVerifier, error) { SignerVerifier: sv, }, nil } + +// GetRFC3161Timestamp fetches an RFC3161 timestamp as raw bytes and as a RFC3161Timestamp object. +// It either returns both objects to be assembled into a bundle by the calling function, +// or writes the formatted timestamp to the provided file path if not using the new bundle format. +func GetRFC3161Timestamp(payload []byte, ko options.KeyOpts) ([]byte, *cbundle.RFC3161Timestamp, error) { + if ko.TSAServerURL == "" { + return nil, nil, nil + } + if ko.RFC3161TimestampPath == "" && !ko.NewBundleFormat { + return nil, nil, fmt.Errorf("expected either new bundle or an rfc3161-timestamp path when using a TSA server") + } + tc := client.NewTSAClient(ko.TSAServerURL) + if ko.TSAClientCert != "" { + tc = client.NewTSAClientMTLS( + ko.TSAServerURL, + ko.TSAClientCACert, + ko.TSAClientCert, + ko.TSAClientKey, + ko.TSAServerName, + ) + } + timestampBytes, err := tsa.GetTimestampedSignature(payload, tc) + if err != nil { + return nil, nil, fmt.Errorf("getting timestamped signature: %w", err) + } + rfc3161Timestamp := cbundle.TimestampToRFC3161Timestamp(timestampBytes) + if rfc3161Timestamp == nil { + return nil, nil, fmt.Errorf("rfc3161 timestamp is nil") + } + if ko.NewBundleFormat || ko.RFC3161TimestampPath == "" { + return timestampBytes, rfc3161Timestamp, nil + } + ts, err := json.Marshal(rfc3161Timestamp) + if err != nil { + return nil, nil, fmt.Errorf("marshalling timestamp: %w", err) + } + if err := os.WriteFile(ko.RFC3161TimestampPath, ts, 0600); err != nil { + return nil, nil, fmt.Errorf("creating RFC3161 timestamp file: %w", err) + } + fmt.Fprintln(os.Stderr, "RFC3161 timestamp written to file ", ko.RFC3161TimestampPath) + return timestampBytes, rfc3161Timestamp, nil +} From 9eb7c47ce68f397974985f3c4fe0a9888ef42a90 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Thu, 2 Oct 2025 17:07:24 -0700 Subject: [PATCH 04/12] Deduplicate rekor upload Signed-off-by: Colleen Murphy --- cmd/cosign/cli/attest/attest.go | 53 +++++++--------------------- cmd/cosign/cli/attest/attest_blob.go | 27 +++++--------- cmd/cosign/cli/sign/sign.go | 18 +++------- cmd/cosign/cli/sign/sign_blob.go | 32 +++++++---------- cmd/cosign/cli/signcommon/common.go | 26 ++++++++++++++ 5 files changed, 64 insertions(+), 92 deletions(-) diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index 1321c48f64e..a4ad1b766a8 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -21,14 +21,12 @@ import ( _ "crypto/sha256" // for `crypto.SHA256` "encoding/json" "fmt" - "os" "time" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" @@ -46,26 +44,6 @@ import ( signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) -type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error) - -func uploadToTlog(ctx context.Context, sv *signcommon.SignerVerifier, rekorURL string, upload tlogUploadFn) (*models.LogEntryAnon, error) { - rekorBytes, err := sv.Bytes(ctx) - if err != nil { - return nil, err - } - - rekorClient, err := rekor.NewClient(rekorURL) - if err != nil { - return nil, err - } - entry, err := upload(rekorClient, rekorBytes) - if err != nil { - return nil, err - } - fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) - return entry, nil -} - // nolint type AttestCommand struct { options.KeyOpts @@ -232,24 +210,21 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { // Add predicateType as manifest annotation opts = append(opts, static.WithAnnotations(predicateTypeAnnotation)) - // Check whether we should be uploading to the transparency log - shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, digest, c.TlogUpload) + signerBytes, err := sv.Bytes(ctx) if err != nil { - return fmt.Errorf("should upload to tlog: %w", err) + return fmt.Errorf("converting signer to bytes: %w", err) } - var rekorEntry *models.LogEntryAnon - if shouldUpload { - rekorEntry, err = uploadToTlog(ctx, sv, c.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - if c.RekorEntryType == "intoto" { - return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) - } else { - return cosign.TLogUploadDSSEEnvelope(ctx, r, signedPayload, b) - } - - }) - if err != nil { - return err + rekorEntry, err := signcommon.UploadToTlog(ctx, c.KeyOpts, digest, c.TlogUpload, signerBytes, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { + if c.RekorEntryType == "intoto" { + return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) + } else { + return cosign.TLogUploadDSSEEnvelope(ctx, r, signedPayload, b) } + }) + if err != nil { + return err + } + if rekorEntry != nil { opts = append(opts, static.WithBundle(cbundle.EntryToBundle(rekorEntry))) } @@ -259,10 +234,6 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { } if c.KeyOpts.NewBundleFormat { - signerBytes, err := sv.Bytes(ctx) - if err != nil { - return err - } pubKey, err := sv.PublicKey() if err != nil { return err diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index 25998540a2c..e7da29d57f2 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -31,13 +31,13 @@ import ( intotov1 "github.com/in-toto/attestation/go/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" "github.com/sigstore/cosign/v3/pkg/types" + rekorclient "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore-go/pkg/sign" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -201,28 +201,19 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error if err != nil { return err } - shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, nil, c.TlogUpload) - if err != nil { - return fmt.Errorf("upload to tlog: %w", err) - } signedPayload := cosign.LocalSignedPayload{} - var rekorEntry *models.LogEntryAnon - if shouldUpload { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return err - } + rekorEntry, err := signcommon.UploadToTlog(ctx, c.KeyOpts, nil, c.TlogUpload, signer, func(r *rekorclient.Rekor, b []byte) (*models.LogEntryAnon, error) { if c.RekorEntryType == "intoto" { - rekorEntry, err = cosign.TLogUploadInTotoAttestation(ctx, rekorClient, sig, signer) + return cosign.TLogUploadInTotoAttestation(ctx, r, sig, b) } else { - rekorEntry, err = cosign.TLogUploadDSSEEnvelope(ctx, rekorClient, sig, signer) - } - - if err != nil { - return err + return cosign.TLogUploadDSSEEnvelope(ctx, r, sig, b) } - fmt.Fprintln(os.Stderr, "tlog entry created with index:", *rekorEntry.LogIndex) + }) + if err != nil { + return err + } + if rekorEntry != nil { signedPayload.Bundle = cbundle.EntryToBundle(rekorEntry) } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index a1c7afb1ef4..0ef11fe4d91 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -46,6 +46,7 @@ import ( ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/walk" "github.com/sigstore/cosign/v3/pkg/types" + rekorclient "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore-go/pkg/sign" "github.com/sigstore/sigstore/pkg/signature/dsse" @@ -241,20 +242,11 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt return err } - var rekorEntry *models.LogEntryAnon - shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) + rekorEntry, err := signcommon.UploadToTlog(ctx, ko, digest, signOpts.TlogUpload, signerBytes, func(r *rekorclient.Rekor, b []byte) (*models.LogEntryAnon, error) { + return cosign.TLogUploadDSSEEnvelope(ctx, r, signedPayload, b) + }) if err != nil { - return fmt.Errorf("should upload to tlog: %w", err) - } - if shouldUpload { - rClient, err := rekor.NewClient(ko.RekorURL) - if err != nil { - return err - } - rekorEntry, err = cosign.TLogUploadDSSEEnvelope(ctx, rClient, signedPayload, signerBytes) - if err != nil { - return err - } + return err } regOpts := signOpts.Registry diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index 5faa6836092..a3e462a1bae 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -30,7 +30,6 @@ import ( "google.golang.org/protobuf/encoding/protojson" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" internal "github.com/sigstore/cosign/v3/internal/pkg/cosign" "github.com/sigstore/cosign/v3/internal/ui" @@ -38,6 +37,7 @@ import ( cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + rekorclient "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore-go/pkg/sign" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -140,27 +140,23 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string digest := payload.Sum(nil) signedPayload := cosign.LocalSignedPayload{} - var rekorEntry *models.LogEntryAnon timestampBytes, _, err := signcommon.GetRFC3161Timestamp(sig, ko) if err != nil { return nil, fmt.Errorf("getting timestamp: %w", err) } - if shouldUpload { - rekorBytes, err := sv.Bytes(ctx) - if err != nil { - return nil, err - } - rekorClient, err := rekor.NewClient(ko.RekorURL) - if err != nil { - return nil, err - } - rekorEntry, err = cosign.TLogUploadWithCustomHash(ctx, rekorClient, sig, &payload, rekorBytes) - if err != nil { - return nil, err - } - ui.Infof(ctx, "tlog entry created with index: %d", *rekorEntry.LogIndex) + signer, err := sv.Bytes(ctx) + if err != nil { + return nil, err + } + rekorEntry, err := signcommon.UploadToTlog(ctx, ko, nil, shouldUpload, signer, func(r *rekorclient.Rekor, b []byte) (*models.LogEntryAnon, error) { + return cosign.TLogUploadWithCustomHash(ctx, r, sig, &payload, b) + }) + if err != nil { + return nil, err + } + if rekorEntry != nil { signedPayload.Bundle = cbundle.EntryToBundle(rekorEntry) } @@ -172,10 +168,6 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string var hint string var rawCert []byte - signer, err := sv.Bytes(ctx) - if err != nil { - return nil, fmt.Errorf("error getting signer: %w", err) - } cert, err := cryptoutils.UnmarshalCertificatesFromPEM(signer) if err != nil || len(cert) == 0 { pubKey, err := sv.PublicKey() diff --git a/cmd/cosign/cli/signcommon/common.go b/cmd/cosign/cli/signcommon/common.go index bf9279cbe81..53928a7bd28 100644 --- a/cmd/cosign/cli/signcommon/common.go +++ b/cmd/cosign/cli/signcommon/common.go @@ -30,6 +30,7 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio/fulcioverifier" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign/privacy" "github.com/sigstore/cosign/v3/internal/auth" "github.com/sigstore/cosign/v3/internal/key" @@ -41,6 +42,8 @@ import ( "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" sigs "github.com/sigstore/cosign/v3/pkg/signature" + rekorclient "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore-go/pkg/sign" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" @@ -435,3 +438,26 @@ func GetRFC3161Timestamp(payload []byte, ko options.KeyOpts) ([]byte, *cbundle.R fmt.Fprintln(os.Stderr, "RFC3161 timestamp written to file ", ko.RFC3161TimestampPath) return timestampBytes, rfc3161Timestamp, nil } + +type tlogUploadFn func(*rekorclient.Rekor, []byte) (*models.LogEntryAnon, error) + +// UploadToTlog uploads an entry to rekor v1 and returns the response from rekor. +func UploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool, rekorBytes []byte, upload tlogUploadFn) (*models.LogEntryAnon, error) { + shouldUpload, err := ShouldUploadToTlog(ctx, ko, ref, tlogUpload) + if err != nil { + return nil, fmt.Errorf("checking upload to tlog: %w", err) + } + if !shouldUpload { + return nil, nil + } + rekorClient, err := rekor.NewClient(ko.RekorURL) + if err != nil { + return nil, fmt.Errorf("creating rekor client: %w", err) + } + entry, err := upload(rekorClient, rekorBytes) + if err != nil { + return nil, fmt.Errorf("uploading to rekor: %w", err) + } + fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) + return entry, nil +} From 160246e081b076020392ac3ef7378fa89b9e53c6 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Fri, 3 Oct 2025 19:05:42 -0700 Subject: [PATCH 05/12] Deduplicate bundle compilation Signed-off-by: Colleen Murphy --- cmd/cosign/cli/attest/attest.go | 103 ++++--------------------- cmd/cosign/cli/attest/attest_blob.go | 89 ++++------------------ cmd/cosign/cli/sign/sign.go | 76 ++----------------- cmd/cosign/cli/signcommon/common.go | 108 +++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 231 deletions(-) diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index a4ad1b766a8..a1f0bae0f04 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -16,7 +16,6 @@ package attest import ( - "bytes" "context" _ "crypto/sha256" // for `crypto.SHA256` "encoding/json" @@ -29,7 +28,6 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote" @@ -37,11 +35,6 @@ import ( ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/cosign/v3/pkg/types" - "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore-go/pkg/sign" - "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) // nolint @@ -131,43 +124,19 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { } if c.SigningConfig != nil { - keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, c.KeyOpts, c.CertPath, c.CertChainPath) - if err != nil { - return fmt.Errorf("getting keypair and token: %w", err) - } - - content := &sign.DSSEData{ - Data: payload, - PayloadType: "application/vnd.in-toto+json", - } - bundle, err := cbundle.SignData(ctx, content, keypair, idToken, c.SigningConfig, c.TrustedMaterial) - if err != nil { - return fmt.Errorf("signing bundle: %w", err) - } - - ociremoteOpts, err := c.RegistryOptions.ClientOpts(ctx) - if err != nil { - return err - } - return ociremote.WriteAttestationNewBundleFormat(digest, bundle, types.CosignSignPredicateType, ociremoteOpts...) + return signcommon.WriteNewBundleWithSigningConfig(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, payload, digest, types.CosignSignPredicateType, "", c.SigningConfig, c.TrustedMaterial, ociremoteOpts...) } - sv, closeSV, err := signcommon.GetSignerVerifier(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + bundleComponents, closeSV, err := signcommon.GetBundleComponents(ctx, c.CertPath, c.CertChainPath, c.KeyOpts, c.NoUpload, c.TlogUpload, payload, digest, c.RekorEntryType) if err != nil { - return fmt.Errorf("getting signer: %w", err) + return fmt.Errorf("getting bundle components: %w", err) } defer closeSV() - wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) - dd := cremote.NewDupeDetector(sv) - - signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return fmt.Errorf("signing: %w", err) - } + sv := bundleComponents.SV if c.NoUpload { - fmt.Println(string(signedPayload)) + fmt.Println(string(bundleComponents.SignedPayload)) return nil } @@ -175,28 +144,9 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { if sv.Cert != nil { opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) } - var tsaPayload []byte - // We need to decide what signature to send to the timestamp authority. - // - // Historically, cosign sent `signedPayload`, which is the entire JSON DSSE - // Envelope. However, when sigstore clients are verifying a bundle they - // will use the DSSE Sig field, so we choose what signature to send to - // the timestamp authority based on our output format. - if c.KeyOpts.NewBundleFormat { - tsaPayload, err = cosign.GetDSSESigBytes(signedPayload) - if err != nil { - return err - } - } else { - tsaPayload = signedPayload - } - timestampBytes, rfc3161Timestamp, err := signcommon.GetRFC3161Timestamp(tsaPayload, c.KeyOpts) - if err != nil { - return fmt.Errorf("getting timestamp: %w", err) - } - if rfc3161Timestamp != nil { - opts = append(opts, static.WithRFC3161Timestamp(rfc3161Timestamp)) + if bundleComponents.RFC3161Timestamp != nil { + opts = append(opts, static.WithRFC3161Timestamp(bundleComponents.RFC3161Timestamp)) } predicateType, err := options.ParsePredicateType(c.PredicateType) @@ -210,45 +160,19 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { // Add predicateType as manifest annotation opts = append(opts, static.WithAnnotations(predicateTypeAnnotation)) - signerBytes, err := sv.Bytes(ctx) - if err != nil { - return fmt.Errorf("converting signer to bytes: %w", err) - } - rekorEntry, err := signcommon.UploadToTlog(ctx, c.KeyOpts, digest, c.TlogUpload, signerBytes, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - if c.RekorEntryType == "intoto" { - return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) - } else { - return cosign.TLogUploadDSSEEnvelope(ctx, r, signedPayload, b) - } - }) - if err != nil { - return err - } - if rekorEntry != nil { - opts = append(opts, static.WithBundle(cbundle.EntryToBundle(rekorEntry))) - } - - sig, err := static.NewAttestation(signedPayload, opts...) - if err != nil { - return err + if bundleComponents.RekorEntry != nil { + opts = append(opts, static.WithBundle(cbundle.EntryToBundle(bundleComponents.RekorEntry))) } if c.KeyOpts.NewBundleFormat { - pubKey, err := sv.PublicKey() - if err != nil { - return err - } - bundleBytes, err := cbundle.MakeNewBundle(pubKey, rekorEntry, payload, signedPayload, signerBytes, timestampBytes) - if err != nil { - return err - } - return ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, predicateType, ociremoteOpts...) + return signcommon.WriteBundle(sv, bundleComponents.RekorEntry, payload, bundleComponents.SignedPayload, bundleComponents.SignerBytes, bundleComponents.TimestampBytes, digest, predicateType, ociremoteOpts...) } // We don't actually need to access the remote entity to attach things to it // so we use a placeholder here. se := ociremote.SignedUnknown(digest, ociremoteOpts...) + dd := cremote.NewDupeDetector(sv) signOpts := []mutate.SignOption{ mutate.WithDupeDetector(dd), mutate.WithRecordCreationTimestamp(c.RecordCreationTimestamp), @@ -259,6 +183,11 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { signOpts = append(signOpts, mutate.WithReplaceOp(ro)) } + sig, err := static.NewAttestation(bundleComponents.SignedPayload, opts...) + if err != nil { + return err + } + // Attach the attestation to the entity. newSE, err := mutate.AttachAttestationToEntity(se, sig, signOpts...) if err != nil { diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index e7da29d57f2..4f0b9748658 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -29,21 +29,15 @@ import ( "strings" "time" + "github.com/google/go-containerregistry/pkg/name" intotov1 "github.com/in-toto/attestation/go/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" - "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/types" - rekorclient "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore-go/pkg/sign" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" - sigstoredsse "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) // nolint @@ -149,72 +143,21 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } if c.SigningConfig != nil { - keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, c.KeyOpts, c.CertPath, c.CertChainPath) - if err != nil { - return fmt.Errorf("getting keypair and token: %w", err) - } - - content := &sign.DSSEData{ - Data: payload, - PayloadType: "application/vnd.in-toto+json", - } - bundle, err := cbundle.SignData(ctx, content, keypair, idToken, c.SigningConfig, c.TrustedMaterial) - if err != nil { - return fmt.Errorf("signing bundle: %w", err) - } - if err := os.WriteFile(c.BundlePath, bundle, 0600); err != nil { - return fmt.Errorf("create bundle file: %w", err) - } - ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath) - return nil + return signcommon.WriteNewBundleWithSigningConfig(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, payload, name.Digest{}, "", c.BundlePath, c.SigningConfig, c.TrustedMaterial, nil) } - sv, closeSV, err := signcommon.GetSignerVerifier(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + bundleComponents, closeSV, err := signcommon.GetBundleComponents(ctx, c.CertPath, c.CertChainPath, c.KeyOpts, false, c.TlogUpload, payload, nil, c.RekorEntryType) if err != nil { - return fmt.Errorf("getting signer: %w", err) + return fmt.Errorf("getting bundle components: %w", err) } defer closeSV() - wrapped := sigstoredsse.WrapSigner(sv, types.IntotoPayloadType) - - sig, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return fmt.Errorf("signing: %w", err) - } - - // We need to decide what signature to send to the timestamp authority. - // - // Historically, cosign sent `sig`, which is the entire JSON DSSE - // Envelope. However, when sigstore clients are verifying a bundle they - // will use the DSSE Sig field, so we choose what signature to send to - // the timestamp authority based on our output format. - tsaPayload := sig - if c.NewBundleFormat { - tsaPayload, err = cosign.GetDSSESigBytes(sig) - if err != nil { - return err - } - } - timestampBytes, _, err := signcommon.GetRFC3161Timestamp(tsaPayload, c.KeyOpts) + sv := bundleComponents.SV - signer, err := sv.Bytes(ctx) - if err != nil { - return err - } signedPayload := cosign.LocalSignedPayload{} - rekorEntry, err := signcommon.UploadToTlog(ctx, c.KeyOpts, nil, c.TlogUpload, signer, func(r *rekorclient.Rekor, b []byte) (*models.LogEntryAnon, error) { - if c.RekorEntryType == "intoto" { - return cosign.TLogUploadInTotoAttestation(ctx, r, sig, b) - } else { - return cosign.TLogUploadDSSEEnvelope(ctx, r, sig, b) - } - }) - if err != nil { - return err - } - if rekorEntry != nil { - signedPayload.Bundle = cbundle.EntryToBundle(rekorEntry) + if bundleComponents.RekorEntry != nil { + signedPayload.Bundle = cbundle.EntryToBundle(bundleComponents.RekorEntry) } if c.BundlePath != "" { @@ -225,13 +168,13 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error return err } - contents, err = cbundle.MakeNewBundle(pubKey, rekorEntry, payload, sig, signer, timestampBytes) + contents, err = cbundle.MakeNewBundle(pubKey, bundleComponents.RekorEntry, payload, bundleComponents.SignedPayload, bundleComponents.SignerBytes, bundleComponents.TimestampBytes) if err != nil { return err } } else { - signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig) - signedPayload.Cert = base64.StdEncoding.EncodeToString(signer) + signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(bundleComponents.SignedPayload) + signedPayload.Cert = base64.StdEncoding.EncodeToString(bundleComponents.SignerBytes) contents, err = json.Marshal(signedPayload) if err != nil { @@ -246,12 +189,12 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } if c.OutputSignature != "" { - if err := os.WriteFile(c.OutputSignature, sig, 0600); err != nil { + if err := os.WriteFile(c.OutputSignature, bundleComponents.SignedPayload, 0600); err != nil { return fmt.Errorf("create signature file: %w", err) } fmt.Fprintf(os.Stderr, "Signature written in %s\n", c.OutputSignature) } else { - fmt.Fprintln(os.Stdout, string(sig)) + fmt.Fprintln(os.Stdout, string(bundleComponents.SignedPayload)) } if c.OutputAttestation != "" { @@ -262,11 +205,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } if c.OutputCertificate != "" { - signer, err := sv.Bytes(ctx) - if err != nil { - return fmt.Errorf("error getting signer: %w", err) - } - cert, err := cryptoutils.UnmarshalCertificatesFromPEM(signer) + cert, err := cryptoutils.UnmarshalCertificatesFromPEM(bundleComponents.SignerBytes) // signer is a certificate if err != nil { fmt.Fprintln(os.Stderr, "Could not output signer certificate. Was a certificate used? ", err) @@ -277,7 +216,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error fmt.Fprintln(os.Stderr, "Could not output signer certificate. Expected a single certificate") return nil } - bts := signer + bts := bundleComponents.SignerBytes if err := os.WriteFile(c.OutputCertificate, bts, 0600); err != nil { return fmt.Errorf("create certificate file: %w", err) } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 0ef11fe4d91..92a51336400 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -39,18 +39,12 @@ import ( "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" - cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/walk" "github.com/sigstore/cosign/v3/pkg/types" - rekorclient "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore-go/pkg/sign" - "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" sigPayload "github.com/sigstore/sigstore/pkg/signature/payload" "google.golang.org/protobuf/encoding/protojson" @@ -192,79 +186,23 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt return err } - if ko.SigningConfig != nil { - keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, signOpts.Cert, signOpts.CertChain) - if err != nil { - return fmt.Errorf("getting keypair and token: %w", err) - } - - content := &sign.DSSEData{ - Data: payload, - PayloadType: "application/vnd.in-toto+json", - } - - bundle, err := cbundle.SignData(ctx, content, keypair, idToken, ko.SigningConfig, ko.TrustedMaterial) - if err != nil { - return fmt.Errorf("signing bundle: %w", err) - } - - regOpts := signOpts.Registry - ociremoteOpts, err := regOpts.ClientOpts(ctx) - if err != nil { - return fmt.Errorf("constructing client options: %w", err) - } - return ociremote.WriteAttestationNewBundleFormat(digest, bundle, types.CosignSignPredicateType, ociremoteOpts...) - } - - sv, closeSV, err := signcommon.GetSignerVerifier(ctx, signOpts.Cert, signOpts.CertChain, ko) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - defer closeSV() - - wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) - signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return fmt.Errorf("signing: %w", err) - } - - tsaPayload, err := cosign.GetDSSESigBytes(signedPayload) - if err != nil { - return err - } - timestampBytes, _, err := signcommon.GetRFC3161Timestamp(tsaPayload, ko) - if err != nil { - return fmt.Errorf("getting timestamp: %w", err) - } - - signerBytes, err := sv.Bytes(ctx) - if err != nil { - return err - } - - rekorEntry, err := signcommon.UploadToTlog(ctx, ko, digest, signOpts.TlogUpload, signerBytes, func(r *rekorclient.Rekor, b []byte) (*models.LogEntryAnon, error) { - return cosign.TLogUploadDSSEEnvelope(ctx, r, signedPayload, b) - }) - if err != nil { - return err - } - regOpts := signOpts.Registry ociremoteOpts, err := regOpts.ClientOpts(ctx) if err != nil { return fmt.Errorf("constructing client options: %w", err) } - pubKey, err := sv.PublicKey() - if err != nil { - return err + if ko.SigningConfig != nil { + return signcommon.WriteNewBundleWithSigningConfig(ctx, ko, signOpts.Cert, signOpts.CertChain, payload, digest, types.CosignSignPredicateType, "", ko.SigningConfig, ko.TrustedMaterial, ociremoteOpts...) } - bundleBytes, err := cbundle.MakeNewBundle(pubKey, rekorEntry, payload, signedPayload, signerBytes, timestampBytes) + bundleComponents, closeSV, err := signcommon.GetBundleComponents(ctx, signOpts.Cert, signOpts.CertChain, ko, false, signOpts.TlogUpload, payload, digest, "dsse") if err != nil { - return err + return fmt.Errorf("getting bundle components: %w", err) } - return ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, types.CosignSignPredicateType, ociremoteOpts...) + defer closeSV() + + return signcommon.WriteBundle(bundleComponents.SV, bundleComponents.RekorEntry, payload, bundleComponents.SignedPayload, bundleComponents.SignerBytes, bundleComponents.TimestampBytes, digest, types.CosignSignPredicateType, ociremoteOpts...) } func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko options.KeyOpts, signOpts options.SignOptions, diff --git a/cmd/cosign/cli/signcommon/common.go b/cmd/cosign/cli/signcommon/common.go index 53928a7bd28..8bda3c6504d 100644 --- a/cmd/cosign/cli/signcommon/common.go +++ b/cmd/cosign/cli/signcommon/common.go @@ -41,12 +41,16 @@ import ( cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" + ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" sigs "github.com/sigstore/cosign/v3/pkg/signature" + "github.com/sigstore/cosign/v3/pkg/types" rekorclient "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore-go/pkg/sign" "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" ) @@ -461,3 +465,107 @@ func UploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, t fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) return entry, nil } + +// WriteBundle compiles a protobuf bundle from components and writes the bundle to the OCI remote layer. +func WriteBundle(sv *SignerVerifier, rekorEntry *models.LogEntryAnon, payload, signedPayload, signerBytes, timestampBytes []byte, digest name.Digest, predicateType string, ociremoteOpts ...ociremote.Option) error { + pubKey, err := sv.PublicKey() + if err != nil { + return err + } + bundleBytes, err := cbundle.MakeNewBundle(pubKey, rekorEntry, payload, signedPayload, signerBytes, timestampBytes) + if err != nil { + return err + } + return ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, predicateType, ociremoteOpts...) +} + +// WriteNewBundleWithSigningConfig uses signing config and trusted root to fetch responses from services for the bundle and writes the bundle to the OCI remote layer. +func WriteNewBundleWithSigningConfig(ctx context.Context, ko options.KeyOpts, cert, certChain string, payload []byte, digest name.Digest, predicateType, bundlePath string, signingConfig *root.SigningConfig, trustedMaterial root.TrustedMaterial, ociremoteOpts ...ociremote.Option) error { + keypair, idToken, err := GetKeypairAndToken(ctx, ko, cert, certChain) + if err != nil { + return fmt.Errorf("getting keypair and token: %w", err) + } + + content := &sign.DSSEData{ + Data: payload, + PayloadType: "application/vnd.in-toto+json", + } + bundle, err := cbundle.SignData(ctx, content, keypair, idToken, signingConfig, trustedMaterial) + if err != nil { + return fmt.Errorf("signing bundle: %w", err) + } + + if bundlePath != "" { + if err := os.WriteFile(bundlePath, bundle, 0600); err != nil { + return fmt.Errorf("creating bundle file: %w", err) + } + ui.Infof(ctx, "Wrote bundle to file %s", bundlePath) + return nil + } + return ociremote.WriteAttestationNewBundleFormat(digest, bundle, predicateType, ociremoteOpts...) +} + +type bundleComponents struct { + SV *SignerVerifier + SignedPayload []byte + TimestampBytes []byte + RFC3161Timestamp *cbundle.RFC3161Timestamp + SignerBytes []byte + RekorEntry *models.LogEntryAnon +} + +// GetBundleComponents fetches data needed to compose the bundle or disparate verification material for any signing command. +func GetBundleComponents(ctx context.Context, cert, certChain string, ko options.KeyOpts, noupload, tlogUpload bool, payload []byte, digest name.Reference, rekorEntryType string) (*bundleComponents, func(), error) { //nolint:revive + bc := &bundleComponents{} + var err error + var closeSV func() + bc.SV, closeSV, err = GetSignerVerifier(ctx, cert, certChain, ko) + if err != nil { + return nil, nil, fmt.Errorf("getting signer: %w", err) + } + wrapped := dsse.WrapSigner(bc.SV, types.IntotoPayloadType) + + bc.SignedPayload, err = wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("signing: %w", err) + } + if noupload { + return bc, closeSV, nil + } + // We need to decide what signature to send to the timestamp authority. + // + // Historically, cosign sent `signedPayload`, which is the entire JSON DSSE + // Envelope. However, when sigstore clients are verifying a bundle they + // will use the DSSE Sig field, so we choose what signature to send to + // the timestamp authority based on our output format. + tsaPayload := bc.SignedPayload + if ko.NewBundleFormat { + tsaPayload, err = cosign.GetDSSESigBytes(bc.SignedPayload) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("getting DSSE signature: %w", err) + } + } + bc.TimestampBytes, bc.RFC3161Timestamp, err = GetRFC3161Timestamp(tsaPayload, ko) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("getting timestamp: %w", err) + } + bc.SignerBytes, err = bc.SV.Bytes(ctx) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("converting signer to bytes: %w", err) + } + bc.RekorEntry, err = UploadToTlog(ctx, ko, digest, tlogUpload, bc.SignerBytes, func(r *rekorclient.Rekor, b []byte) (*models.LogEntryAnon, error) { + if rekorEntryType == "intoto" { + return cosign.TLogUploadInTotoAttestation(ctx, r, bc.SignedPayload, b) + } + return cosign.TLogUploadDSSEEnvelope(ctx, r, bc.SignedPayload, b) + }) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("uploading to tlog: %w", err) + } + return bc, closeSV, nil +} From 10bb68a87fcfd336b47abb3abd8ce0e24cbc200c Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Wed, 8 Oct 2025 15:23:39 -0700 Subject: [PATCH 06/12] Move OCI parsing function to signcommon Signed-off-by: Colleen Murphy --- cmd/cosign/cli/attest/attest.go | 8 +------- cmd/cosign/cli/sign/sign.go | 14 +------------ cmd/cosign/cli/sign/sign_test.go | 25 ------------------------ cmd/cosign/cli/signcommon/common.go | 12 ++++++++++++ cmd/cosign/cli/signcommon/common_test.go | 23 ++++++++++++++++++++++ 5 files changed, 37 insertions(+), 45 deletions(-) diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index a1f0bae0f04..4f17f0ae578 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -22,12 +22,10 @@ import ( "fmt" "time" - "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" - "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote" @@ -73,14 +71,10 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { if err != nil { return err } - ref, err := name.ParseReference(imageRef, c.NameOptions()...) + ref, err := signcommon.ParseOCIReference(ctx, imageRef, c.NameOptions()...) if err != nil { return fmt.Errorf("parsing reference: %w", err) } - if _, ok := ref.(name.Digest); !ok { - msg := fmt.Sprintf(ui.TagReferenceMessage, imageRef) - ui.Warnf(ctx, msg) - } if c.Timeout != 0 { var cancelFn context.CancelFunc diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 92a51336400..443a62fb447 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -62,18 +62,6 @@ func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremot return nil, fmt.Errorf("unknown attachment type %s", attachment) } -// ParseOCIReference parses a string reference to an OCI image into a reference, warning if the reference did not include a digest. -func ParseOCIReference(ctx context.Context, refStr string, opts ...name.Option) (name.Reference, error) { - ref, err := name.ParseReference(refStr, opts...) - if err != nil { - return nil, fmt.Errorf("parsing reference: %w", err) - } - if _, ok := ref.(name.Digest); !ok { - ui.Warnf(ctx, ui.TagReferenceMessage, refStr) - } - return ref, nil -} - // nolint func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string) error { if options.NOf(ko.KeyRef, ko.Sk) > 1 { @@ -109,7 +97,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO } annotations := am.Annotations for _, inputImg := range imgs { - ref, err := ParseOCIReference(ctx, inputImg, regOpts.NameOptions()...) + ref, err := signcommon.ParseOCIReference(ctx, inputImg, regOpts.NameOptions()...) if err != nil { return err } diff --git a/cmd/cosign/cli/sign/sign_test.go b/cmd/cosign/cli/sign/sign_test.go index cd7b73f256b..a8be500276f 100644 --- a/cmd/cosign/cli/sign/sign_test.go +++ b/cmd/cosign/cli/sign/sign_test.go @@ -16,15 +16,11 @@ package sign import ( - "context" "errors" "testing" - "github.com/stretchr/testify/assert" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/internal/ui" ) // TestSignCmdLocalKeyAndSk verifies the SignCmd returns an error @@ -47,24 +43,3 @@ func TestSignCmdLocalKeyAndSk(t *testing.T) { } } } - -func Test_ParseOCIReference(t *testing.T) { - var tests = []struct { - ref string - expectedWarning string - }{ - {"image:bytag", "WARNING: Image reference image:bytag uses a tag, not a digest"}, - {"image:bytag@sha256:abcdef", ""}, - {"image:@sha256:abcdef", ""}, - } - for _, tt := range tests { - stderr := ui.RunWithTestCtx(func(ctx context.Context, _ ui.WriteFunc) { - ParseOCIReference(ctx, tt.ref) - }) - if len(tt.expectedWarning) > 0 { - assert.Contains(t, stderr, tt.expectedWarning, stderr, "bad warning message") - } else { - assert.Empty(t, stderr, "expected no warning") - } - } -} diff --git a/cmd/cosign/cli/signcommon/common.go b/cmd/cosign/cli/signcommon/common.go index 8bda3c6504d..6b1690693b2 100644 --- a/cmd/cosign/cli/signcommon/common.go +++ b/cmd/cosign/cli/signcommon/common.go @@ -569,3 +569,15 @@ func GetBundleComponents(ctx context.Context, cert, certChain string, ko options } return bc, closeSV, nil } + +// ParseOCIReference parses a string reference to an OCI image into a reference, warning if the reference did not include a digest. +func ParseOCIReference(ctx context.Context, refStr string, opts ...name.Option) (name.Reference, error) { + ref, err := name.ParseReference(refStr, opts...) + if err != nil { + return nil, fmt.Errorf("parsing reference: %w", err) + } + if _, ok := ref.(name.Digest); !ok { + ui.Warnf(ctx, ui.TagReferenceMessage, refStr) + } + return ref, nil +} diff --git a/cmd/cosign/cli/signcommon/common_test.go b/cmd/cosign/cli/signcommon/common_test.go index 43da4a128d2..3c17baed0a0 100644 --- a/cmd/cosign/cli/signcommon/common_test.go +++ b/cmd/cosign/cli/signcommon/common_test.go @@ -26,8 +26,10 @@ import ( "github.com/secure-systems-lab/go-securesystemslib/encrypted" "github.com/sigstore/cosign/v3/internal/test" + "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" ) func pass(s string) cosign.PassFunc { @@ -180,3 +182,24 @@ func Test_signerFromKeyRefFailureEmptyChainFile(t *testing.T) { t.Fatalf("expected empty chain error, got %v", err) } } + +func Test_ParseOCIReference(t *testing.T) { + var tests = []struct { + ref string + expectedWarning string + }{ + {"image:bytag", "WARNING: Image reference image:bytag uses a tag, not a digest"}, + {"image:bytag@sha256:abcdef", ""}, + {"image:@sha256:abcdef", ""}, + } + for _, tt := range tests { + stderr := ui.RunWithTestCtx(func(ctx context.Context, _ ui.WriteFunc) { + ParseOCIReference(ctx, tt.ref) + }) + if len(tt.expectedWarning) > 0 { + assert.Contains(t, stderr, tt.expectedWarning, stderr, "bad warning message") + } else { + assert.Empty(t, stderr, "expected no warning") + } + } +} From 818de9aa9a8c6c9bfae83b6575f3c56be22e2094 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Mon, 6 Oct 2025 14:08:57 -0700 Subject: [PATCH 07/12] Make flag compatibility checking consistent Move flag checks when --new-bundle-format is used to a common helper module and have all four verify commands use it. Remove redundant flag checker code. Signed-off-by: Colleen Murphy --- cmd/cosign/cli/verify/common.go | 48 +++++++++++++++++++ cmd/cosign/cli/verify/verify.go | 22 +-------- cmd/cosign/cli/verify/verify_attestation.go | 29 +---------- cmd/cosign/cli/verify/verify_blob.go | 12 ++--- .../cli/verify/verify_blob_attestation.go | 12 ++--- 5 files changed, 60 insertions(+), 63 deletions(-) create mode 100644 cmd/cosign/cli/verify/common.go diff --git a/cmd/cosign/cli/verify/common.go b/cmd/cosign/cli/verify/common.go new file mode 100644 index 00000000000..44fed4e59ce --- /dev/null +++ b/cmd/cosign/cli/verify/common.go @@ -0,0 +1,48 @@ +// Copyright 2025 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package verify + +import ( + "fmt" + "reflect" + + "github.com/sigstore/cosign/v3/pkg/cosign" +) + +// CheckSigstoreBundleUnsupportedOptions checks for incompatible settings on any Verify* command struct when NewBundleFormat is used. +func CheckSigstoreBundleUnsupportedOptions(cmd any, co *cosign.CheckOpts) error { + if !co.NewBundleFormat { + return nil + } + fieldToErr := map[string]string{ + "CertRef": "certificate must be in bundle and may not be provided using --certificate", + "CertChain": "certificate chain must be in bundle and may not be provided using --certificate-chain", + "CARoots": "CA roots/intermediates must be provided using --trusted-root", + "CAIntermedias": "CA roots/intermediates must be provided using --trusted-root", + "TSACertChainPath": "TSA certificate chain path may only be provided using --trusted-root", + "RFC3161TimestampPath": "RFC3161 timestamp may not be provided using --rfc3161-timestamp", + "SigRef": "signature may not be provided using --signature", + "SCTRef": "SCT may not be provided using --sct", + } + v := reflect.ValueOf(cmd) + for f, e := range fieldToErr { + if field := v.FieldByName(f); field.IsValid() && field.String() != "" { + return fmt.Errorf("unsupported: %s when using --new-bundle-format", e) + } + } + if co.TrustedMaterial == nil { + return fmt.Errorf("trusted root is required when using new bundle format") + } + return nil +} diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 4cc76ea88b8..fa4ed09cedf 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -172,22 +172,8 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } } - if co.NewBundleFormat { - if c.CertRef != "" { - return fmt.Errorf("unsupported: certificate may not be provided using --certificate when using --new-bundle-format (cert must be in bundle)") - } - if c.CertChain != "" { - return fmt.Errorf("unsupported: certificate chain may not be provided using --certificate-chain when using --new-bundle-format (cert must be in bundle)") - } - if c.CARoots != "" || c.CAIntermediates != "" { - return fmt.Errorf("unsupported: CA roots/intermediates must be provided using --trusted-root when using --new-bundle-format") - } - if c.TSACertChainPath != "" { - return fmt.Errorf("unsupported: TSA certificate chain path may only be provided using --trusted-root when using --new-bundle-format") - } - if co.TrustedMaterial == nil { - return fmt.Errorf("trusted root is required when using new bundle format") - } + if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { + return err } if c.CheckClaims { @@ -262,10 +248,6 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return fmt.Errorf("initializing piv token verifier: %w", err) } case certRef != "": - if co.NewBundleFormat { - // This shouldn't happen because we already checked for this above in checkSigstoreBundleUnsupportedOptions - return fmt.Errorf("unsupported: certificate reference currently not supported with --new-bundle-format") - } cert, err := loadCertFromFileOrURL(c.CertRef) if err != nil { return err diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index cbcb19e4ef2..2bc4bea4bcd 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -153,13 +153,8 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } } - if co.NewBundleFormat { - if err = checkSigstoreBundleUnsupportedOptions(c); err != nil { - return err - } - if co.TrustedMaterial == nil { - return fmt.Errorf("trusted root is required when using new bundle format") - } + if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { + return err } // Ignore Signed Certificate Timestamp if the flag is set or a key is provided @@ -229,10 +224,6 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("initializing piv token verifier: %w", err) } case c.CertRef != "": - if co.NewBundleFormat { - // This shouldn't happen because we already checked for this above in checkSigstoreBundleUnsupportedOptions - return fmt.Errorf("unsupported: certificate reference currently not supported with --new-bundle-format") - } cert, err := loadCertFromFileOrURL(c.CertRef) if err != nil { return fmt.Errorf("loading certificate from reference: %w", err) @@ -383,19 +374,3 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return nil } - -func checkSigstoreBundleUnsupportedOptions(c *VerifyAttestationCommand) error { - if c.CertRef != "" { - return fmt.Errorf("unsupported: certificate may not be provided using --certificate when using --new-bundle-format (cert must be in bundle)") - } - if c.CertChain != "" { - return fmt.Errorf("unsupported: certificate chain may not be provided using --certificate-chain when using --new-bundle-format (cert must be in bundle)") - } - if c.CARoots != "" || c.CAIntermediates != "" { - return fmt.Errorf("unsupported: CA roots/intermediates must be provided using --trusted-root when using --new-bundle-format") - } - if c.TSACertChainPath != "" { - return fmt.Errorf("unsupported: TSA certificate chain path may only be provided using --trusted-root when using --new-bundle-format") - } - return nil -} diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index dd06cfc5c33..6f7ddca3f74 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -163,15 +163,11 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } } - if co.NewBundleFormat { - if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SigRef, c.SCTRef) > 0 { - return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root") - } - - if co.TrustedMaterial == nil { - return fmt.Errorf("trusted root is required when using new bundle format") - } + if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { + return err + } + if co.NewBundleFormat { bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath) if err != nil { return err diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 84a8aa47eb2..36f8296aa2c 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -221,15 +221,11 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } } - if co.NewBundleFormat { - if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SCTRef) > 0 { - return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root") - } - - if co.TrustedMaterial == nil { - return fmt.Errorf("trusted root is required when using new bundle format") - } + if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { + return err + } + if co.NewBundleFormat { bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath) if err != nil { return err From 621e1dd7c4d53e06ffea7a1697290ebe7bc47c54 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Mon, 6 Oct 2025 15:11:55 -0700 Subject: [PATCH 08/12] Remove duplicate certs setting RootCerts and IntermediateCerts are already set on CheckOpts during loadCertsKeylessVerification. Signed-off-by: Colleen Murphy --- cmd/cosign/cli/verify/verify.go | 47 +++++---------------- cmd/cosign/cli/verify/verify_attestation.go | 11 ----- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index fa4ed09cedf..a60ea11dad5 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -47,7 +47,6 @@ import ( "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/payload" ) @@ -214,9 +213,6 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } } - keyRef := c.KeyRef - certRef := c.CertRef - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) @@ -226,14 +222,13 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } // Keys are optional! - var pubKey signature.Verifier switch { - case keyRef != "": - pubKey, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, c.HashAlgorithm) + case c.KeyRef != "": + co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, c.KeyRef, c.HashAlgorithm) if err != nil { return fmt.Errorf("loading public key: %w", err) } - pkcs11Key, ok := pubKey.(*pkcs11key.Key) + pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) if ok { defer pkcs11Key.Close() } @@ -243,50 +238,29 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return fmt.Errorf("opening piv token: %w", err) } defer sk.Close() - pubKey, err = sk.Verifier() + co.SigVerifier, err = sk.Verifier() if err != nil { return fmt.Errorf("initializing piv token verifier: %w", err) } - case certRef != "": + case c.CertRef != "": cert, err := loadCertFromFileOrURL(c.CertRef) if err != nil { return err } - switch { - case c.CertChain == "" && co.RootCerts == nil: - // If no certChain and no CARoots are passed, the Fulcio root certificate will be used - if co.TrustedMaterial == nil { - 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) - } - } - pubKey, err = cosign.ValidateAndUnpackCert(cert, co) + if c.CertChain == "" { + co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) if err != nil { return err } - case c.CertChain != "": - // Verify certificate with chain + } else { chain, err := loadCertChainFromFileOrURL(c.CertChain) if err != nil { return err } - pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return err - } - case co.RootCerts != nil: - // Verify certificate with root (and if given, intermediate) certificate - pubKey, err = cosign.ValidateAndUnpackCert(cert, co) + co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) if err != nil { return err } - default: - return errors.New("no certificate chain provided to verify certificate") } if c.SCTRef != "" { @@ -297,10 +271,9 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { co.SCT = sct } default: - // Do nothing. Neither keyRef, c.Sk, nor certRef were set - can happen for example when using Fulcio and TSA. + // Do nothing. Neither c.KeyRef, c.Sk, nor c.CertRef were set - can happen for example when using Fulcio and TSA. // For an example see the TestAttachWithRFC3161Timestamp test in test/e2e_test.go. } - co.SigVerifier = pubKey // NB: There are only 2 kinds of verification right now: // 1. You gave us the public key explicitly to verify against so co.SigVerifier is non-nil or, diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 2bc4bea4bcd..8bba2fcd729 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -26,7 +26,6 @@ import ( "strings" "github.com/google/go-containerregistry/pkg/name" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/internal/ui" @@ -230,16 +229,6 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } if c.CertChain == "" { // If no certChain is passed, the Fulcio root certificate will be used - if co.TrustedMaterial == nil { - 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 = cosign.ValidateAndUnpackCert(cert, co) if err != nil { return fmt.Errorf("creating certificate verifier: %w", err) From 044b836f5d9bacd2fff017fa9e0e9d9bb2b338b9 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Mon, 6 Oct 2025 16:02:25 -0700 Subject: [PATCH 09/12] Move loading key to common Move the setting of SigVerifier based on the key ref, key slot, or cert and cert chain, to the common file. For verifying blobs and blob attestations with a certificate instead of a key, we return the cert which is used directly in the options list for verification. For images, the cert and cert chain must be validated and then unpacked into the SigVerifier, where the cosign Verify* functions check its validity by extracting it from the verifier. Signed-off-by: Colleen Murphy --- cmd/cosign/cli/verify/common.go | 63 +++++++++++++++ cmd/cosign/cli/verify/verify.go | 62 +++------------ cmd/cosign/cli/verify/verify_attestation.go | 77 ++++--------------- cmd/cosign/cli/verify/verify_blob.go | 34 ++------ .../cli/verify/verify_blob_attestation.go | 37 ++------- 5 files changed, 100 insertions(+), 173 deletions(-) diff --git a/cmd/cosign/cli/verify/common.go b/cmd/cosign/cli/verify/common.go index 44fed4e59ce..33d5b4f111c 100644 --- a/cmd/cosign/cli/verify/common.go +++ b/cmd/cosign/cli/verify/common.go @@ -14,10 +14,17 @@ package verify import ( + "context" + "crypto" + "crypto/x509" "fmt" "reflect" "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" + "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" + csignature "github.com/sigstore/cosign/v3/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature" ) // CheckSigstoreBundleUnsupportedOptions checks for incompatible settings on any Verify* command struct when NewBundleFormat is used. @@ -46,3 +53,59 @@ func CheckSigstoreBundleUnsupportedOptions(cmd any, co *cosign.CheckOpts) error } return nil } + +// LoadVerifierFromKeyOrCert returns either a signature.Verifier or a certificate from the provided flags to use for verifying an artifact. +// In the case of certain types of keys, it returns a close function that must be called by the calling method. +func LoadVerifierFromKeyOrCert(ctx context.Context, keyRef, slot, certRef, certChain string, hashAlgorithm crypto.Hash, sk, withGetCert bool, co *cosign.CheckOpts) (signature.Verifier, *x509.Certificate, func(), error) { + var sigVerifier signature.Verifier + var err error + switch { + case keyRef != "": + sigVerifier, err = csignature.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm) + if err != nil { + return nil, nil, nil, fmt.Errorf("loading public key: %w", err) + } + pkcs11Key, ok := sigVerifier.(*pkcs11key.Key) + closeSV := func() {} + if ok { + closeSV = pkcs11Key.Close + } + return sigVerifier, nil, closeSV, nil + case sk: + sk, err := pivkey.GetKeyWithSlot(slot) + if err != nil { + return nil, nil, nil, fmt.Errorf("opening piv token: %w", err) + } + sigVerifier, err = sk.Verifier() + if err != nil { + sk.Close() + return nil, nil, nil, fmt.Errorf("initializing piv token verifier: %w", err) + } + return sigVerifier, nil, sk.Close, nil + case certRef != "": + cert, err := loadCertFromFileOrURL(certRef) + if err != nil { + return nil, nil, nil, fmt.Errorf("loading cert: %w", err) + } + if withGetCert { + return nil, cert, func() {}, nil + } + if certChain == "" { + sigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) + if err != nil { + return nil, nil, nil, fmt.Errorf("validating cert: %w", err) + } + return sigVerifier, nil, func() {}, nil + } + chain, err := loadCertChainFromFileOrURL(certChain) + if err != nil { + return nil, nil, nil, fmt.Errorf("loading cert chain: %w", err) + } + sigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + if err != nil { + return nil, nil, nil, fmt.Errorf("validating cert with chain: %w", err) + } + return sigVerifier, nil, func() {}, nil + } + return nil, nil, func() {}, nil +} diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index a60ea11dad5..7e10cc756f3 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -39,8 +39,6 @@ import ( "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/env" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/static" sigs "github.com/sigstore/cosign/v3/pkg/signature" @@ -222,57 +220,19 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } // Keys are optional! - switch { - case c.KeyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, c.KeyRef, c.HashAlgorithm) - if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() - } - case c.Sk: - sk, err := pivkey.GetKeyWithSlot(c.Slot) - if err != nil { - return fmt.Errorf("opening piv token: %w", err) - } - defer sk.Close() - co.SigVerifier, err = sk.Verifier() - if err != nil { - return fmt.Errorf("initializing piv token verifier: %w", err) - } - case c.CertRef != "": - cert, err := loadCertFromFileOrURL(c.CertRef) - if err != nil { - return err - } - if c.CertChain == "" { - co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - return err - } - } else { - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return err - } - } + var closeSV func() + co.SigVerifier, _, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, c.CertChain, c.HashAlgorithm, c.Sk, false, co) + if err != nil { + return fmt.Errorf("loading verifier from key opts: %w", err) + } + defer closeSV() - if c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct + if c.CertRef != "" && c.SCTRef != "" { + sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) + if err != nil { + return fmt.Errorf("reading sct from file: %w", err) } - default: - // Do nothing. Neither c.KeyRef, c.Sk, nor c.CertRef were set - can happen for example when using Fulcio and TSA. - // For an example see the TestAttachWithRFC3161Timestamp test in test/e2e_test.go. + co.SCT = sct } // NB: There are only 2 kinds of verification right now: diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 8bba2fcd729..cc6e1e92ad3 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -32,12 +32,9 @@ import ( "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/cue" "github.com/sigstore/cosign/v3/pkg/cosign/env" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/cosign/rego" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/policy" - sigs "github.com/sigstore/cosign/v3/pkg/signature" "github.com/sigstore/sigstore-go/pkg/root" ) @@ -137,6 +134,9 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } if c.TrustedRootPath != "" { + if !co.NewBundleFormat { + return fmt.Errorf("unsupported: trusted root path currently only supported with --new-bundle-format") + } co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) if err != nil { return fmt.Errorf("loading trusted root: %w", err) @@ -199,69 +199,20 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } } - keyRef := c.KeyRef - // Keys are optional! - switch { - case keyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, c.HashAlgorithm) - if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() - } - case c.Sk: - sk, err := pivkey.GetKeyWithSlot(c.Slot) - if err != nil { - return fmt.Errorf("opening piv token: %w", err) - } - defer sk.Close() - co.SigVerifier, err = sk.Verifier() - if err != nil { - return fmt.Errorf("initializing piv token verifier: %w", err) - } - case c.CertRef != "": - cert, err := loadCertFromFileOrURL(c.CertRef) + var closeSV func() + co.SigVerifier, _, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, c.CertChain, c.HashAlgorithm, c.Sk, false, co) + if err != nil { + return fmt.Errorf("loading verifierfrom key opts: %w", err) + } + defer closeSV() + + if c.CertRef != "" && c.SCTRef != "" { + sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) if err != nil { - return fmt.Errorf("loading certificate from reference: %w", err) - } - if c.CertChain == "" { - // If no certChain is passed, the Fulcio root certificate will be used - co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - return fmt.Errorf("creating certificate verifier: %w", err) - } - } else { - // Verify certificate with chain - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return fmt.Errorf("creating certificate verifier: %w", err) - } + return fmt.Errorf("reading sct from file: %w", err) } - if c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct - } - case c.TrustedRootPath != "": - if !co.NewBundleFormat { - return fmt.Errorf("unsupported: trusted root path currently only supported with --new-bundle-format") - } - - // If a trusted root path is provided, we will use it to verify the bundle. - // Otherwise, the verifier will default to the public good instance. - // co.TrustedMaterial is already loaded from c.TrustedRootPath above, - case c.CARoots != "": - // CA roots + possible intermediates are already loaded into co.RootCerts with the call to - // loadCertsKeylessVerification above. + co.SCT = sct } // NB: There are only 2 kinds of verification right now: diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 6f7ddca3f74..013272ef704 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -38,8 +38,6 @@ import ( "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/bundle" "github.com/sigstore/cosign/v3/pkg/cosign/env" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/oci/static" sigs "github.com/sigstore/cosign/v3/pkg/signature" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" @@ -118,34 +116,13 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } // Keys are optional! + var closeSV func() var cert *x509.Certificate - opts := make([]static.Option, 0) - switch { - case c.KeyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, c.KeyRef, c.HashAlgorithm) - if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() - } - case c.Sk: - sk, err := pivkey.GetKeyWithSlot(c.Slot) - if err != nil { - return fmt.Errorf("opening piv token: %w", err) - } - defer sk.Close() - co.SigVerifier, err = sk.Verifier() - if err != nil { - return fmt.Errorf("loading public key from token: %w", err) - } - case c.CertRef != "": - cert, err = loadCertFromFileOrURL(c.CertRef) - if err != nil { - return err - } + co.SigVerifier, cert, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, "", c.HashAlgorithm, c.Sk, true, co) + if err != nil { + return fmt.Errorf("loading verifier from key opts: %w", err) } + defer closeSV() if c.TrustedRootPath != "" { co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) @@ -240,6 +217,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return err } } + opts := make([]static.Option, 0) if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 36f8296aa2c..2332e787693 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -38,8 +38,6 @@ import ( "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/bundle" "github.com/sigstore/cosign/v3/pkg/cosign/env" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/cosign/v3/pkg/policy" sigs "github.com/sigstore/cosign/v3/pkg/signature" @@ -128,37 +126,13 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } // Keys are optional! + var closeSV func() var cert *x509.Certificate - opts := make([]static.Option, 0) - switch { - case c.KeyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, c.KeyRef, c.HashAlgorithm) - if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() - } - case c.Sk: - sk, err := pivkey.GetKeyWithSlot(c.Slot) - if err != nil { - return fmt.Errorf("opening piv token: %w", err) - } - defer sk.Close() - co.SigVerifier, err = sk.Verifier() - if err != nil { - return fmt.Errorf("loading public key from token: %w", err) - } - case c.CertRef != "": - cert, err = loadCertFromFileOrURL(c.CertRef) - if err != nil { - return err - } - case c.CARoots != "": - // CA roots + possible intermediates are already loaded into co.RootCerts with the call to - // loadCertsKeylessVerification above. + co.SigVerifier, cert, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, "", c.HashAlgorithm, c.Sk, true, co) + if err != nil { + return fmt.Errorf("loading verifier from key opts: %w", err) } + defer closeSV() var h v1.Hash var digest []byte @@ -304,6 +278,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } } + opts := make([]static.Option, 0) if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { From ec8261629e8de4a2a86ce1affed53b936d636281 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Tue, 7 Oct 2025 11:50:57 -0700 Subject: [PATCH 10/12] Deduplicate TUF v1 fetch and rekor client setup Signed-off-by: Colleen Murphy --- cmd/cosign/cli/verify/common.go | 45 +++++++++++++++++++ cmd/cosign/cli/verify/verify.go | 44 ++---------------- cmd/cosign/cli/verify/verify_attestation.go | 45 ++----------------- cmd/cosign/cli/verify/verify_blob.go | 42 ++--------------- .../cli/verify/verify_blob_attestation.go | 42 ++--------------- 5 files changed, 57 insertions(+), 161 deletions(-) diff --git a/cmd/cosign/cli/verify/common.go b/cmd/cosign/cli/verify/common.go index 33d5b4f111c..773f7339ccb 100644 --- a/cmd/cosign/cli/verify/common.go +++ b/cmd/cosign/cli/verify/common.go @@ -20,6 +20,7 @@ import ( "fmt" "reflect" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" @@ -109,3 +110,47 @@ func LoadVerifierFromKeyOrCert(ctx context.Context, keyRef, slot, certRef, certC } return nil, nil, func() {}, nil } + +// SetLegacyClientsAndKeys sets up TSA and rekor clients and keys for TSA, rekor, and CT log. +// It may perform an online fetch of keys, so using trusted root instead of these TUF v1 methos is recommended. +// It takes a CheckOpts as input and modifies it. +func SetLegacyClientsAndKeys(ctx context.Context, ignoreTlog, shouldVerifySCT, keylessVerification bool, rekorURL, tsaCertChain, certChain, caRoots, caIntermediates string, co *cosign.CheckOpts) error { + var err error + if !ignoreTlog && !co.NewBundleFormat && rekorURL != "" { + co.RekorClient, err = rekor.NewClient(rekorURL) + if err != nil { + return fmt.Errorf("creating rekor client: %w", err) + } + } + // If trusted material is set, we don't need to fetch disparate keys. + if co.TrustedMaterial != nil { + return nil + } + if co.UseSignedTimestamps { + tsaCertificates, err := cosign.GetTSACerts(ctx, tsaCertChain, cosign.GetTufTargets) + if err != nil { + return fmt.Errorf("loading TSA certificates: %w", err) + } + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts + } + if !ignoreTlog { + co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) + if err != nil { + return fmt.Errorf("getting rekor public keys: %w", err) + } + } + if shouldVerifySCT { + co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + if err != nil { + return fmt.Errorf("getting ctlog public keys: %w", err) + } + } + if keylessVerification { + if err := loadCertsKeylessVerification(certChain, caRoots, caIntermediates, co); err != nil { + return fmt.Errorf("loading certs for keyless verification: %w", err) + } + } + return nil +} diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 7e10cc756f3..672076ed8d5 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -32,7 +32,6 @@ import ( "github.com/in-toto/in-toto-golang/in_toto" "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" cosignError "github.com/sigstore/cosign/v3/cmd/cosign/errors" "github.com/sigstore/cosign/v3/internal/ui" @@ -177,46 +176,9 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { co.ClaimVerifier = cosign.SimpleClaimVerifier } - // If we are using signed timestamps and there is no trusted root, we need to load the TSA certificates - if co.UseSignedTimestamps && co.TrustedMaterial == nil && !co.NewBundleFormat { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts - } - - if !c.IgnoreTlog && !co.NewBundleFormat { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient - } - if co.TrustedMaterial == nil { - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) - } - } - } - if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } - } - - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) - } + err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + if err != nil { + return fmt.Errorf("setting up clients and keys: %w", err) } // Keys are optional! diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index cc6e1e92ad3..eaf4d86e16c 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -27,7 +27,6 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/cue" @@ -156,47 +155,9 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return err } - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) && !co.NewBundleFormat { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) - } - } - - // If we are using signed timestamps, we need to load the TSA certificates - if co.UseSignedTimestamps && co.TrustedMaterial == nil && !co.NewBundleFormat { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts - } - - if !c.IgnoreTlog && !co.NewBundleFormat { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient - } - if co.TrustedMaterial == nil { - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) - } - } - } - - if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } + err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + if err != nil { + return fmt.Errorf("setting up clients and keys: %w", err) } // Keys are optional! diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 013272ef704..d2fa9116740 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -32,7 +32,6 @@ import ( "strings" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" @@ -184,39 +183,12 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps { return fmt.Errorf("when specifying --use-signed-timestamps or --timestamp-certificate-chain, you must also specify --rfc3161-timestamp-path") } - if co.UseSignedTimestamps && co.TrustedMaterial == nil { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts - } - if !c.IgnoreTlog { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient - } - if co.TrustedMaterial == nil { - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) - } - } + err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + if err != nil { + return fmt.Errorf("setting up clients and keys: %w", err) } - if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } - } opts := make([]static.Option, 0) if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) @@ -310,14 +282,6 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { opts = append(opts, static.WithCertChain(certPEM, chainPEM)) } - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) - } - } - sig, err := base64signature(c.SigRef, c.BundlePath) if err != nil { return err diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 2332e787693..73033e00fbb 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -30,7 +30,6 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" internal "github.com/sigstore/cosign/v3/internal/pkg/cosign" payloadsize "github.com/sigstore/cosign/v3/internal/pkg/cosign/payload/size" "github.com/sigstore/cosign/v3/internal/ui" @@ -229,45 +228,10 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps { return fmt.Errorf("when specifying --use-signed-timestamps or --timestamp-certificate-chain, you must also specify --rfc3161-timestamp-path") } - if co.UseSignedTimestamps && co.TrustedMaterial == nil { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts - } - if !c.IgnoreTlog { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient - } - if co.TrustedMaterial == nil { - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) - } - } - } - if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } - } - - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) - } + err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + if err != nil { + return fmt.Errorf("setting up clients and keys: %w", err) } var encodedSig []byte From 3b862a6e55ecedc38affc0721f4a89a9e8e63e06 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Tue, 7 Oct 2025 14:57:57 -0700 Subject: [PATCH 11/12] Deduplicate trusted material setting Signed-off-by: Colleen Murphy --- cmd/cosign/cli/verify/common.go | 28 +++++++++++++++++++ cmd/cosign/cli/verify/verify.go | 20 ++----------- cmd/cosign/cli/verify/verify_attestation.go | 22 ++------------- cmd/cosign/cli/verify/verify_blob.go | 19 ++----------- .../cli/verify/verify_blob_attestation.go | 19 ++----------- 5 files changed, 40 insertions(+), 68 deletions(-) diff --git a/cmd/cosign/cli/verify/common.go b/cmd/cosign/cli/verify/common.go index 773f7339ccb..546be40761b 100644 --- a/cmd/cosign/cli/verify/common.go +++ b/cmd/cosign/cli/verify/common.go @@ -20,11 +20,15 @@ import ( "fmt" "reflect" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" csignature "github.com/sigstore/cosign/v3/pkg/signature" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/signature" ) @@ -154,3 +158,27 @@ func SetLegacyClientsAndKeys(ctx context.Context, ignoreTlog, shouldVerifySCT, k } return nil } + +// SetTrustedMaterial sets TrustedMaterial on CheckOpts, either from the provided trusted root path or from TUF. +// It does not set TrustedMaterial if the user provided trusted material via other flags or environment variables. +func SetTrustedMaterial(ctx context.Context, trustedRootPath, certChain, caRoots, caIntermediates, tsaCertChainPath string, co *cosign.CheckOpts) error { + var err error + if trustedRootPath != "" { + co.TrustedMaterial, err = root.NewTrustedRootFromPath(trustedRootPath) + if err != nil { + return fmt.Errorf("loading trusted root: %w", err) + } + return nil + } + if options.NOf(certChain, caRoots, caIntermediates, tsaCertChainPath) == 0 && + env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && + env.Getenv(env.VariableSigstoreRootFile) == "" && + env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && + env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { + co.TrustedMaterial, err = cosign.TrustedRoot() + if err != nil { + ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) + } + } + return nil +} diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 672076ed8d5..c05b0006a2b 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -37,12 +37,10 @@ import ( "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/static" sigs "github.com/sigstore/cosign/v3/pkg/signature" "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" - "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature/payload" ) @@ -151,21 +149,9 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } } - if c.TrustedRootPath != "" { - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) - } - } else if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - // don't overrule the user's intentions if they provided their own keys - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } + err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, co) + if err != nil { + return fmt.Errorf("setting trusted material: %w", err) } if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index eaf4d86e16c..f1d3719eeeb 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -30,11 +30,9 @@ import ( "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/cue" - "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/cosign/rego" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/policy" - "github.com/sigstore/sigstore-go/pkg/root" ) // VerifyAttestationCommand verifies a signature on a supplied container image @@ -132,23 +130,9 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } - if c.TrustedRootPath != "" { - if !co.NewBundleFormat { - return fmt.Errorf("unsupported: trusted root path currently only supported with --new-bundle-format") - } - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) - } - } else if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } + err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, co) + if err != nil { + return fmt.Errorf("setting trusted material: %w", err) } if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index d2fa9116740..bfe73b965f0 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -36,11 +36,9 @@ import ( "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/oci/static" sigs "github.com/sigstore/cosign/v3/pkg/signature" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" - "github.com/sigstore/sigstore-go/pkg/root" sgverify "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -123,20 +121,9 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } defer closeSV() - if c.TrustedRootPath != "" { - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) - } - } else if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } + err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, co) + if err != nil { + return fmt.Errorf("setting trusted material: %w", err) } if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 73033e00fbb..8ca38177baf 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -36,12 +36,10 @@ import ( "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/cosign/v3/pkg/policy" sigs "github.com/sigstore/cosign/v3/pkg/signature" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" - "github.com/sigstore/sigstore-go/pkg/root" sgverify "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -178,20 +176,9 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } - if c.TrustedRootPath != "" { - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) - } - } else if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } + err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, co) + if err != nil { + return fmt.Errorf("setting trusted material: %w", err) } if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { From d56549049bed663858991de2515621a43a997554 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Tue, 7 Oct 2025 15:24:55 -0700 Subject: [PATCH 12/12] Move common functions to common.go Signed-off-by: Colleen Murphy --- cmd/cosign/cli/verify/common.go | 281 ++++++++++++++++++++++++++++++++ cmd/cosign/cli/verify/verify.go | 278 ------------------------------- 2 files changed, 281 insertions(+), 278 deletions(-) diff --git a/cmd/cosign/cli/verify/common.go b/cmd/cosign/cli/verify/common.go index 546be40761b..57cecfffc23 100644 --- a/cmd/cosign/cli/verify/common.go +++ b/cmd/cosign/cli/verify/common.go @@ -14,22 +14,32 @@ package verify import ( + "bytes" "context" "crypto" "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" "fmt" + "os" "reflect" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/internal/ui" + "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" + "github.com/sigstore/cosign/v3/pkg/oci" csignature "github.com/sigstore/cosign/v3/pkg/signature" "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/payload" ) // CheckSigstoreBundleUnsupportedOptions checks for incompatible settings on any Verify* command struct when NewBundleFormat is used. @@ -182,3 +192,274 @@ func SetTrustedMaterial(ctx context.Context, trustedRootPath, certChain, caRoots } return nil } + +// PrintVerificationHeader prints boilerplate information after successful verification. +func PrintVerificationHeader(ctx context.Context, imgRef string, co *cosign.CheckOpts, bundleVerified, fulcioVerified bool) { + ui.Infof(ctx, "\nVerification for %s --", imgRef) + ui.Infof(ctx, "The following checks were performed on each of these signatures:") + if co.ClaimVerifier != nil { + if co.Annotations != nil { + ui.Infof(ctx, " - The specified annotations were verified.") + } + ui.Infof(ctx, " - The cosign claims were validated") + } + if bundleVerified { + ui.Infof(ctx, " - Existence of the claims in the transparency log was verified offline") + } else if co.RekorClient != nil { + ui.Infof(ctx, " - The claims were present in the transparency log") + ui.Infof(ctx, " - The signatures were integrated into the transparency log when the certificate was valid") + } + if co.SigVerifier != nil { + ui.Infof(ctx, " - The signatures were verified against the specified public key") + } + if fulcioVerified { + ui.Infof(ctx, " - The code-signing certificate was verified using trusted certificate authority certificates") + } +} + +// PrintVerification logs details about the verification to stdout. +func PrintVerification(ctx context.Context, verified []oci.Signature, output string) { + switch output { + case "text": + for _, sig := range verified { + if cert, err := sig.Cert(); err == nil && cert != nil { + ce := cosign.CertExtensions{Cert: cert} + sub := "" + if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 { + sub = sans[0] + } + ui.Infof(ctx, "Certificate subject: %s", sub) + if issuerURL := ce.GetIssuer(); issuerURL != "" { + ui.Infof(ctx, "Certificate issuer URL: %s", issuerURL) + } + + if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { + ui.Infof(ctx, "GitHub Workflow Trigger: %s", githubWorkflowTrigger) + } + + if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { + ui.Infof(ctx, "GitHub Workflow SHA: %s", githubWorkflowSha) + } + if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { + ui.Infof(ctx, "GitHub Workflow Name: %s", githubWorkflowName) + } + + if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { + ui.Infof(ctx, "GitHub Workflow Repository: %s", githubWorkflowRepository) + } + + if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { + ui.Infof(ctx, "GitHub Workflow Ref: %s", githubWorkflowRef) + } + } + + p, err := sig.Payload() + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching payload: %v", err) + return + } + fmt.Println(string(p)) + } + + default: + var outputKeys []payload.SimpleContainerImage + for _, sig := range verified { + p, err := sig.Payload() + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching payload: %v", err) + return + } + + ss := payload.SimpleContainerImage{} + if err := json.Unmarshal(p, &ss); err != nil { + fmt.Println("error decoding the payload:", err.Error()) + return + } + + if cert, err := sig.Cert(); err == nil && cert != nil { + ce := cosign.CertExtensions{Cert: cert} + if ss.Optional == nil { + ss.Optional = make(map[string]interface{}) + } + sub := "" + if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 { + sub = sans[0] + } + ss.Optional["Subject"] = sub + if issuerURL := ce.GetIssuer(); issuerURL != "" { + ss.Optional["Issuer"] = issuerURL + ss.Optional[cosign.CertExtensionOIDCIssuer] = issuerURL + } + if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowTrigger]] = githubWorkflowTrigger + ss.Optional[cosign.CertExtensionGithubWorkflowTrigger] = githubWorkflowTrigger + } + + if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowSha]] = githubWorkflowSha + ss.Optional[cosign.CertExtensionGithubWorkflowSha] = githubWorkflowSha + } + if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowName]] = githubWorkflowName + ss.Optional[cosign.CertExtensionGithubWorkflowName] = githubWorkflowName + } + + if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowRepository]] = githubWorkflowRepository + ss.Optional[cosign.CertExtensionGithubWorkflowRepository] = githubWorkflowRepository + } + + if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowRef]] = githubWorkflowRef + ss.Optional[cosign.CertExtensionGithubWorkflowRef] = githubWorkflowRef + } + } + if bundle, err := sig.Bundle(); err == nil && bundle != nil { + if ss.Optional == nil { + ss.Optional = make(map[string]interface{}) + } + ss.Optional["Bundle"] = bundle + } + if rfc3161Timestamp, err := sig.RFC3161Timestamp(); err == nil && rfc3161Timestamp != nil { + if ss.Optional == nil { + ss.Optional = make(map[string]interface{}) + } + ss.Optional["RFC3161Timestamp"] = rfc3161Timestamp + } + + outputKeys = append(outputKeys, ss) + } + + b, err := json.Marshal(outputKeys) + if err != nil { + fmt.Println("error when generating the output:", err.Error()) + return + } + + fmt.Printf("\n%s\n", string(b)) + } +} + +func loadCertFromFileOrURL(path string) (*x509.Certificate, error) { + pems, err := blob.LoadFileOrURL(path) + if err != nil { + return nil, err + } + return loadCertFromPEM(pems) +} + +func loadCertFromPEM(pems []byte) (*x509.Certificate, error) { + var out []byte + out, err := base64.StdEncoding.DecodeString(string(pems)) + if err != nil { + // not a base64 + out = pems + } + + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(out) + if err != nil { + return nil, err + } + if len(certs) == 0 { + return nil, errors.New("no certs found in pem file") + } + return certs[0], nil +} + +func loadCertChainFromFileOrURL(path string) ([]*x509.Certificate, error) { + pems, err := blob.LoadFileOrURL(path) + if err != nil { + return nil, err + } + certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(pems)) + if err != nil { + return nil, err + } + return certs, nil +} + +func keylessVerification(keyRef string, sk bool) bool { + if keyRef != "" { + return false + } + if sk { + return false + } + return true +} + +func shouldVerifySCT(ignoreSCT bool, keyRef string, sk bool) bool { + if keyRef != "" { + return false + } + if sk { + return false + } + if ignoreSCT { + return false + } + return true +} + +// loadCertsKeylessVerification loads certificates provided as a certificate chain or CA roots + CA intermediate +// certificate files. If both certChain and caRootsFile are empty strings, the Fulcio roots are loaded. +// +// The co *cosign.CheckOpts is both input and output parameter - it gets updated +// with the root and intermediate certificates needed for verification. +func loadCertsKeylessVerification(certChainFile string, + caRootsFile string, + caIntermediatesFile string, + co *cosign.CheckOpts) error { + var err error + switch { + case certChainFile != "": + chain, err := loadCertChainFromFileOrURL(certChainFile) + if err != nil { + return err + } + co.RootCerts = x509.NewCertPool() + co.RootCerts.AddCert(chain[len(chain)-1]) + if len(chain) > 1 { + co.IntermediateCerts = x509.NewCertPool() + for _, cert := range chain[:len(chain)-1] { + co.IntermediateCerts.AddCert(cert) + } + } + case caRootsFile != "": + caRoots, err := loadCertChainFromFileOrURL(caRootsFile) + if err != nil { + return err + } + co.RootCerts = x509.NewCertPool() + if len(caRoots) > 0 { + for _, cert := range caRoots { + co.RootCerts.AddCert(cert) + } + } + if caIntermediatesFile != "" { + caIntermediates, err := loadCertChainFromFileOrURL(caIntermediatesFile) + if err != nil { + return err + } + if len(caIntermediates) > 0 { + co.IntermediateCerts = x509.NewCertPool() + for _, cert := range caIntermediates { + co.IntermediateCerts.AddCert(cert) + } + } + } + default: + // This performs an online fetch of the Fulcio roots from a TUF repository. + // This is needed for verifying keyless certificates (both online and offline). + 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) + } + } + + return nil +} diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index c05b0006a2b..3607466dfc8 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -16,13 +16,9 @@ package verify import ( - "bytes" "context" "crypto" - "crypto/x509" - "encoding/base64" "encoding/json" - "errors" "flag" "fmt" "os" @@ -30,18 +26,14 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/in-toto/in-toto-golang/in_toto" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" cosignError "github.com/sigstore/cosign/v3/cmd/cosign/errors" - "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/static" sigs "github.com/sigstore/cosign/v3/pkg/signature" "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" - "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature/payload" ) @@ -246,276 +238,6 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return nil } -func PrintVerificationHeader(ctx context.Context, imgRef string, co *cosign.CheckOpts, bundleVerified, fulcioVerified bool) { - ui.Infof(ctx, "\nVerification for %s --", imgRef) - ui.Infof(ctx, "The following checks were performed on each of these signatures:") - if co.ClaimVerifier != nil { - if co.Annotations != nil { - ui.Infof(ctx, " - The specified annotations were verified.") - } - ui.Infof(ctx, " - The cosign claims were validated") - } - if bundleVerified { - ui.Infof(ctx, " - Existence of the claims in the transparency log was verified offline") - } else if co.RekorClient != nil { - ui.Infof(ctx, " - The claims were present in the transparency log") - ui.Infof(ctx, " - The signatures were integrated into the transparency log when the certificate was valid") - } - if co.SigVerifier != nil { - ui.Infof(ctx, " - The signatures were verified against the specified public key") - } - if fulcioVerified { - ui.Infof(ctx, " - The code-signing certificate was verified using trusted certificate authority certificates") - } -} - -// PrintVerification logs details about the verification to stdout -func PrintVerification(ctx context.Context, verified []oci.Signature, output string) { - switch output { - case "text": - for _, sig := range verified { - if cert, err := sig.Cert(); err == nil && cert != nil { - ce := cosign.CertExtensions{Cert: cert} - sub := "" - if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 { - sub = sans[0] - } - ui.Infof(ctx, "Certificate subject: %s", sub) - if issuerURL := ce.GetIssuer(); issuerURL != "" { - ui.Infof(ctx, "Certificate issuer URL: %s", issuerURL) - } - - if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { - ui.Infof(ctx, "GitHub Workflow Trigger: %s", githubWorkflowTrigger) - } - - if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { - ui.Infof(ctx, "GitHub Workflow SHA: %s", githubWorkflowSha) - } - if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { - ui.Infof(ctx, "GitHub Workflow Name: %s", githubWorkflowName) - } - - if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { - ui.Infof(ctx, "GitHub Workflow Repository: %s", githubWorkflowRepository) - } - - if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { - ui.Infof(ctx, "GitHub Workflow Ref: %s", githubWorkflowRef) - } - } - - p, err := sig.Payload() - if err != nil { - fmt.Fprintf(os.Stderr, "Error fetching payload: %v", err) - return - } - fmt.Println(string(p)) - } - - default: - var outputKeys []payload.SimpleContainerImage - for _, sig := range verified { - p, err := sig.Payload() - if err != nil { - fmt.Fprintf(os.Stderr, "Error fetching payload: %v", err) - return - } - - ss := payload.SimpleContainerImage{} - if err := json.Unmarshal(p, &ss); err != nil { - fmt.Println("error decoding the payload:", err.Error()) - return - } - - if cert, err := sig.Cert(); err == nil && cert != nil { - ce := cosign.CertExtensions{Cert: cert} - if ss.Optional == nil { - ss.Optional = make(map[string]interface{}) - } - sub := "" - if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 { - sub = sans[0] - } - ss.Optional["Subject"] = sub - if issuerURL := ce.GetIssuer(); issuerURL != "" { - ss.Optional["Issuer"] = issuerURL - ss.Optional[cosign.CertExtensionOIDCIssuer] = issuerURL - } - if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowTrigger]] = githubWorkflowTrigger - ss.Optional[cosign.CertExtensionGithubWorkflowTrigger] = githubWorkflowTrigger - } - - if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowSha]] = githubWorkflowSha - ss.Optional[cosign.CertExtensionGithubWorkflowSha] = githubWorkflowSha - } - if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowName]] = githubWorkflowName - ss.Optional[cosign.CertExtensionGithubWorkflowName] = githubWorkflowName - } - - if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowRepository]] = githubWorkflowRepository - ss.Optional[cosign.CertExtensionGithubWorkflowRepository] = githubWorkflowRepository - } - - if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowRef]] = githubWorkflowRef - ss.Optional[cosign.CertExtensionGithubWorkflowRef] = githubWorkflowRef - } - } - if bundle, err := sig.Bundle(); err == nil && bundle != nil { - if ss.Optional == nil { - ss.Optional = make(map[string]interface{}) - } - ss.Optional["Bundle"] = bundle - } - if rfc3161Timestamp, err := sig.RFC3161Timestamp(); err == nil && rfc3161Timestamp != nil { - if ss.Optional == nil { - ss.Optional = make(map[string]interface{}) - } - ss.Optional["RFC3161Timestamp"] = rfc3161Timestamp - } - - outputKeys = append(outputKeys, ss) - } - - b, err := json.Marshal(outputKeys) - if err != nil { - fmt.Println("error when generating the output:", err.Error()) - return - } - - fmt.Printf("\n%s\n", string(b)) - } -} - -func loadCertFromFileOrURL(path string) (*x509.Certificate, error) { - pems, err := blob.LoadFileOrURL(path) - if err != nil { - return nil, err - } - return loadCertFromPEM(pems) -} - -func loadCertFromPEM(pems []byte) (*x509.Certificate, error) { - var out []byte - out, err := base64.StdEncoding.DecodeString(string(pems)) - if err != nil { - // not a base64 - out = pems - } - - certs, err := cryptoutils.UnmarshalCertificatesFromPEM(out) - if err != nil { - return nil, err - } - if len(certs) == 0 { - return nil, errors.New("no certs found in pem file") - } - return certs[0], nil -} - -func loadCertChainFromFileOrURL(path string) ([]*x509.Certificate, error) { - pems, err := blob.LoadFileOrURL(path) - if err != nil { - return nil, err - } - certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(pems)) - if err != nil { - return nil, err - } - return certs, nil -} - -func keylessVerification(keyRef string, sk bool) bool { - if keyRef != "" { - return false - } - if sk { - return false - } - return true -} - -func shouldVerifySCT(ignoreSCT bool, keyRef string, sk bool) bool { - if keyRef != "" { - return false - } - if sk { - return false - } - if ignoreSCT { - return false - } - return true -} - -// loadCertsKeylessVerification loads certificates provided as a certificate chain or CA roots + CA intermediate -// certificate files. If both certChain and caRootsFile are empty strings, the Fulcio roots are loaded. -// -// The co *cosign.CheckOpts is both input and output parameter - it gets updated -// with the root and intermediate certificates needed for verification. -func loadCertsKeylessVerification(certChainFile string, - caRootsFile string, - caIntermediatesFile string, - co *cosign.CheckOpts) error { - var err error - switch { - case certChainFile != "": - chain, err := loadCertChainFromFileOrURL(certChainFile) - if err != nil { - return err - } - co.RootCerts = x509.NewCertPool() - co.RootCerts.AddCert(chain[len(chain)-1]) - if len(chain) > 1 { - co.IntermediateCerts = x509.NewCertPool() - for _, cert := range chain[:len(chain)-1] { - co.IntermediateCerts.AddCert(cert) - } - } - case caRootsFile != "": - caRoots, err := loadCertChainFromFileOrURL(caRootsFile) - if err != nil { - return err - } - co.RootCerts = x509.NewCertPool() - if len(caRoots) > 0 { - for _, cert := range caRoots { - co.RootCerts.AddCert(cert) - } - } - if caIntermediatesFile != "" { - caIntermediates, err := loadCertChainFromFileOrURL(caIntermediatesFile) - if err != nil { - return err - } - if len(caIntermediates) > 0 { - co.IntermediateCerts = x509.NewCertPool() - for _, cert := range caIntermediates { - co.IntermediateCerts.AddCert(cert) - } - } - } - default: - // This performs an online fetch of the Fulcio roots from a TUF repository. - // This is needed for verifying keyless certificates (both online and offline). - 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) - } - } - - return nil -} - func transformOutput(verified []oci.Signature, name string) (verifiedOutput []oci.Signature, err error) { for _, v := range verified { dssePayload, err := v.Payload()