diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 8c3c241618a..49ade7539df 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -30,6 +30,7 @@ type CommonVerifyOptions struct { // it for other verify options. ExperimentalOCI11 bool PrivateInfrastructure bool + UseSignedTimestamps bool } func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) { @@ -40,6 +41,9 @@ func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) { "path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. "+ "Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp") + cmd.Flags().BoolVar(&o.UseSignedTimestamps, "use-signed-timestamps", false, + "use signed timestamps if available") + cmd.Flags().BoolVar(&o.IgnoreTlog, "insecure-ignore-tlog", false, "ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts "+ "cannot be publicly verified when not included in a log") diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index de91d9229a8..31cb88c2641 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" @@ -77,11 +76,23 @@ type VerifyCommand struct { NameOptions []name.Option Offline bool TSACertChainPath string + UseSignedTimestamps bool IgnoreTlog bool MaxWorkers int ExperimentalOCI11 bool } +func (c *VerifyCommand) loadTSACertificates(ctx context.Context) (*cosign.TSACertificates, error) { + if c.TSACertChainPath == "" && !c.UseSignedTimestamps { + return nil, fmt.Errorf("TSA certificate chain path not provided and use-signed-timestamps not set") + } + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) + if err != nil { + return nil, fmt.Errorf("unable to load TSA certificates: %w", err) + } + return tsaCertificates, nil +} + // Exec runs the verification command func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if len(images) == 0 { @@ -136,29 +147,14 @@ 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 c.TSACertChainPath != "" || c.UseSignedTimestamps { + tsaCertificates, err := c.loadTSACertificates(ctx) 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] + return fmt.Errorf("unable to load TSA certificates: %w", err) } - co.TSAIntermediateCertificates = intermediates - co.TSARootCertificates = roots + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts } if !c.IgnoreTlog { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index a9ff6dd9982..09fcc718516 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" @@ -68,6 +67,18 @@ type VerifyAttestationCommand struct { TSACertChainPath string IgnoreTlog bool MaxWorkers int + UseSignedTimestamps bool +} + +func (c *VerifyAttestationCommand) loadTSACertificates(ctx context.Context) (*cosign.TSACertificates, error) { + if c.TSACertChainPath == "" && !c.UseSignedTimestamps { + return nil, fmt.Errorf("TSA certificate chain path not provided and use-signed-timestamps not set") + } + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) + if err != nil { + return nil, fmt.Errorf("unable to load TSA certificates: %w", err) + } + return tsaCertificates, nil } // Exec runs the verification command @@ -118,30 +129,16 @@ 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 c.TSACertChainPath != "" || c.UseSignedTimestamps { + tsaCertificates, err := c.loadTSACertificates(ctx) if err != nil { - return fmt.Errorf("error reading certification chain path file: %w", err) + return fmt.Errorf("unable to load TSA certificates: %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 + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts } + if !c.IgnoreTlog { if c.RekorURL != "" { rekorClient, err := rekor.NewClient(c.RekorURL) @@ -157,6 +154,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 +167,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..c58146ca0d1 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" @@ -64,9 +63,21 @@ type VerifyBlobCmd struct { IgnoreSCT bool SCTRef string Offline bool + UseSignedTimestamps bool IgnoreTlog bool } +func (c *VerifyBlobCmd) loadTSACertificates(ctx context.Context) (*cosign.TSACertificates, error) { + if c.TSACertChainPath == "" && !c.UseSignedTimestamps { + return nil, fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set") + } + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) + if err != nil { + return nil, fmt.Errorf("unable to load TSA certificates: %w", err) + } + return tsaCertificates, nil +} + // nolint func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { var cert *x509.Certificate @@ -112,32 +123,17 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, } - if c.RFC3161TimestampPath != "" && c.KeyOpts.TSACertChainPath == "" { - return fmt.Errorf("timestamp-certificate-chain is required to validate a RFC3161 timestamp") + 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") } - 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 c.TSACertChainPath != "" || c.UseSignedTimestamps { + tsaCertificates, err := c.loadTSACertificates(ctx) 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] + return err } - co.TSAIntermediateCertificates = intermediates - co.TSARootCertificates = roots + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts } 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..4c29501ce09 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -35,7 +35,6 @@ import ( "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/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" @@ -71,7 +70,8 @@ type VerifyBlobAttestationCommand struct { PredicateType string // TODO: Add policies - SignaturePath string // Path to the signature + SignaturePath string // Path to the signature + UseSignedTimestamps bool } // Exec runs the verification command @@ -140,32 +140,18 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } // Set up TSA, Fulcio roots and tlog public keys and clients. - if c.RFC3161TimestampPath != "" && c.KeyOpts.TSACertChainPath == "" { - return fmt.Errorf("timestamp-cert-chain is required to validate a rfc3161 timestamp bundle") + 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") } - 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 c.TSACertChainPath != "" || c.UseSignedTimestamps { + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) 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] + return fmt.Errorf("unable to load or get TSA certificates: %w", err) } - co.TSAIntermediateCertificates = intermediates - co.TSARootCertificates = roots + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts } if !c.IgnoreTlog { diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index 9ceb113ce20..7817b32a67b 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -89,6 +89,7 @@ cosign dockerfile verify [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp + --use-signed-timestamps use signed timestamps if available ``` ### Options inherited from parent commands diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index ae29314c6d5..39768ed1973 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -83,6 +83,7 @@ cosign manifest verify [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp + --use-signed-timestamps use signed timestamps if available ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index caa19214723..e7c26e17f7a 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -92,6 +92,7 @@ cosign verify-attestation [flags] --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp --type string specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|openvex|custom) or an URI (default "custom") + --use-signed-timestamps use signed timestamps if available ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md index f08675bb163..f688ee7e8fc 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -57,6 +57,7 @@ cosign verify-blob-attestation [flags] --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp --type string specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|openvex|custom) or an URI (default "custom") + --use-signed-timestamps use signed timestamps if available ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index dc0b13e4bcd..72089672249 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -85,6 +85,7 @@ cosign verify-blob [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp + --use-signed-timestamps use signed timestamps if available ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index 749e565342c..5bf88f6de73 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -106,6 +106,7 @@ cosign verify [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp + --use-signed-timestamps use signed timestamps if available ``` ### Options inherited from parent commands diff --git a/pkg/cosign/env/env.go b/pkg/cosign/env/env.go index a2960e08143..016687acb8e 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" @@ -138,7 +139,12 @@ var ( Sensitive: false, External: true, }, - + VariableSigstoreTSACertificateFile: { + Description: "path to the concatenated PEM-encoded TSA certificate file (leaf, intermediate(s), root) used by Sigstore", + Expects: "path to the TSA certificate file", + 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/tsa.go b/pkg/cosign/tsa.go new file mode 100644 index 00000000000..26c678ee320 --- /dev/null +++ b/pkg/cosign/tsa.go @@ -0,0 +1,139 @@ +// 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 ( + "bytes" + "context" + "crypto/x509" + "fmt" + "os" + + "github.com/sigstore/cosign/v2/pkg/cosign/env" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/tuf" +) + +const ( + tsaLeafCertStr = `tsa_leaf.crt.pem` + tsaRootCertStr = `tsa_root.crt.pem` + tsaIntermediateCertStrPattern = `tsa_intermediate_%d.crt.pem` +) + +type TSACertificates struct { + LeafCert *x509.Certificate + IntermediateCerts []*x509.Certificate + RootCert []*x509.Certificate +} + +type GetTargetStub func(ctx context.Context, usage tuf.UsageKind, names []string) ([]byte, error) + +func GetTufTargets(ctx context.Context, usage tuf.UsageKind, names []string) ([]byte, error) { + tufClient, err := tuf.NewFromEnv(ctx) + if err != nil { + return nil, fmt.Errorf("error creating TUF client: %w", err) + } + targets, err := tufClient.GetTargetsByMeta(usage, names) + if err != nil { + return nil, fmt.Errorf("error fetching targets by metadata with usage %v: %w", usage, err) + } + + var buffer bytes.Buffer + for _, target := range targets { + buffer.Write(target.Target) + buffer.WriteByte('\n') + } + return buffer.Bytes(), nil +} + +// 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` or a file path +// specified in `TSACertChainPath`. If using an alternate, the file should be in PEM format. +func GetTSACerts(ctx context.Context, certChainPath string, fn GetTargetStub) (*TSACertificates, error) { + altTSACert := env.Getenv(env.VariableSigstoreTSACertificateFile) + + var raw []byte + var err error + + switch { + case altTSACert != "": + raw, err = os.ReadFile(altTSACert) + case certChainPath != "": + raw, err = os.ReadFile(certChainPath) + default: + certNames := []string{tsaLeafCertStr, tsaRootCertStr} + for i := 0; ; i++ { + intermediateCertStr := fmt.Sprintf(tsaIntermediateCertStrPattern, i) + _, err := fn(ctx, tuf.TSA, []string{intermediateCertStr}) + if err != nil { + break + } + certNames = append(certNames, intermediateCertStr) + } + raw, err = fn(ctx, tuf.TSA, certNames) + if err != nil { + return nil, fmt.Errorf("error fetching TSA certificates: %w", err) + } + } + + if err != nil { + return nil, fmt.Errorf("error reading TSA certificate file: %w", err) + } + + leaves, intermediates, roots, err := splitPEMCertificateChain(raw) + if err != nil { + return nil, fmt.Errorf("error splitting TSA certificates: %w", err) + } + + if len(leaves) > 1 { + return nil, fmt.Errorf("TSA certificate chain must contain at most one leaf certificate") + } + + if len(roots) == 0 { + return nil, fmt.Errorf("TSA certificate chain must contain at least one root certificate") + } + + return &TSACertificates{ + LeafCert: leaves[0], + IntermediateCerts: intermediates, + RootCert: roots, + }, 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 +} diff --git a/pkg/cosign/tsa_test.go b/pkg/cosign/tsa_test.go new file mode 100644 index 00000000000..9487d1d25fa --- /dev/null +++ b/pkg/cosign/tsa_test.go @@ -0,0 +1,119 @@ +// 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 ( + "context" + "errors" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + testLeafCert = `-----BEGIN CERTIFICATE----- +MIIBjzCCATSgAwIBAgIRAOoa5khdNMW26Nz0VCvjbBAwCgYIKoZIzj0EAwIwGzEZ +MBcGA1UEAxMQaHR0cHM6Ly9ibGFoLmNvbTAgFw0yNDA2MDMyMDE2MDFaGA8yMTI0 +MDUxMDIwMTYwMFowGzEZMBcGA1UEAxMQaHR0cHM6Ly9ibGFoLmNvbTBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABL7w/TW5lOU9KwnGQRIyZp/ReNQF1eA2rKC582Jo +nMomwCk2bA8c5dHrvvHe+mI8JeMNEg3lkIsVQp46dKGlgYujVzBVMA4GA1UdDwEB +/wQEAwIBBjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSA7lVsQm5OUzvYi+o8PuBs +CrAnljAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNJADBGAiEA +oJSZgJPX2tqXhfvLm+5UR399+E6+rgUnSRUf4+p+K5gCIQCmtfuv8IkUIYE5ybtx ++bn5E95xINfDMSPBa+0PEbB5RA== +-----END CERTIFICATE-----` + testRootCert = `-----BEGIN CERTIFICATE----- +MIIBezCCASKgAwIBAgIRAMvdlXw/uuYvsJaCTa02uW4wCgYIKoZIzj0EAwIwGzEZ +MBcGA1UEAxMQaHR0cHM6Ly9ibGFoLmNvbTAgFw0yNDA2MDMyMDE1NTFaGA8yMTI0 +MDUxMDIwMTU1MFowGzEZMBcGA1UEAxMQaHR0cHM6Ly9ibGFoLmNvbTBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABLziRBPdWUTx9x3Z7zIMyo/C9cqsLK+hqnWDQS7K +TA38mZhMHnJ0vSaEA4g9J2ccI1x4G/HegCi9LkJG/EZLBjyjRTBDMA4GA1UdDwEB +/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBQuDQqo97s/5Lc5 +IxmFcVg3arCV2DAKBggqhkjOPQQDAgNHADBEAiAJOr0GnYaqVxShSEgVJKi/hYXf +PH5bKk0M9ceasS7VwQIgMkxzlWr+m10OELtAbOlI8faN/5WFKm8m8rrwnhmHzjw= +-----END CERTIFICATE-----` +) + +func MockGetTufTargets(name string) ([]byte, error) { + if name == `tsa_leaf.crt.pem` { + return []byte(testLeafCert), nil + } else if name == `tsa_root.crt.pem` { + return []byte(testRootCert), nil + } + + return nil, errors.New("no intermediates") +} + +func TestGetTSACertsFromEnv(t *testing.T) { + tempFile, err := os.CreateTemp("", "tsa_cert_chain.pem") + require.NoError(t, err) + defer os.Remove(tempFile.Name()) + + _, err = tempFile.Write([]byte(testLeafCert + "\n" + testRootCert)) + require.NoError(t, err) + + os.Setenv("SIGSTORE_TSA_CERTIFICATE_FILE", tempFile.Name()) + defer os.Unsetenv("SIGSTORE_TSA_CERTIFICATE_FILE") + + tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name(), GetTufTargets) + if err != nil { + t.Fatalf("Failed to get TSA certs from env: %v", err) + } + require.NotNil(t, tsaCerts) + require.NotNil(t, tsaCerts.LeafCert) + require.NotNil(t, tsaCerts.RootCert) + require.Len(t, tsaCerts.RootCert, 1) +} + +func TestGetTSACertsFromPath(t *testing.T) { + tempFile, err := os.CreateTemp("", "tsa_cert_chain_path.pem") + require.NoError(t, err) + defer os.Remove(tempFile.Name()) + + _, err = tempFile.Write([]byte(testLeafCert + "\n" + testRootCert)) + require.NoError(t, err) + + tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name(), GetTufTargets) + if err != nil { + t.Fatalf("Failed to get TSA certs from path: %v", err) + } + require.NotNil(t, tsaCerts) + require.NotNil(t, tsaCerts.LeafCert) + require.NotNil(t, tsaCerts.RootCert) + require.Len(t, tsaCerts.RootCert, 1) +} + +func TestGetTSACertsFromTUF(t *testing.T) { + originalValue := os.Getenv("SIGSTORE_TSA_CERTIFICATE_FILE") + os.Unsetenv("SIGSTORE_TSA_CERTIFICATE_FILE") + defer os.Setenv("SIGSTORE_TSA_CERTIFICATE_FILE", originalValue) + + tempFile, err := os.CreateTemp("", "tsa_cert_chain.pem") + require.NoError(t, err) + defer os.Remove(tempFile.Name()) + + _, err = tempFile.Write([]byte(testLeafCert + "\n" + testRootCert)) + require.NoError(t, err) + + tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name(), GetTufTargets) + if err != nil { + t.Fatalf("Failed to get TSA certs from TUF: %v", err) + } + require.NotNil(t, tsaCerts) + require.NotNil(t, tsaCerts.LeafCert) + require.NotNil(t, tsaCerts.RootCert) + require.Len(t, tsaCerts.RootCert, 1) +}