From cb58bd3be38944b097f3cc291f810f18230f38f0 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Fri, 18 Oct 2024 13:46:53 -0700 Subject: [PATCH] Upgrade to TUF v2 client with trusted root Use sigstore-go's TUF client to fetch the trusted_root.json from the TUF mirror, if available. Where possible, use sigstore-go's verifiers which natively accept the trusted root as its trusted material. Where there is no trusted root available in TUF or sigstore-go doesn't support a use case, fall back to the sigstore/sigstore TUF v1 client and the existing verifiers in cosign. Signed-off-by: Colleen Murphy --- .../fulcio/fulcioverifier/fulcioverifier.go | 20 +++- cmd/cosign/cli/initialize/init.go | 36 +++++- cmd/cosign/cli/options/key.go | 8 +- cmd/cosign/cli/sign.go | 5 + cmd/cosign/cli/sign/sign.go | 16 ++- cmd/cosign/cli/sign/sign_test.go | 10 +- cmd/cosign/cli/signblob.go | 3 + cmd/cosign/cli/verify/verify.go | 42 +++++-- cmd/cosign/cli/verify/verify_attestation.go | 43 +++++-- cmd/cosign/cli/verify/verify_blob.go | 26 +++- .../cli/verify/verify_blob_attestation.go | 26 +++- cmd/cosign/cli/verify/verify_blob_test.go | 2 + pkg/cosign/env/env.go | 23 ++++ pkg/cosign/tlog.go | 26 +++- pkg/cosign/tlog_test.go | 2 +- pkg/cosign/tuf.go | 106 +++++++++++++++++ pkg/cosign/verify.go | 112 ++++++++++++++---- pkg/cosign/verify_sct.go | 50 ++++---- 18 files changed, 449 insertions(+), 107 deletions(-) create mode 100644 pkg/cosign/tuf.go diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 8646bb298bd..5a74c4e1fee 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -23,6 +23,8 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" ) @@ -32,12 +34,26 @@ func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerV return nil, err } - // Grab the PublicKeys for the CTFE, either from tuf or env. + if ko.TrustedMaterial != nil && len(fs.SCT) == 0 { + // We assume that if a trusted_root.json was found, the fulcio chain was included in it. + // fs.Chain will be ignored as root.VerifySignedCertificateTimestamp relies on the trusted root. + // Detached SCTs cannot be verified with this function. + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(fs.Cert) + if err != nil || len(certs) < 1 { + return nil, fmt.Errorf("unmarshalling SCT from PEM: %w", err) + } + if err := verify.VerifySignedCertificateTimestamp(certs[0], 1, ko.TrustedMaterial); err != nil { + return nil, fmt.Errorf("verifying SCT using trusted root: %w", err) + } + ui.Infof(ctx, "Successfully verified SCT...") + return fs, nil + } + + // There was no trusted_root.json or we need to verify a detached SCT, so grab the PublicKeys for the CTFE, either from tuf or env. pubKeys, err := cosign.GetCTLogPubs(ctx) if err != nil { return nil, fmt.Errorf("getting CTFE public keys: %w", err) } - // verify the sct if err := cosign.VerifySCT(ctx, fs.Cert, fs.Chain, fs.SCT, pubKeys); err != nil { return nil, fmt.Errorf("verifying SCT: %w", err) diff --git a/cmd/cosign/cli/initialize/init.go b/cmd/cosign/cli/initialize/init.go index 158bc0a5f0d..7daed5a75b9 100644 --- a/cmd/cosign/cli/initialize/init.go +++ b/cmd/cosign/cli/initialize/init.go @@ -20,9 +20,14 @@ import ( _ "embed" // To enable the `go:embed` directive. "encoding/json" "fmt" + "os" + "path/filepath" + "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/blob" - "github.com/sigstore/sigstore/pkg/tuf" + tufroot "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" + tufv1 "github.com/sigstore/sigstore/pkg/tuf" ) func DoInitialize(ctx context.Context, root, mirror string) error { @@ -36,11 +41,36 @@ func DoInitialize(ctx context.Context, root, mirror string) error { } } - if err := tuf.Initialize(ctx, mirror, rootFileBytes); err != nil { + opts := tuf.DefaultOptions() + if root != "" { + opts.Root = rootFileBytes + } + if mirror != "" { + opts.RepositoryBaseURL = mirror + } + trustedRoot, err := tufroot.NewLiveTrustedRoot(opts) + if err != nil { + ui.Warnf(ctx, "Could not find trusted_root.json in TUF mirror, falling back to individual targets. It is recommended to update your TUF metadata repository to include trusted_root.json.") + } + // Leave a hint for where the current remote is. Adopted from sigstore/sigstore TUF client. + remote := map[string]string{"remote": opts.RepositoryBaseURL} + remoteBytes, err := json.Marshal(remote) + if err != nil { + return err + } + if err := os.WriteFile(filepath.FromSlash(filepath.Join(opts.CachePath, "remote.json")), remoteBytes, 0o600); err != nil { + return fmt.Errorf("storing remote: %w", err) + } + if trustedRoot != nil { + return nil + } + + // The mirror did not have a trusted_root.json, so initialize the legacy TUF targets. + if err := tufv1.Initialize(ctx, mirror, rootFileBytes); err != nil { return err } - status, err := tuf.GetRootStatus(ctx) + status, err := tufv1.GetRootStatus(ctx) if err != nil { return err } diff --git a/cmd/cosign/cli/options/key.go b/cmd/cosign/cli/options/key.go index 634911fdb59..2cae8d5cdbe 100644 --- a/cmd/cosign/cli/options/key.go +++ b/cmd/cosign/cli/options/key.go @@ -15,7 +15,10 @@ package options -import "github.com/sigstore/cosign/v2/pkg/cosign" +import ( + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore-go/pkg/root" +) type KeyOpts struct { Sk bool @@ -53,4 +56,7 @@ type KeyOpts struct { // Modeled after InsecureSkipVerify in tls.Config, this disables // verifying the SCT. InsecureSkipFulcioVerify bool + + // TrustedMaterial contains trusted metadata for all Sigstore services. It is exclusive with RekorPubKeys, RootCerts, IntermediateCerts, CTLogPubKeys, and the TSA* cert fields. + TrustedMaterial root.TrustedMaterial } diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index a4ae71210f6..c5651cc07c6 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -24,6 +24,7 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v2/pkg/cosign" ) func Sign() *cobra.Command { @@ -103,6 +104,9 @@ race conditions or (worse) malicious tampering. if err != nil { return err } + + trustedMaterial, _ := cosign.TrustedRoot() + ko := options.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, @@ -126,6 +130,7 @@ race conditions or (worse) malicious tampering. TSAServerName: o.TSAServerName, TSAServerURL: o.TSAServerURL, IssueCertificateForExistingKey: o.IssueCertificate, + TrustedMaterial: trustedMaterial, } if err := sign.SignCmd(ro, ko, *o, args); err != nil { if o.Attachment == "" { diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 1289e7b1bb4..5949f9854e3 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -54,6 +54,7 @@ import ( ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" "github.com/sigstore/cosign/v2/pkg/oci/walk" sigs "github.com/sigstore/cosign/v2/pkg/signature" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" @@ -391,7 +392,7 @@ func signerFromSecurityKey(ctx context.Context, keySlot string) (*SignerVerifier }, nil } -func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc) (*SignerVerifier, error) { +func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc, trustedMaterial root.TrustedMaterial) (*SignerVerifier, error) { k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc) if err != nil { return nil, fmt.Errorf("reading key: %w", err) @@ -505,9 +506,14 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin return nil, err } if contains { - pubKeys, err := cosign.GetCTLogPubs(ctx) - if err != nil { - return nil, fmt.Errorf("getting CTLog public keys: %w", err) + var pubKeys any + if trustedMaterial != nil { + pubKeys = trustedMaterial.CTLogs() + } else { + pubKeys, err = cosign.GetCTLogPubs(ctx) + if err != nil { + return nil, fmt.Errorf("getting CTLog public keys: %w", err) + } } var chain []*x509.Certificate chain = append(chain, leafCert) @@ -567,7 +573,7 @@ func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath strin case ko.Sk: sv, err = signerFromSecurityKey(ctx, ko.Slot) case ko.KeyRef != "": - sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc) + sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc, ko.TrustedMaterial) default: genKey = true ui.Infof(ctx, "Generating ephemeral keys...") diff --git a/cmd/cosign/cli/sign/sign_test.go b/cmd/cosign/cli/sign/sign_test.go index 2735741d09e..b34009c2499 100644 --- a/cmd/cosign/cli/sign/sign_test.go +++ b/cmd/cosign/cli/sign/sign_test.go @@ -134,7 +134,7 @@ func Test_signerFromKeyRefSuccess(t *testing.T) { ctx := context.Background() keyFile, certFile, chainFile, privKey, cert, chain := generateCertificateFiles(t, tmpDir, pass("foo")) - signer, err := signerFromKeyRef(ctx, certFile, chainFile, keyFile, pass("foo")) + signer, err := signerFromKeyRef(ctx, certFile, chainFile, keyFile, pass("foo"), nil) if err != nil { t.Fatalf("unexpected error generating signer: %v", err) } @@ -173,17 +173,17 @@ func Test_signerFromKeyRefFailure(t *testing.T) { _, certFile2, chainFile2, _, _, _ := generateCertificateFiles(t, tmpDir2, pass("bar")) // Public keys don't match - _, err := signerFromKeyRef(ctx, certFile2, chainFile2, keyFile, pass("foo")) + _, 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")) + _, 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")) + _, 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) } @@ -203,7 +203,7 @@ func Test_signerFromKeyRefFailureEmptyChainFile(t *testing.T) { t.Fatalf("failed to write chain file: %v", err) } - _, err = signerFromKeyRef(ctx, certFile, tmpChainFile.Name(), keyFile, pass("foo")) + _, 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) } diff --git a/cmd/cosign/cli/signblob.go b/cmd/cosign/cli/signblob.go index e1a2cbf8b0e..bd275a94162 100644 --- a/cmd/cosign/cli/signblob.go +++ b/cmd/cosign/cli/signblob.go @@ -22,6 +22,7 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -68,6 +69,7 @@ func SignBlob() *cobra.Command { if err != nil { return err } + trustedMaterial, _ := cosign.TrustedRoot() ko := options.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, @@ -93,6 +95,7 @@ func SignBlob() *cobra.Command { TSAServerURL: o.TSAServerURL, RFC3161TimestampPath: o.RFC3161TimestampPath, IssueCertificateForExistingKey: o.IssueCertificate, + TrustedMaterial: trustedMaterial, } for _, blob := range args { diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 17fd63e8330..d7a3602ee64 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -37,6 +37,7 @@ import ( "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/pkg/cosign/env" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/oci" @@ -128,6 +129,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return fmt.Errorf("constructing client options: %w", err) } + trustedMaterial, _ := cosign.TrustedRoot() + 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) != "" { + // trusted_root.json was found, but a cert chain was explicitly provided, so don't overrule the user's intentions. + trustedMaterial = nil + } + co := &cosign.CheckOpts{ Annotations: c.Annotations.Annotations, RegistryClientOpts: ociremoteOpts, @@ -144,6 +155,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { IgnoreTlog: c.IgnoreTlog, MaxWorkers: c.MaxWorkers, ExperimentalOCI11: c.ExperimentalOCI11, + TrustedMaterial: trustedMaterial, } if c.CheckClaims { co.ClaimVerifier = cosign.SimpleClaimVerifier @@ -167,11 +179,13 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } co.RekorClient = rekorClient } - // 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 { + // 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 keylessVerification(c.KeyRef, c.Sk) { @@ -184,7 +198,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { certRef := c.CertRef // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { + 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) @@ -221,13 +235,15 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { switch { case c.CertChain == "" && co.RootCerts == nil: // If no certChain and no CARoots are passed, the Fulcio root certificate will be used - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) + 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 err != nil { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 93c27690455..efcaa8dde48 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -31,6 +31,7 @@ import ( "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/cue" + "github.com/sigstore/cosign/v2/pkg/cosign/env" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/cosign/rego" @@ -107,6 +108,17 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("constructing client options: %w", err) } + trustedMaterial, _ := cosign.TrustedRoot() + 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) != "" { + // trusted_root.json was found, but a cert chain was explicitly provided, or environment variables point to the key material, + // so don't overrule the user's intentions. + trustedMaterial = nil + } + co := &cosign.CheckOpts{ RegistryClientOpts: ociremoteOpts, CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, @@ -119,12 +131,13 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, MaxWorkers: c.MaxWorkers, + TrustedMaterial: trustedMaterial, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { + 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) @@ -149,11 +162,13 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } co.RekorClient = rekorClient } - // 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 { + // 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) + } } } @@ -193,13 +208,15 @@ 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 - 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) + 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 { diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 79475c90d80..1fab51330cb 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -34,6 +34,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + "github.com/sigstore/cosign/v2/pkg/cosign/env" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/oci/static" @@ -127,6 +128,16 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return err } + trustedMaterial, _ := cosign.TrustedRoot() + 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) != "" { + // trusted_root.json was found, but a cert chain was explicitly provided, so don't overrule the user's intentions. + trustedMaterial = nil + } + co := &cosign.CheckOpts{ CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, CertGithubWorkflowSha: c.CertGithubWorkflowSHA, @@ -137,6 +148,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + TrustedMaterial: trustedMaterial, } if c.RFC3161TimestampPath != "" && !(c.TSACertChainPath != "" || c.UseSignedTimestamps) { return fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set when using RFC3161 timestamp path") @@ -159,11 +171,13 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } co.RekorClient = rekorClient } - // 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 { + // 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) + } } } @@ -293,7 +307,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { + 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) diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 3f2c33cc63b..2f370a9f120 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -37,6 +37,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + "github.com/sigstore/cosign/v2/pkg/cosign/env" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/oci/static" @@ -113,6 +114,16 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } } + trustedMaterial, _ := cosign.TrustedRoot() + 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) != "" { + // trusted_root.json was found, but a cert chain was explicitly provided, so don't overrule the user's intentions. + trustedMaterial = nil + } + co := &cosign.CheckOpts{ Identities: identities, CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, @@ -123,6 +134,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st IgnoreSCT: c.IgnoreSCT, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + TrustedMaterial: trustedMaterial, } var h v1.Hash if c.CheckClaims { @@ -177,11 +189,13 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } co.RekorClient = rekorClient } - // 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 { + // 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 keylessVerification(c.KeyRef, c.Sk) { @@ -191,7 +205,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { + 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) diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index 6b1b127052c..6b959b9ce2f 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -820,6 +820,7 @@ func makeLocalNewBundle(t *testing.T, sig []byte, digest [32]byte) string { } func TestVerifyBlobCmdWithBundle(t *testing.T) { + t.Setenv("TUF_ROOT", t.TempDir()) keyless := newKeylessStack(t) defer os.RemoveAll(keyless.td) @@ -1333,6 +1334,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { } func TestVerifyBlobCmdInvalidRootCA(t *testing.T) { + t.Setenv("TUF_ROOT", t.TempDir()) keyless := newKeylessStack(t) defer os.RemoveAll(keyless.td) diff --git a/pkg/cosign/env/env.go b/pkg/cosign/env/env.go index 016687acb8e..782f685a9c8 100644 --- a/pkg/cosign/env/env.go +++ b/pkg/cosign/env/env.go @@ -60,6 +60,11 @@ const ( VariableSigstoreIDToken Variable = "SIGSTORE_ID_TOKEN" //nolint:gosec VariableSigstoreTSACertificateFile Variable = "SIGSTORE_TSA_CERTIFICATE_FILE" + // TUF environment variables + VariableTUFRootDir Variable = "TUF_ROOT" + VariableTUFMirror Variable = "TUF_MIRROR" + VariableTUFRootJSON Variable = "TUF_ROOT_JSON" + // Other external environment variables VariableGitHubHost Variable = "GITHUB_HOST" VariableGitHubToken Variable = "GITHUB_TOKEN" //nolint:gosec @@ -145,6 +150,24 @@ var ( Sensitive: false, External: true, }, + VariableTUFMirror: { + Description: "URL of the TUF mirror. Use with TUF_ROOT_JSON to refresh TUF metadata during signing and verification commands. Setting this will cause cosign to attempt to use trusted_root.json if available and will ignore custom TUF metadata.", + Expects: "URL of the TUF mirror", + Sensitive: false, + External: true, + }, + VariableTUFRootDir: { + Description: "path to the TUF cache directory", + Expects: "path fo the TUF cache directory", + Sensitive: false, + External: true, + }, + VariableTUFRootJSON: { + Description: "path to the TUF root.json file used to initialize and update a local TUF repository. Use with TUF_MIRROR to refresh TUF metadata during signing and verification commands. Setting this will cause cosign to attempt to use trusted_root.json if available and will ignore custom TUF metadata.", + Expects: "path to root.json", + Sensitive: false, + External: true, + }, VariableGitHubHost: { Description: "is URL of the GitHub Enterprise instance", Expects: "string with the URL of GitHub Enterprise instance", diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index 83d6f61f179..af716938f01 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -47,6 +47,8 @@ import ( hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" "github.com/sigstore/rekor/pkg/types/intoto" intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tlog" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/tuf" ) @@ -219,7 +221,7 @@ func doUpload(ctx context.Context, rekorClient *client.Rekor, pe models.Proposed if err != nil { return nil, err } - return e, VerifyTLogEntryOffline(ctx, e, rekorPubsFromAPI) + return e, VerifyTLogEntryOffline(ctx, e, rekorPubsFromAPI, nil) } return nil, err } @@ -443,14 +445,15 @@ func FindTlogEntry(ctx context.Context, rekorClient *client.Rekor, // VerifyTLogEntryOffline verifies a TLog entry against a map of trusted rekorPubKeys indexed // by log id. -func VerifyTLogEntryOffline(ctx context.Context, e *models.LogEntryAnon, rekorPubKeys *TrustedTransparencyLogPubKeys) error { +func VerifyTLogEntryOffline(ctx context.Context, e *models.LogEntryAnon, rekorPubKeys *TrustedTransparencyLogPubKeys, trustedMaterial root.TrustedMaterial) error { if e.Verification == nil || e.Verification.InclusionProof == nil { return errors.New("inclusion proof not provided") } - if rekorPubKeys == nil || rekorPubKeys.Keys == nil { + if trustedMaterial == nil && (rekorPubKeys == nil || rekorPubKeys.Keys == nil) { return errors.New("no trusted rekor public keys provided") } + // Make sure all the rekorPubKeys are ecsda.PublicKeys for k, v := range rekorPubKeys.Keys { if _, ok := v.PubKey.(*ecdsa.PublicKey); !ok { @@ -478,6 +481,23 @@ func VerifyTLogEntryOffline(ctx context.Context, e *models.LogEntryAnon, rekorPu } // Verify rekor's signature over the SET. + if trustedMaterial != nil { + logID, err := hex.DecodeString(*e.LogID) + if err != nil { + return fmt.Errorf("decoding log ID: %w", err) + } + entry, err := tlog.NewEntry(entryBytes, *e.IntegratedTime, *e.LogIndex, logID, e.Verification.SignedEntryTimestamp, e.Verification.InclusionProof) + if err != nil { + return fmt.Errorf("converting tlog entry: %w", err) + } + if err := tlog.VerifySET(entry, trustedMaterial.RekorLogs()); err != nil { + return fmt.Errorf("verifying SET offline: %w", err) + } + return nil + } + + // No trusted root available, so verify the SET with legacy TUF metadata: + payload := bundle.RekorPayload{ Body: e.Body, IntegratedTime: *e.IntegratedTime, diff --git a/pkg/cosign/tlog_test.go b/pkg/cosign/tlog_test.go index 55bd76bb9f6..c3829c32326 100644 --- a/pkg/cosign/tlog_test.go +++ b/pkg/cosign/tlog_test.go @@ -173,7 +173,7 @@ func TestVerifyTLogEntryOfflineFailsWithInvalidPublicKey(t *testing.T) { t.Fatalf("failed to add RSA key to transparency log public keys: %v", err) } - err = VerifyTLogEntryOffline(context.Background(), &models.LogEntryAnon{Verification: &models.LogEntryAnonVerification{InclusionProof: &models.InclusionProof{}}}, &rekorPubKeys) + err = VerifyTLogEntryOffline(context.Background(), &models.LogEntryAnon{Verification: &models.LogEntryAnonVerification{InclusionProof: &models.InclusionProof{}}}, &rekorPubKeys, nil) if err == nil { t.Fatal("Wanted error got none") } diff --git a/pkg/cosign/tuf.go b/pkg/cosign/tuf.go new file mode 100644 index 00000000000..f46af34f67a --- /dev/null +++ b/pkg/cosign/tuf.go @@ -0,0 +1,106 @@ +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/sigstore/cosign/v2/pkg/cosign/env" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" +) + +func TrustedRoot() (root.TrustedMaterial, error) { + opts, err := setTUFOpts() + if err != nil { + return nil, fmt.Errorf("error setting TUF options: %w", err) + } + return root.NewLiveTrustedRoot(opts) +} + +// setTUFOpts sets the TUF cache directory, the mirror URL, and the root.json in the TUF options. +// The cache directory is provided by the user as an environment variable TUF_ROOT, or the default $HOME/.sigstore/root is used. +// The mirror URL is provided by the user as an environment variable TUF_MIRROR. If not overridden by the user, the value set during `cosign initialize` in remote.json in the cache directory is used. +// If the mirror happens to be the sigstore.dev production TUF CDN, the options are returned since it is safe to use all the default settings. +// If the mirror is a custom mirror, we try to find a cached root.json. We must not use the default embedded root.json. +// If the TUF options cannot be found through these steps, the caller should not try to use this TUF client to fetch the trusted root and should instead fall back to the legacy TUF client to fetch individual trusted keys. +func setTUFOpts() (*tuf.Options, error) { + opts := tuf.DefaultOptions() + if tufCacheDir := env.Getenv(env.VariableTUFRootDir); tufCacheDir != "" { //nolint:forbidigo + opts.CachePath = tufCacheDir + } + err := setTUFMirror(opts) + if err != nil { + return nil, fmt.Errorf("error setting TUF mirror: %w", err) + } + if opts.RepositoryBaseURL == tuf.DefaultMirror { + // Using the default mirror, so just use the embedded root.json. + return opts, nil + } + err = setTUFRootJSON(opts) + if err != nil { + return nil, fmt.Errorf("error setting root: %w", err) + } + return opts, nil +} + +func setTUFMirror(opts *tuf.Options) error { + if tufMirror := env.Getenv(env.VariableTUFMirror); tufMirror != "" { //nolint:forbidigo + opts.RepositoryBaseURL = tufMirror + return nil + } + // try using the mirror set by `cosign initialize` + cachedRemote := filepath.Join(opts.CachePath, "remote.json") + remoteBytes, err := os.ReadFile(cachedRemote) + if errors.Is(err, os.ErrNotExist) { + return nil // `cosign initialize` wasn't run, so use the default + } + if err != nil { + return fmt.Errorf("error reading remote.json: %w", err) + } + remote := make(map[string]string) + err = json.Unmarshal(remoteBytes, &remote) + if err != nil { + return fmt.Errorf("error unmarshalling remote.json: %w", err) + } + opts.RepositoryBaseURL = remote["mirror"] + return nil +} + +func setTUFRootJSON(opts *tuf.Options) error { + // TUF root set by TUF_ROOT_JSON + if tufRootJSON := env.Getenv(env.VariableTUFRootJSON); tufRootJSON != "" { //nolint:forbidigo + rootJSONBytes, err := os.ReadFile(tufRootJSON) + if err != nil { + return fmt.Errorf("error reading root.json given by TUF_ROOT_JSON") + } + opts.Root = rootJSONBytes + return nil + } + // Look for cached root.json + cachedRootJSON := filepath.Join(opts.CachePath, tuf.URLToPath(opts.RepositoryBaseURL), "root.json") + if _, err := os.Stat(cachedRootJSON); !os.IsNotExist(err) { + rootJSONBytes, err := os.ReadFile(cachedRootJSON) + if err != nil { + return fmt.Errorf("error reading cached root.json") + } + opts.Root = rootJSONBytes + } + return fmt.Errorf("could not find cached root.json") +} diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3ab5d76026a..7745b88a75f 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -65,6 +65,9 @@ import ( intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" intoto_v002 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.2" rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tlog" + "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/dsse" @@ -94,6 +97,9 @@ type CheckOpts struct { // ClaimVerifier, if provided, verifies claims present in the oci.Signature. ClaimVerifier func(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}) error + // TrustedMaterial contains trusted metadata for all Sigstore services. It is exclusive with RekorPubKeys, RootCerts, IntermediateCerts, CTLogPubKeys, and the TSA* cert fields. + TrustedMaterial root.TrustedMaterial + // RekorClient, if set, is used to make online tlog calls use to verify signatures and public keys. RekorClient *client.Rekor // RekorPubKeys, if set, is used to validate signatures on log entries from @@ -248,11 +254,27 @@ func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpt } // Now verify the cert, then the signature. - chains, err := TrustedCert(cert, co.RootCerts, intermediateCerts) + shouldVerifyEmbeddedSCT := !co.IgnoreSCT + contains, err := ContainsSCT(cert.Raw) if err != nil { return nil, err } + shouldVerifyEmbeddedSCT = shouldVerifyEmbeddedSCT && contains + // If trusted root is available and the SCT is embedded, use the verifiers from sigstore-go (preferred). + var chains [][]*x509.Certificate + if co.TrustedMaterial != nil && shouldVerifyEmbeddedSCT { + if err = verify.VerifyLeafCertificate(cert.NotBefore, cert, co.TrustedMaterial); err != nil { + return nil, err + } + } else { + // If the trusted root is not available, OR if the SCT is detached, use the verifiers from cosign (legacy). + // The certificate chains will be needed for the legacy SCT verifiers, which is why we can't use sigstore-go. + chains, err = TrustedCert(cert, co.RootCerts, intermediateCerts) + if err != nil { + return nil, err + } + } err = CheckCertificatePolicy(cert, co) if err != nil { @@ -263,15 +285,20 @@ func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpt if co.IgnoreSCT { return verifier, nil } - contains, err := ContainsSCT(cert.Raw) - if err != nil { - return nil, err - } if !contains && len(co.SCT) == 0 { return nil, &VerificationFailure{ fmt.Errorf("certificate does not include required embedded SCT and no detached SCT was set"), } } + + // If trusted root is available and the SCT is embedded, use the verifiers from sigstore-go (preferred). + if co.TrustedMaterial != nil && contains { + if err := verify.VerifySignedCertificateTimestamp(cert, 1, co.TrustedMaterial); err != nil { + return nil, err + } + return verifier, nil + } + // handle if chains has more than one chain - grab first and print message if len(chains) > 1 { fmt.Fprintf(os.Stderr, "**Info** Multiple valid certificate chains found. Selecting the first to verify the SCT.\n") @@ -280,22 +307,22 @@ func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpt if err := VerifyEmbeddedSCT(context.Background(), chains[0], co.CTLogPubKeys); err != nil { return nil, err } - } else { - chain := chains[0] - if len(chain) < 2 { - return nil, errors.New("certificate chain must contain at least a certificate and its issuer") - } - certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) - if err != nil { - return nil, err - } - chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:]) - if err != nil { - return nil, err - } - if err := VerifySCT(context.Background(), certPEM, chainPEM, co.SCT, co.CTLogPubKeys); err != nil { - return nil, err - } + return verifier, nil + } + chain := chains[0] + if len(chain) < 2 { + return nil, errors.New("certificate chain must contain at least a certificate and its issuer") + } + certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) + if err != nil { + return nil, err + } + chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:]) + if err != nil { + return nil, err + } + if err := VerifySCT(context.Background(), certPEM, chainPEM, co.SCT, co.CTLogPubKeys); err != nil { + return nil, err } return verifier, nil @@ -433,7 +460,7 @@ func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certif return ValidateAndUnpackCert(cert, co) } -func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedTransparencyLogPubKeys, +func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedTransparencyLogPubKeys, trustedMaterial root.TrustedMaterial, sig oci.Signature, pem []byte) (*models.LogEntryAnon, error) { b64sig, err := sig.Base64Signature() if err != nil { @@ -457,7 +484,7 @@ func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys * entryVerificationErrs := make([]string, 0) for _, e := range tlogEntries { entry := e - if err := VerifyTLogEntryOffline(ctx, &entry, rekorPubKeys); err != nil { + if err := VerifyTLogEntryOffline(ctx, &entry, rekorPubKeys, trustedMaterial); err != nil { entryVerificationErrs = append(entryVerificationErrs, err.Error()) continue } @@ -704,7 +731,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, return false, err } - e, err := tlogValidateEntry(ctx, co.RekorClient, co.RekorPubKeys, sig, pemBytes) + e, err := tlogValidateEntry(ctx, co.RekorClient, co.RekorPubKeys, co.TrustedMaterial, sig, pemBytes) if err != nil { return false, err } @@ -1060,9 +1087,22 @@ func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) { return false, nil } - if co.RekorPubKeys == nil || co.RekorPubKeys.Keys == nil { + if co.TrustedMaterial == nil && (co.RekorPubKeys == nil || co.RekorPubKeys.Keys == nil) { return false, errors.New("no trusted rekor public keys provided") } + + if co.TrustedMaterial != nil { + payload := bundle.Payload + body, _ := base64.StdEncoding.DecodeString(payload.Body.(string)) + entry, err := tlog.NewEntry(body, payload.IntegratedTime, payload.LogIndex, []byte(payload.LogID), bundle.SignedEntryTimestamp, nil) + if err != nil { + return false, fmt.Errorf("converting tlog entry: %w", err) + } + if err := tlog.VerifySET(entry, co.TrustedMaterial.RekorLogs()); err != nil { + return false, fmt.Errorf("verifying bundle with trusted root: %w", err) + } + return true, nil + } // Make sure all the rekorPubKeys are ecsda.PublicKeys for k, v := range co.RekorPubKeys.Keys { if _, ok := v.PubKey.(*ecdsa.PublicKey); !ok { @@ -1153,6 +1193,28 @@ func VerifyRFC3161Timestamp(sig oci.Signature, co *CheckOpts) (*timestamp.Timest tsBytes = rawSig } + if co.TrustedMaterial != nil { + for _, ca := range co.TrustedMaterial.TimestampingAuthorities() { + ts, err := tsaverification.VerifyTimestampResponse(ts.SignedRFC3161Timestamp, bytes.NewReader(tsBytes), + tsaverification.VerifyOpts{ + TSACertificate: ca.Leaf, + Intermediates: ca.Intermediates, + Roots: []*x509.Certificate{ca.Root}, + }) + if err != nil { + continue + } + if !ca.ValidityPeriodStart.IsZero() && ts.Time.Before(ca.ValidityPeriodStart) { + continue + } + if !ca.ValidityPeriodEnd.IsZero() && ts.Time.After(ca.ValidityPeriodEnd) { + continue + } + return ts, nil + } + return nil, fmt.Errorf("unable to verify signed timestamps with trusted root") + } + return tsaverification.VerifyTimestampResponse(ts.SignedRFC3161Timestamp, bytes.NewReader(tsBytes), tsaverification.VerifyOpts{ TSACertificate: co.TSACertificate, diff --git a/pkg/cosign/verify_sct.go b/pkg/cosign/verify_sct.go index 1b904c2c4fd..df435d17257 100644 --- a/pkg/cosign/verify_sct.go +++ b/pkg/cosign/verify_sct.go @@ -16,20 +16,19 @@ package cosign import ( "context" + "crypto" "crypto/x509" "encoding/hex" "encoding/json" - "errors" "fmt" - "os" ct "github.com/google/certificate-transparency-go" ctx509 "github.com/google/certificate-transparency-go/x509" "github.com/google/certificate-transparency-go/x509util" "github.com/sigstore/cosign/v2/pkg/cosign/fulcioverifier/ctutil" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/tuf" ) // ContainsSCT checks if the certificate contains embedded SCTs. cert can either be @@ -45,14 +44,23 @@ func ContainsSCT(cert []byte) (bool, error) { return false, nil } -func getCTPublicKey(sct *ct.SignedCertificateTimestamp, - pubKeys *TrustedTransparencyLogPubKeys) (*TransparencyLogPubKey, error) { +func getCTPublicKey(sct *ct.SignedCertificateTimestamp, pubKeys any) (crypto.PublicKey, error) { keyID := hex.EncodeToString(sct.LogID.KeyID[:]) - pubKeyMetadata, ok := pubKeys.Keys[keyID] - if !ok { - return nil, errors.New("ctfe public key not found for payload. Check your TUF root (see cosign initialize) or set a custom key with env var SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") + switch p := pubKeys.(type) { + case *TrustedTransparencyLogPubKeys: + pubKeyMetadata, ok := p.Keys[keyID] + if !ok { + return nil, fmt.Errorf("ctfe public key not found for payload. Check your TUF root (see cosign initialize) or set a custom key with env var SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") + } + return &pubKeyMetadata.PubKey, nil + case map[string]*root.TransparencyLog: + pubKeyMetadata, ok := p[keyID] + if !ok { + return nil, fmt.Errorf("ctfe public key not found for payload. Check your TUF root (see cosign initialize) or set a custom key with env var SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") + } + return pubKeyMetadata.PublicKey, nil } - return &pubKeyMetadata, nil + return nil, fmt.Errorf("invalid pubkey type: %T", pubKeys) } // VerifySCT verifies SCTs against the Fulcio CT log public key. @@ -71,9 +79,9 @@ func getCTPublicKey(sct *ct.SignedCertificateTimestamp, // By default the public keys comes from TUF, but you can override this for test // purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using // an alternate, the file can be PEM, or DER format. -func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *TrustedTransparencyLogPubKeys) error { - if pubKeys == nil || len(pubKeys.Keys) == 0 { - return errors.New("none of the CTFE keys have been found") +func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys any) error { + if pubKeys == nil { + return fmt.Errorf("none of the CTFE keys have been found") } // parse certificate and chain @@ -86,7 +94,7 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru return err } if len(certChain) == 0 { - return errors.New("no certificate chain found") + return fmt.Errorf("no certificate chain found") } // fetch embedded SCT if present @@ -96,7 +104,7 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru } // SCT must be either embedded or in header if len(embeddedSCTs) == 0 && len(rawSCT) == 0 { - return errors.New("no SCT found") + return fmt.Errorf("no SCT found") } // check SCT embedded in certificate @@ -106,13 +114,10 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru if err != nil { return err } - err = ctutil.VerifySCT(pubKeyMetadata.PubKey, []*ctx509.Certificate{cert, certChain[0]}, sct, true) + err = ctutil.VerifySCT(pubKeyMetadata, []*ctx509.Certificate{cert, certChain[0]}, sct, true) if err != nil { return fmt.Errorf("error verifying embedded SCT: %w", err) } - if pubKeyMetadata.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified embedded SCT using an expired verification key\n") - } } return nil } @@ -130,20 +135,17 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru if err != nil { return err } - err = ctutil.VerifySCT(pubKeyMetadata.PubKey, []*ctx509.Certificate{cert}, sct, false) + err = ctutil.VerifySCT(pubKeyMetadata, []*ctx509.Certificate{cert}, sct, false) if err != nil { return fmt.Errorf("error verifying SCT") } - if pubKeyMetadata.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") - } return nil } // VerifyEmbeddedSCT verifies an embedded SCT in a certificate. -func VerifyEmbeddedSCT(ctx context.Context, chain []*x509.Certificate, pubKeys *TrustedTransparencyLogPubKeys) error { +func VerifyEmbeddedSCT(ctx context.Context, chain []*x509.Certificate, pubKeys any) error { if len(chain) < 2 { - return errors.New("certificate chain must contain at least a certificate and its issuer") + return fmt.Errorf("certificate chain must contain at least a certificate and its issuer") } certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) if err != nil {