From 71fb7522d8d871dce30cdeafe8e81789bf82ce9f Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Tue, 12 Apr 2022 23:01:12 +0000 Subject: [PATCH] Add policy flag to enforce SCT Signed-off-by: Hayden Blauzvern --- cmd/cosign/cli/dockerfile.go | 1 + cmd/cosign/cli/manifest.go | 1 + cmd/cosign/cli/options/certificate.go | 5 ++++ cmd/cosign/cli/verify.go | 5 +++- cmd/cosign/cli/verify/verify.go | 2 ++ cmd/cosign/cli/verify/verify_attestation.go | 2 ++ cmd/cosign/cli/verify/verify_blob.go | 10 +++++--- doc/cosign_dockerfile_verify.md | 1 + doc/cosign_manifest_verify.md | 1 + doc/cosign_verify-attestation.md | 1 + doc/cosign_verify-blob.md | 1 + doc/cosign_verify.md | 1 + pkg/cosign/verify.go | 7 +++++- pkg/cosign/verify_test.go | 28 +++++++++++++++++++++ test/e2e_test.go | 14 +++++------ 15 files changed, 68 insertions(+), 12 deletions(-) diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index fdcf524c49e..27e37b2b778 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -92,6 +92,7 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, 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 07c213cbe58..5854065f43e 100644 --- a/cmd/cosign/cli/manifest.go +++ b/cmd/cosign/cli/manifest.go @@ -87,6 +87,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, 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 f801f4f801a..615842c810e 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -24,6 +24,7 @@ type CertVerifyOptions struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool } var _ Interface = (*RekorOptions)(nil) @@ -44,4 +45,8 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { "when building the certificate chain for the signing certificate. "+ "Must start with the parent intermediate CA certificate of the "+ "signing certificate and end with the root certificate") + + cmd.Flags().BoolVar(&o.EnforceSCT, "enforce-sct", false, + "whether to enforce that a certificate contain 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 680e30239a1..5471da9a1b0 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -97,6 +97,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, @@ -174,6 +175,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, @@ -252,7 +254,8 @@ The blob may be specified as a path to a file or - for stdin.`, BundlePath: o.BundlePath, } if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert, - o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, o.Signature, args[0]); err != nil { + o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, + o.Signature, args[0], o.CertVerify.EnforceSCT); err != nil { return errors.Wrapf(err, "verifying blob %s", args) } return nil diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 79cc1325c6d..e55381e0b1d 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -54,6 +54,7 @@ type VerifyCommand struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool Sk bool Slot string Output string @@ -95,6 +96,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, + EnforceSCT: c.EnforceSCT, SignatureRef: c.SignatureRef, } if c.CheckClaims { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index fb4cb7713b0..8ab95fd7dac 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -50,6 +50,7 @@ type VerifyAttestationCommand struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool Sk bool Slot string Output string @@ -77,6 +78,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, + EnforceSCT: c.EnforceSCT, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index d0741f13044..0341ed0a025 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -61,7 +61,8 @@ func isb64(data []byte) bool { } // nolint -func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, certOidcIssuer, certChain, sigRef, blobRef string) error { +func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, + certOidcIssuer, certChain, sigRef, blobRef string, enforceSCT bool) error { var verifier signature.Verifier var cert *x509.Certificate @@ -119,6 +120,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer co := &cosign.CheckOpts{ CertEmail: certEmail, CertOidcIssuer: certOidcIssuer, + EnforceSCT: enforceSCT, } verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) if err != nil { @@ -162,7 +164,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer if len(uuids) == 0 { return errors.New("could not find a tlog entry for provided blob") } - return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes) + return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes, enforceSCT) } // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. @@ -184,7 +186,8 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer return nil } -func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, uuids []string, blobBytes []byte) error { +func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, + uuids []string, blobBytes []byte, enforceSCT bool) error { var validSigExists bool for _, u := range uuids { tlogEntry, err := cosign.GetTlogEntry(ctx, rClient, u) @@ -202,6 +205,7 @@ func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor IntermediateCerts: fulcio.GetIntermediates(), CertEmail: certEmail, CertOidcIssuer: certOidcIssuer, + EnforceSCT: enforceSCT, } cert := certs[0] verifier, err := cosign.ValidateAndUnpackCert(cert, co) diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index bf0ae43bf8d..fa6ec41bfe3 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -62,6 +62,7 @@ cosign dockerfile verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string 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 --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 diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index a78e08bfb59..81f314d83f8 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -56,6 +56,7 @@ cosign manifest verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string 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 --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 diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 90c7e7a4f09..65ca6994699 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -66,6 +66,7 @@ cosign verify-attestation [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string 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 --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 diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index cb02172985a..894e46afde6 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -68,6 +68,7 @@ cosign verify-blob [flags] --cert-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string 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 --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 diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index c27f4d50808..68bf94f6d90 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -72,6 +72,7 @@ cosign verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string 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 --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 diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index e27c4b81f12..e124f4ca0c3 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -82,6 +82,9 @@ type CheckOpts struct { CertEmail string // CertOidcIssuer is the OIDC issuer expected for a certificate to be valid. The empty string means any certificate can be valid. CertOidcIssuer string + // EnforceSCT requires that a certificate contain an embedded SCT during verification. An SCT is proof of inclusion in a + // certificate transparency log. + EnforceSCT bool // SignatureRef is the reference to the signature file SignatureRef string @@ -180,11 +183,13 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver return nil, errors.New("expected oidc issuer not found in certificate") } } - // TODO: Add flag in CheckOpts to enforce embedded SCT contains, err := ctl.ContainsSCT(cert.Raw) if err != nil { return nil, err } + if co.EnforceSCT && !contains { + return nil, errors.New("certificate does not include required embedded SCT") + } if contains { // handle if chains has more than one chain - grab first and print message if len(chains) > 1 { diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 5753c29633e..a019afc53e2 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -315,6 +315,34 @@ func TestValidateAndUnpackCertWithSCT(t *testing.T) { 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" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertOidcIssuer: oidcIssuer, + EnforceSCT: true, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "certificate does not include required embedded SCT") } func TestValidateAndUnpackCertInvalidRoot(t *testing.T) { diff --git a/test/e2e_test.go b/test/e2e_test.go index c62280ba28a..1302b9d8478 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -501,8 +501,8 @@ func TestSignBlob(t *testing.T) { KeyRef: pubKeyPath2, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) // Now sign the blob with one key ko := sign.KeyOpts{ @@ -514,8 +514,8 @@ func TestSignBlob(t *testing.T) { t.Fatal(err) } // Now verify should work with that one, but not the other - must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) } func TestSignBlobBundle(t *testing.T) { @@ -540,7 +540,7 @@ func TestSignBlobBundle(t *testing.T) { BundlePath: bundlePath, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, false), t) // Now sign the blob with one key ko := sign.KeyOpts{ @@ -553,7 +553,7 @@ func TestSignBlobBundle(t *testing.T) { t.Fatal(err) } // Now verify should work - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) // Now we turn on the tlog and sign again defer setenv(t, options.ExperimentalEnv, "1")() @@ -563,7 +563,7 @@ func TestSignBlobBundle(t *testing.T) { // Point to a fake rekor server to make sure offline verification of the tlog entry works os.Setenv(serverEnv, "notreal") - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) } func TestGenerate(t *testing.T) {