diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 900178033fb..f9b07d8d04d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -71,7 +71,7 @@ jobs: env: # See #2091 for the issue describing this temp workaround. GODEBUG: x509sha1=1 - run: go test -covermode atomic -coverprofile coverage.txt $(go list ./... | grep -v third_party/) + run: go test -tags=sct -covermode atomic -coverprofile coverage.txt $(go list ./... | grep -v third_party/) - name: Upload Coverage Report uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 with: @@ -81,7 +81,7 @@ jobs: # See #2091 for the issue describing this temp workaround. GODEBUG: x509sha1=1 if: ${{ runner.os == 'Linux' }} - run: go test -race $(go list ./... | grep -v third_party/) + run: go test -tags=sct -race $(go list ./... | grep -v third_party/) e2e-tests: name: Run e2e tests diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index 245164b7f8f..36738296924 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -98,7 +98,8 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, CertChain: o.CertVerify.CertChain, - EnforceSCT: o.CertVerify.EnforceSCT, + IgnoreSCT: o.CertVerify.IgnoreSCT, + SCTRef: o.CertVerify.SCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, diff --git a/cmd/cosign/cli/manifest.go b/cmd/cosign/cli/manifest.go index 253d0619791..39b66070fb3 100644 --- a/cmd/cosign/cli/manifest.go +++ b/cmd/cosign/cli/manifest.go @@ -93,7 +93,8 @@ against the transparency log.`, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, CertChain: o.CertVerify.CertChain, - EnforceSCT: o.CertVerify.EnforceSCT, + IgnoreSCT: o.CertVerify.IgnoreSCT, + SCTRef: o.CertVerify.SCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index 223363243e6..ba3d7320ca0 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -29,7 +29,8 @@ type CertVerifyOptions struct { CertGithubWorkflowRepository string CertGithubWorkflowRef string CertChain string - EnforceSCT bool + SCT string + IgnoreSCT bool } var _ Interface = (*RekorOptions)(nil) @@ -70,7 +71,10 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { "signing certificate and end with the root certificate") _ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"}) - cmd.Flags().BoolVar(&o.EnforceSCT, "enforce-sct", false, - "whether to enforce that a certificate contain an embedded SCT, a proof of "+ + cmd.Flags().StringVar(&o.SCT, "sct", "", + "path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. "+ + "If a certificate contains an SCT, verification will check both the detached and embedded SCTs.") + cmd.Flags().BoolVar(&o.IgnoreSCT, "insecure-ignore-sct", false, + "when set, verification will not check that a certificate contains an embedded SCT, a proof of "+ "inclusion in a certificate transparency log") } diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 70cca4878ac..29572963518 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -105,7 +105,8 @@ against the transparency log.`, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, CertChain: o.CertVerify.CertChain, - EnforceSCT: o.CertVerify.EnforceSCT, + IgnoreSCT: o.CertVerify.IgnoreSCT, + SCTRef: o.CertVerify.SCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, @@ -193,7 +194,8 @@ against the transparency log.`, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, - EnforceSCT: o.CertVerify.EnforceSCT, + IgnoreSCT: o.CertVerify.IgnoreSCT, + SCTRef: o.CertVerify.SCT, KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, @@ -283,7 +285,8 @@ The blob may be specified as a path to a file or - for stdin.`, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, - EnforceSCT: o.CertVerify.EnforceSCT, + IgnoreSCT: o.CertVerify.IgnoreSCT, + SCTRef: o.CertVerify.SCT, } if err := verifyBlobCmd.Exec(cmd.Context(), args[0]); err != nil { return fmt.Errorf("verifying blob %s: %w", args, err) diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 66638ea8698..72812f7ea68 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -26,6 +26,7 @@ import ( "flag" "fmt" "os" + "path/filepath" "github.com/google/go-containerregistry/pkg/name" @@ -60,7 +61,8 @@ type VerifyCommand struct { CertGithubWorkflowRef string CertChain string CertOidcProvider string - EnforceSCT bool + IgnoreSCT bool + SCTRef string Sk bool Slot string Output string @@ -108,7 +110,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { CertGithubWorkflowName: c.CertGithubWorkflowName, CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, CertGithubWorkflowRef: c.CertGithubWorkflowRef, - EnforceSCT: c.EnforceSCT, + IgnoreSCT: c.IgnoreSCT, SignatureRef: c.SignatureRef, Identities: []cosign.Identity{{Issuer: c.CertOidcIssuer, Subject: c.CertIdentity}}, } @@ -188,6 +190,13 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return 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 + } } co.SigVerifier = pubKey diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index c9e5638043f..e678bb1f5f6 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -53,7 +53,8 @@ type VerifyAttestationCommand struct { CertGithubWorkflowRepository string CertGithubWorkflowRef string CertChain string - EnforceSCT bool + IgnoreSCT bool + SCTRef string Sk bool Slot string Output string @@ -90,7 +91,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e CertGithubWorkflowName: c.CertGithubWorkflowName, CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, CertGithubWorkflowRef: c.CertGithubWorkflowRef, - EnforceSCT: c.EnforceSCT, + IgnoreSCT: c.IgnoreSCT, Identities: []cosign.Identity{{Issuer: c.CertOidcIssuer, Subject: c.CertIdentity}}, } if c.CheckClaims { @@ -166,6 +167,13 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("creating certificate verifier: %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 + } } // 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 9aef0bb665b..f230517dc4b 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -28,6 +28,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "time" @@ -78,7 +79,8 @@ type VerifyBlobCmd struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string - EnforceSCT bool + IgnoreSCT bool + SCTRef string } // nolint @@ -110,7 +112,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { CertGithubWorkflowName: c.CertGithubWorkflowName, CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, CertGithubWorkflowRef: c.CertGithubWorkflowRef, - EnforceSCT: c.EnforceSCT, + IgnoreSCT: c.IgnoreSCT, Identities: []cosign.Identity{{Issuer: c.CertOIDCIssuer, Subject: c.CertIdentity}}, } if options.EnableExperimental() { @@ -184,6 +186,13 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return fmt.Errorf("verifying certRef with certChain: %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.BundlePath != "": b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index f12dc6f1c5f..56bb360bfb4 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -545,6 +545,7 @@ func TestVerifyBlob(t *testing.T) { co := &cosign.CheckOpts{ SigVerifier: tt.sigVerifier, RootCerts: rootPool, + IgnoreSCT: true, Identities: []cosign.Identity{{Issuer: issuer, Subject: identity}}, } // if expermental is enabled, add RekorClient to co. @@ -722,7 +723,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { KeyOpts: options.KeyOpts{BundlePath: bundlePath}, CertIdentity: identity, CertOIDCIssuer: issuer, - EnforceSCT: false, + IgnoreSCT: true, } if err := cmd.Exec(context.Background(), blobPath); err != nil { t.Fatal(err) @@ -757,8 +758,8 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { // Verify command cmd := VerifyBlobCmd{ - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + KeyOpts: options.KeyOpts{BundlePath: bundlePath}, + IgnoreSCT: true, } if err := cmd.Exec(context.Background(), blobPath); err == nil { t.Fatal("expecting err due to mismatched signatures, got nil") @@ -787,8 +788,8 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { // Verify command cmd := VerifyBlobCmd{ - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + KeyOpts: options.KeyOpts{BundlePath: bundlePath}, + IgnoreSCT: true, } if err := cmd.Exec(context.Background(), blobPath); err == nil { @@ -824,7 +825,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE SigRef: "", // Sig is fetched from bundle KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + IgnoreSCT: true, } if err := cmd.Exec(context.Background(), blobPath); err != nil { t.Fatal(err) @@ -859,7 +860,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE SigRef: "", // Sig is fetched from bundle KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err == nil || !strings.Contains(err.Error(), "unable to verify SET") { @@ -895,7 +896,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { CertIdentity: "invalid@example.com", CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE SigRef: "", // Sig is fetched from bundle - EnforceSCT: false, + IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err == nil || !strings.Contains(err.Error(), "none of the expected identities matched what was in the certificate") { @@ -931,7 +932,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE SigRef: "", // Sig is fetched from bundle KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err == nil || !strings.Contains(err.Error(), "none of the expected identities matched what was in the certificate") { @@ -968,7 +969,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE SigRef: "", // Sig is fetched from bundle KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err != nil { @@ -1003,7 +1004,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { CertChain: os.Getenv("SIGSTORE_ROOT_FILE"), SigRef: "", // Sig is fetched from bundle KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err != nil { @@ -1049,7 +1050,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { CertChain: tmpChainFile.Name(), SigRef: "", // Sig is fetched from bundle KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err == nil || !strings.Contains(err.Error(), "verifying certificate from bundle with chain: x509: certificate signed by unknown authority") { @@ -1092,7 +1093,7 @@ func TestVerifyBlobCmdInvalidRootCA(t *testing.T) { CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE SigRef: "", // Sig is fetched from bundle KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err == nil || !strings.Contains(err.Error(), "certificate signed by unknown authority") { @@ -1128,7 +1129,7 @@ func TestVerifyBlobCmdInvalidRootCA(t *testing.T) { CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE SigRef: "", // Sig is fetched from bundle KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - EnforceSCT: false, + IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err == nil || !strings.Contains(err.Error(), "certificate signed by unknown authority") { diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index 363249c0e3f..a7671a26131 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -68,13 +68,14 @@ cosign dockerfile verify [flags] --certificate-identity string Required. The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. --certificate-oidc-issuer string Required. The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) - --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret --local-image whether the specified image is a path to an image saved locally via 'cosign save' -o, --output string output format for the signing image information (json|text) (default "json") --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. --signature string signature content or path or remote URL --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") --sk whether to use a hardware security key diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index 1f5735e9770..8cc0a9c718b 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -62,13 +62,14 @@ cosign manifest verify [flags] --certificate-identity string Required. The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. --certificate-oidc-issuer string Required. The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) - --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret --local-image whether the specified image is a path to an image saved locally via 'cosign save' -o, --output string output format for the signing image information (json|text) (default "json") --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. --signature string signature content or path or remote URL --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") --sk whether to use a hardware security key diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index e80db265af9..8361c0253d3 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -72,14 +72,15 @@ cosign verify-attestation [flags] --certificate-identity string Required. The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. --certificate-oidc-issuer string Required. The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) - --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-attestation + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret --local-image whether the specified image is a path to an image saved locally via 'cosign save' -o, --output string output format for the signing image information (json|text) (default "json") --policy strings specify CUE or Rego files will be using for validation --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. --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) --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index c3e3afd5f1b..72b15c4a46d 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -71,11 +71,12 @@ cosign verify-blob [flags] --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run --certificate-identity string Required. The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. --certificate-oidc-issuer string Required. The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth - --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-blob + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. --signature string signature content or path or remote URL --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) diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index cd7a9f7e023..e27bb72977b 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -78,13 +78,14 @@ cosign verify [flags] --certificate-identity string Required. The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. --certificate-oidc-issuer string Required. The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) - --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret --local-image whether the specified image is a path to an image saved locally via 'cosign save' -o, --output string output format for the signing image information (json|text) (default "json") --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. --signature string signature content or path or remote URL --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") --sk whether to use a hardware security key diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 1cf2b03d5ed..627d2eda633 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -102,9 +102,11 @@ type CheckOpts struct { // CertGithubWorkflowRef is the GitHub Workflow Ref expected for a certificate to be valid. The empty string means any certificate can be valid. CertGithubWorkflowRef string - // EnforceSCT requires that a certificate contain an embedded SCT during verification. An SCT is proof of inclusion in a + // IgnoreSCT requires that a certificate contain an embedded SCT during verification. An SCT is proof of inclusion in a // certificate transparency log. - EnforceSCT bool + IgnoreSCT bool + // Detached SCT. Optional, as the SCT is usually embedded in the certificate. + SCT []byte // SignatureRef is the reference to the signature file SignatureRef string @@ -190,21 +192,41 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver return nil, err } + // If IgnoreSCT is set, skip the SCT check + if co.IgnoreSCT { + return verifier, nil + } contains, err := ctl.ContainsSCT(cert.Raw) if err != nil { return nil, err } - if co.EnforceSCT && !contains { - return nil, &VerificationError{"certificate does not include required embedded SCT"} + if !contains && len(co.SCT) == 0 { + return nil, &VerificationError{"certificate does not include required embedded SCT and no detached SCT was set"} + } + // 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") } if contains { - // 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") - } if err := ctl.VerifyEmbeddedSCT(context.Background(), chains[0]); 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 := ctl.VerifySCT(context.Background(), certPEM, chainPEM, co.SCT); err != nil { + return nil, err + } } return verifier, nil diff --git a/pkg/cosign/verify_sct_test.go b/pkg/cosign/verify_sct_test.go new file mode 100644 index 00000000000..df49df467cc --- /dev/null +++ b/pkg/cosign/verify_sct_test.go @@ -0,0 +1,141 @@ +// Copyright 2022 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. + +//go:build sct +// +build sct + +package cosign + +import ( + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "testing" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/testdata" + "github.com/google/certificate-transparency-go/tls" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +// TODO: Move back into verify_test.go once the test cert has been regenerated +func TestValidateAndUnpackCertWithSCT(t *testing.T) { + chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestEmbeddedCertPEM + testdata.CACertPEM)) + if err != nil { + t.Fatalf("error unmarshalling certificate chain: %v", err) + } + + rootPool := x509.NewCertPool() + rootPool.AddCert(chain[1]) + co := &CheckOpts{ + RootCerts: rootPool, + // explicitly set to false + IgnoreSCT: false, + } + + // write SCT verification key to disk + tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + defer tmpPrivFile.Close() + if _, err := tmpPrivFile.Write([]byte(testdata.LogPublicKeyPEM)); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + t.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) + + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } + + // validate again, explicitly setting ignore SCT to false + co.IgnoreSCT = false + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } +} + +func TestValidateAndUnpackCertWithDetachedSCT(t *testing.T) { + chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestCertPEM + testdata.CACertPEM)) + if err != nil { + t.Fatalf("error unmarshalling certificate chain: %v", err) + } + + rootPool := x509.NewCertPool() + rootPool.AddCert(chain[1]) + co := &CheckOpts{ + RootCerts: rootPool, + // explicitly set to false + IgnoreSCT: false, + } + + // write SCT verification key to disk + tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + defer tmpPrivFile.Close() + if _, err := tmpPrivFile.Write([]byte(testdata.LogPublicKeyPEM)); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + t.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) + + // Fulcio quirk, since it returns a different SCT structure + var sct ct.SignedCertificateTimestamp + if _, err := tls.Unmarshal(testdata.TestCertProof, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + chainResp, err := toAddChainResponse(&sct) + if err != nil { + t.Fatalf("error generating chain response: %v", err) + } + sctBytes, err := json.Marshal(chainResp) + if err != nil { + t.Fatalf("error marshalling chain: %v", err) + } + co.SCT = sctBytes + + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } + + // validate again, explicitly setting ignore SCT to false + co.IgnoreSCT = false + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } +} + +// toAddChainResponse converts an SCT to a response struct, the expected structure for detached SCTs +func toAddChainResponse(sct *ct.SignedCertificateTimestamp) (*ct.AddChainResponse, error) { + sig, err := tls.Marshal(sct.Signature) + if err != nil { + return nil, fmt.Errorf("failed to marshal signature: %w", err) + } + addChainResp := &ct.AddChainResponse{ + SCTVersion: sct.SCTVersion, + Timestamp: sct.Timestamp, + Extensions: base64.StdEncoding.EncodeToString(sct.Extensions), + ID: sct.LogID.KeyID[:], + Signature: sig, + } + + return addChainResp, nil +} diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index e6222dc74b7..e5bfeffdac9 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -31,14 +31,12 @@ import ( "io" "net" "net/url" - "os" "strings" "testing" "time" "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/go-openapi/strfmt" - "github.com/google/certificate-transparency-go/testdata" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" "github.com/secure-systems-lab/go-securesystemslib/dsse" @@ -50,7 +48,6 @@ import ( "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" rtypes "github.com/sigstore/rekor/pkg/types" - "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" "github.com/stretchr/testify/require" @@ -148,6 +145,7 @@ func TestVerifyImageSignature(t *testing.T) { verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ RootCerts: rootPool, + IgnoreSCT: true, Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}}) if err != nil { t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) @@ -181,6 +179,7 @@ func TestVerifyImageSignatureMultipleSubs(t *testing.T) { base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub3, pemSub2, pemSub1, pemRoot}))) verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ RootCerts: rootPool, + IgnoreSCT: true, Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}}) if err != nil { t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) @@ -258,6 +257,7 @@ func TestVerifyImageSignatureWithNoChain(t *testing.T) { verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ RootCerts: rootPool, + IgnoreSCT: true, Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}}) if err == nil { t.Fatalf("expected error due to custom Rekor public key") @@ -284,6 +284,7 @@ func TestVerifyImageSignatureWithOnlyRoot(t *testing.T) { verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ RootCerts: rootPool, + IgnoreSCT: true, Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}}) if err != nil { t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) @@ -312,6 +313,7 @@ func TestVerifyImageSignatureWithMissingSub(t *testing.T) { verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ RootCerts: rootPool, + IgnoreSCT: true, Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}}) if err == nil { t.Fatal("expected error while verifying signature") @@ -352,6 +354,7 @@ func TestVerifyImageSignatureWithExistingSub(t *testing.T) { &CheckOpts{ RootCerts: rootPool, IntermediateCerts: subPool, + IgnoreSCT: true, Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}}) if err == nil { t.Fatal("expected error while verifying signature") @@ -450,6 +453,7 @@ func TestValidateAndUnpackCertSuccess(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, + IgnoreSCT: true, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, } @@ -475,6 +479,7 @@ func TestValidateAndUnpackCertSuccessAllowAllValues(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, + IgnoreSCT: true, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, } @@ -488,44 +493,6 @@ func TestValidateAndUnpackCertSuccessAllowAllValues(t *testing.T) { } } -func TestValidateAndUnpackCertWithSCT(t *testing.T) { - chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestEmbeddedCertPEM + testdata.CACertPEM)) - if err != nil { - t.Fatalf("error unmarshalling certificate chain: %v", err) - } - - rootPool := x509.NewCertPool() - rootPool.AddCert(chain[1]) - - co := &CheckOpts{ - RootCerts: rootPool, - } - - // write SCT verification key to disk - tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") - if err != nil { - t.Fatalf("failed to create temp key file: %v", err) - } - defer tmpPrivFile.Close() - if _, err := tmpPrivFile.Write([]byte(testdata.LogPublicKeyPEM)); err != nil { - t.Fatalf("failed to write key file: %v", err) - } - os.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) - defer os.Unsetenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") - - _, err = ValidateAndUnpackCert(chain[0], co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } - - // validate again, explicitly setting enforce SCT - co.EnforceSCT = true - _, err = ValidateAndUnpackCert(chain[0], co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } -} - func TestValidateAndUnpackCertWithoutRequiredSCT(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" @@ -539,6 +506,8 @@ func TestValidateAndUnpackCertWithoutRequiredSCT(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + // explicitly set to false + IgnoreSCT: false, EnforceSCT: true, } @@ -566,6 +535,7 @@ func TestValidateAndUnpackCertSuccessWithDnsSan(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -598,6 +568,7 @@ func TestValidateAndUnpackCertSuccessWithEmailSan(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -630,6 +601,7 @@ func TestValidateAndUnpackCertSuccessWithIpAddressSan(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -662,6 +634,7 @@ func TestValidateAndUnpackCertSuccessWithUriSan(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: []Identity{{Subject: "scheme://userinfo@host", Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -674,6 +647,39 @@ func TestValidateAndUnpackCertSuccessWithUriSan(t *testing.T) { } } +func TestValidateAndUnpackCertSuccessWithOtherNameSan(t *testing.T) { + // generate with OtherName, which will override other SANs + subject := "subject-othername" + ext, err := MarshalOtherNameSAN(subject, true) + if err != nil { + t.Fatalf("error marshalling SANs: %v", err) + } + exts := []pkix.Extension{*ext} + + oidcIssuer := "https://accounts.google.com" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert("unused", oidcIssuer, rootCert, rootKey, exts...) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertIdentity: subject, + CertOidcIssuer: oidcIssuer, + } + + _, err = ValidateAndUnpackCert(leafCert, co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } + err = CheckCertificatePolicy(leafCert, co) + if err != nil { + t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) + } +} + func TestValidateAndUnpackCertInvalidRoot(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" @@ -689,6 +695,7 @@ func TestValidateAndUnpackCertInvalidRoot(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -708,6 +715,7 @@ func TestValidateAndUnpackCertInvalidOidcIssuer(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: "other"}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -729,6 +737,7 @@ func TestValidateAndUnpackCertInvalidEmail(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: []Identity{{Subject: "other", Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -752,6 +761,7 @@ func TestValidateAndUnpackCertInvalidGithubWorkflowTrigger(t *testing.T) { RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, CertGithubWorkflowTrigger: "otherTrigger", + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -775,6 +785,7 @@ func TestValidateAndUnpackCertInvalidGithubWorkflowSHA(t *testing.T) { RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, CertGithubWorkflowSha: "otherSHA", + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -798,6 +809,7 @@ func TestValidateAndUnpackCertInvalidGithubWorkflowName(t *testing.T) { RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, CertGithubWorkflowName: "otherName", + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -821,6 +833,7 @@ func TestValidateAndUnpackCertInvalidGithubWorkflowRepository(t *testing.T) { RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, CertGithubWorkflowRepository: "otherRepository", + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -844,6 +857,7 @@ func TestValidateAndUnpackCertInvalidGithubWorkflowRef(t *testing.T) { RootCerts: rootPool, Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, CertGithubWorkflowRef: "otherRef", + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co) @@ -862,6 +876,7 @@ func TestValidateAndUnpackCertWithChainSuccess(t *testing.T) { co := &CheckOpts{ Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{subCert, leafCert}, co) @@ -879,6 +894,7 @@ func TestValidateAndUnpackCertWithChainSuccessWithRoot(t *testing.T) { co := &CheckOpts{ Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{rootCert}, co) @@ -896,6 +912,7 @@ func TestValidateAndUnpackCertWithChainFailsWithoutChain(t *testing.T) { co := &CheckOpts{ Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{}, co) @@ -914,6 +931,7 @@ func TestValidateAndUnpackCertWithChainFailsWithInvalidChain(t *testing.T) { co := &CheckOpts{ Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, + IgnoreSCT: true, } _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{rootCertOther}, co) @@ -1011,6 +1029,7 @@ func TestValidateAndUnpackCertWithIdentities(t *testing.T) { co := &CheckOpts{ RootCerts: rootPool, Identities: tc.identities, + IgnoreSCT: true, } _, err := ValidateAndUnpackCert(leafCert, co)