diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index de91d9229a8..a91510dbe16 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -34,7 +34,6 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" cosignError "github.com/sigstore/cosign/v2/cmd/cosign/errors" - "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa" "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" @@ -136,29 +135,13 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { co.ClaimVerifier = cosign.SimpleClaimVerifier } - if c.TSACertChainPath != "" { - _, err := os.Stat(c.TSACertChainPath) - if err != nil { - return fmt.Errorf("unable to open timestamp certificate chain file: %w", err) - } - // TODO: Add support for TUF certificates. - pemBytes, err := os.ReadFile(filepath.Clean(c.TSACertChainPath)) - if err != nil { - return fmt.Errorf("error reading certification chain path file: %w", err) - } - - leaves, intermediates, roots, err := tsa.SplitPEMCertificateChain(pemBytes) - if err != nil { - return fmt.Errorf("error splitting certificates: %w", err) - } - if len(leaves) > 1 { - return fmt.Errorf("certificate chain must contain at most one TSA certificate") - } - if len(leaves) == 1 { - co.TSACertificate = leaves[0] - } - co.TSAIntermediateCertificates = intermediates - co.TSARootCertificates = roots + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath) + if err != nil { + ui.Warnf(ctx, fmt.Sprintf("cannot load tsa certificates: %s", err.Error())) + } else { + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCert } if !c.IgnoreTlog { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index a9ff6dd9982..ba72b0eac82 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -28,7 +28,6 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" - "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa" "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/cue" @@ -118,30 +117,15 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } } - if c.TSACertChainPath != "" { - _, err := os.Stat(c.TSACertChainPath) - if err != nil { - return fmt.Errorf("unable to open timestamp certificate chain file '%s: %w", c.TSACertChainPath, err) - } - // TODO: Add support for TUF certificates. - pemBytes, err := os.ReadFile(filepath.Clean(c.TSACertChainPath)) - if err != nil { - return fmt.Errorf("error reading certification chain path file: %w", err) - } - - leaves, intermediates, roots, err := tsa.SplitPEMCertificateChain(pemBytes) - if err != nil { - return fmt.Errorf("error splitting certificates: %w", err) - } - if len(leaves) > 1 { - return fmt.Errorf("certificate chain must contain at most one TSA certificate") - } - if len(leaves) == 1 { - co.TSACertificate = leaves[0] - } - co.TSAIntermediateCertificates = intermediates - co.TSARootCertificates = roots + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath) + if err != nil { + ui.Warnf(ctx, fmt.Sprintf("cannot load tsa certificates: %s", err.Error())) + } else { + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCert } + if !c.IgnoreTlog { if c.RekorURL != "" { rekorClient, err := rekor.NewClient(c.RekorURL) @@ -157,6 +141,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("getting Rekor public keys: %w", err) } } + if keylessVerification(c.KeyRef, c.Sk) { // This performs an online fetch of the Fulcio roots. This is needed // for verifying keyless certificates (both online and offline). @@ -169,6 +154,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("getting Fulcio intermediates: %w", err) } } + keyRef := c.KeyRef // Keys are optional! diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index bd172a3aae1..399bf4b69d1 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -31,7 +31,6 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" - "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa" "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" @@ -115,29 +114,14 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { if c.RFC3161TimestampPath != "" && c.KeyOpts.TSACertChainPath == "" { return fmt.Errorf("timestamp-certificate-chain is required to validate a RFC3161 timestamp") } - if c.KeyOpts.TSACertChainPath != "" { - _, err := os.Stat(c.KeyOpts.TSACertChainPath) - if err != nil { - return fmt.Errorf("unable to open timestamp certificate chain file '%s: %w", c.KeyOpts.TSACertChainPath, err) - } - // TODO: Add support for TUF certificates. - pemBytes, err := os.ReadFile(filepath.Clean(c.KeyOpts.TSACertChainPath)) - if err != nil { - return fmt.Errorf("error reading certification chain path file: %w", err) - } - leaves, intermediates, roots, err := tsa.SplitPEMCertificateChain(pemBytes) - if err != nil { - return fmt.Errorf("error splitting certificates: %w", err) - } - if len(leaves) > 1 { - return fmt.Errorf("certificate chain must contain at most one TSA certificate") - } - if len(leaves) == 1 { - co.TSACertificate = leaves[0] - } - co.TSAIntermediateCertificates = intermediates - co.TSARootCertificates = roots + tsaCertificates, err := cosign.GetTSACerts(ctx, c.KeyOpts.TSACertChainPath) + if err != nil { + ui.Warnf(ctx, fmt.Sprintf("cannot load tsa certificates: %s", err.Error())) + } else { + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCert } if !c.IgnoreTlog { diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 63983eb4c2d..859b5d5fa47 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -25,17 +25,13 @@ import ( "encoding/json" "errors" "fmt" - "io" - "os" - "path/filepath" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" internal "github.com/sigstore/cosign/v2/internal/pkg/cosign" payloadsize "github.com/sigstore/cosign/v2/internal/pkg/cosign/payload/size" - "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa" + "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/bundle" @@ -45,6 +41,10 @@ import ( "github.com/sigstore/cosign/v2/pkg/policy" sigs "github.com/sigstore/cosign/v2/pkg/signature" "github.com/sigstore/sigstore/pkg/cryptoutils" + + "io" + "os" + "path/filepath" ) // VerifyBlobAttestationCommand verifies an attestation on a supplied blob @@ -143,29 +143,14 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st if c.RFC3161TimestampPath != "" && c.KeyOpts.TSACertChainPath == "" { return fmt.Errorf("timestamp-cert-chain is required to validate a rfc3161 timestamp bundle") } - if c.KeyOpts.TSACertChainPath != "" { - _, err := os.Stat(c.TSACertChainPath) - if err != nil { - return fmt.Errorf("unable to open timestamp certificate chain file: %w", err) - } - // TODO: Add support for TUF certificates. - pemBytes, err := os.ReadFile(filepath.Clean(c.TSACertChainPath)) - if err != nil { - return fmt.Errorf("error reading certification chain path file: %w", err) - } - leaves, intermediates, roots, err := tsa.SplitPEMCertificateChain(pemBytes) - if err != nil { - return fmt.Errorf("error splitting certificates: %w", err) - } - if len(leaves) > 1 { - return fmt.Errorf("certificate chain must contain at most one TSA certificate") - } - if len(leaves) == 1 { - co.TSACertificate = leaves[0] - } - co.TSAIntermediateCertificates = intermediates - co.TSARootCertificates = roots + tsaCertificates, err := cosign.GetTSACerts(ctx, c.KeyOpts.TSACertChainPath) + if err != nil { + ui.Warnf(ctx, fmt.Sprintf("cannot load tsa certificates: %s", err.Error())) + } else { + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCert } if !c.IgnoreTlog { diff --git a/pkg/cosign/env/env.go b/pkg/cosign/env/env.go index a2960e08143..05f2fe12cb0 100644 --- a/pkg/cosign/env/env.go +++ b/pkg/cosign/env/env.go @@ -58,6 +58,7 @@ const ( VariableSigstoreRootFile Variable = "SIGSTORE_ROOT_FILE" VariableSigstoreRekorPublicKey Variable = "SIGSTORE_REKOR_PUBLIC_KEY" VariableSigstoreIDToken Variable = "SIGSTORE_ID_TOKEN" //nolint:gosec + VariableSigstoreTSACertificateFile Variable = "SIGSTORE_TSA_CERTIFICATE_FILE" // Other external environment variables VariableGitHubHost Variable = "GITHUB_HOST" @@ -139,6 +140,13 @@ var ( External: true, }, + VariableSigstoreTSACertificateFile: { + Description: "if specified, you can specify TSA certificates", + Expects: "path to the certificate", + 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/tsaLog.go b/pkg/cosign/tsaLog.go new file mode 100644 index 00000000000..b76acaaa008 --- /dev/null +++ b/pkg/cosign/tsaLog.go @@ -0,0 +1,160 @@ +package cosign + +import ( + "bytes" + "context" + "crypto/x509" + "fmt" + "github.com/sigstore/cosign/v2/pkg/cosign/env" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/tuf" + "os" + "path/filepath" +) + +const ( + tsaLeafCertStr = `tsa_leaf.crt.pem` + tsaRootCertStr = `tsa_root.crt.pem` + tsaIntermediateCertStrPattern = `tsa_intermediate_%d.crt.pem` +) + +type TSACertificate struct { + RootCert []*x509.Certificate + IntermediateCert []*x509.Certificate + LeafCert *x509.Certificate +} + +// GetTSACerts retrieves trusted TSA certificates from the embedded or cached +// TUF root. If expired, makes a network call to retrieve the updated targets. +// By default, the certificates come from TUF, but you can override this for test +// purposes by using an env variable `SIGSTORE_TSA_CERTIFICATE_FILE`. If using +// an alternate, the file should be in PEM format. +func GetTSACerts(ctx context.Context, tsaCertChainPath string) (*TSACertificate, error) { + rootEnv := env.Getenv(env.VariableSigstoreTSACertificateFile) + if tsaCertChainPath != "" { + return getTsaCertFromFile(tsaCertChainPath) + } + if rootEnv != "" { + return getEnvCertFile(rootEnv) + } + return getTsaCertFromTufRoot(ctx) +} + +func getTsaCertFromFile(tsaCertChainPath string) (*TSACertificate, error) { + var tsaCertificate = TSACertificate{} + _, err := os.Stat(tsaCertChainPath) + if err != nil { + return nil, fmt.Errorf("unable to open timestamp certificate chain file: %w", err) + } + pemBytes, err := os.ReadFile(filepath.Clean(tsaCertChainPath)) + if err != nil { + return nil, fmt.Errorf("error reading certification chain path file: %w", err) + } + + leaves, intermediates, roots, err := splitPEMCertificateChain(pemBytes) + if err != nil { + return nil, fmt.Errorf("error splitting certificates: %w", err) + } + if len(leaves) != 1 { + return nil, fmt.Errorf("certificate chain must contain at most one TSA certificate") + } + if len(leaves) == 1 { + tsaCertificate.LeafCert = leaves[0] + } + tsaCertificate.IntermediateCert = intermediates + tsaCertificate.RootCert = roots + return &tsaCertificate, nil +} + +func getTsaCertFromTufRoot(ctx context.Context) (*TSACertificate, error) { + var tsaCertificate = TSACertificate{} + var tmpCerts []*x509.Certificate + tufClient, err := tuf.NewFromEnv(ctx) + if err != nil { + return nil, err + } + leafCert, err := tufClient.GetTarget(tsaLeafCertStr) + if err != nil { + return nil, fmt.Errorf("error fetching TSA leaf certificate: %w", err) + } + + rootCert, err := tufClient.GetTarget(tsaRootCertStr) + if err != nil { + return nil, fmt.Errorf("error fetching TSA root certificate: %w", err) + } + + if tsaCertificate.RootCert, err = cryptoutils.UnmarshalCertificatesFromPEM(rootCert); err != nil { + return nil, fmt.Errorf("error unmarshal TSA root certificate: %w", err) + } + + if tmpCerts, err = cryptoutils.UnmarshalCertificatesFromPEM(leafCert); err != nil { + return nil, fmt.Errorf("error unmarshal TSA leaf certificate: %w", err) + } + + if len(tmpCerts) > 1 { + return nil, fmt.Errorf("certificate chain must contain at most one TSA certificate") + } + + if len(tmpCerts) == 1 { + tsaCertificate.LeafCert = tmpCerts[0] + } + + for i := 0; ; i++ { + intermediateCertStr := fmt.Sprintf(tsaIntermediateCertStrPattern, i) + intermediateRawCert, err := tufClient.GetTarget(intermediateCertStr) + if err != nil { + break + } + intermediateCert, err := cryptoutils.UnmarshalCertificatesFromPEM(intermediateRawCert) + if err != nil { + return nil, fmt.Errorf("error unmarshal TSA intermediate certificate: %w", err) + } + tsaCertificate.IntermediateCert = append(tsaCertificate.IntermediateCert, intermediateCert...) + } + return &tsaCertificate, nil +} + +func getEnvCertFile(rootEnv string) (*TSACertificate, error) { + var tsaCertificate = TSACertificate{} + raw, err := os.ReadFile(rootEnv) + if err != nil { + return nil, fmt.Errorf("error reading certification chain file from env: %w", err) + } + leaves, intermediates, roots, err := splitPEMCertificateChain(raw) + if err != nil { + return nil, fmt.Errorf("error splitting certificates: %w", err) + } + if len(leaves) > 1 { + return nil, fmt.Errorf("certificate chain must contain at most one TSA certificate") + } + if len(leaves) == 1 { + tsaCertificate.LeafCert = leaves[0] + } + tsaCertificate.IntermediateCert = intermediates + tsaCertificate.RootCert = roots + return &tsaCertificate, nil +} + +// splitPEMCertificateChain returns a list of leaf (non-CA) certificates, a certificate pool for +// intermediate CA certificates, and a certificate pool for root CA certificates +func splitPEMCertificateChain(pem []byte) (leaves, intermediates, roots []*x509.Certificate, err error) { + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(pem) + if err != nil { + return nil, nil, nil, err + } + + for _, cert := range certs { + if !cert.IsCA { + leaves = append(leaves, cert) + } else { + // root certificates are self-signed + if bytes.Equal(cert.RawSubject, cert.RawIssuer) { + roots = append(roots, cert) + } else { + intermediates = append(intermediates, cert) + } + } + } + + return leaves, intermediates, roots, nil +}