diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index 7c27fc01d478..fdcf524c49ef 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -91,6 +91,7 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val CertRef: o.CertVerify.Cert, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, + CertChain: o.CertVerify.CertChain, 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 14fcfb7f173b..07c213cbe581 100644 --- a/cmd/cosign/cli/manifest.go +++ b/cmd/cosign/cli/manifest.go @@ -86,6 +86,7 @@ against the transparency log.`, CertRef: o.CertVerify.Cert, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, + CertChain: o.CertVerify.CertChain, 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 782d6a8bb59e..f801f4f801aa 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -23,6 +23,7 @@ type CertVerifyOptions struct { Cert string CertEmail string CertOidcIssuer string + CertChain string } var _ Interface = (*RekorOptions)(nil) @@ -37,4 +38,10 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.CertOidcIssuer, "cert-oidc-issuer", "", "the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth") + + cmd.Flags().StringVar(&o.CertChain, "cert-chain", "", + "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") } diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index e518aada94b8..680e30239a19 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -56,6 +56,9 @@ against the transparency log.`, # verify image with an on-disk signed image from 'cosign save' cosign verify --key cosign.pub --local-image + # verify image with local certificate and certificate chain + cosign verify --cert cosign.crt --cert-chain chain.crt + # verify image with public key provided by URL cosign verify --key https://host.for/[FILE] @@ -93,6 +96,7 @@ against the transparency log.`, CertRef: o.CertVerify.Cert, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, + CertChain: o.CertVerify.CertChain, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, @@ -169,6 +173,7 @@ against the transparency log.`, CertRef: o.CertVerify.Cert, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, + CertChain: o.CertVerify.CertChain, KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, @@ -247,7 +252,7 @@ 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.Signature, args[0]); err != nil { + o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, o.Signature, args[0]); 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 907701551779..a01cb6a233ca 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -16,6 +16,7 @@ package verify import ( + "bytes" "context" "crypto" "crypto/ecdsa" @@ -53,6 +54,7 @@ type VerifyCommand struct { CertRef string CertEmail string CertOidcIssuer string + CertChain string Sk bool Slot string Output string @@ -139,9 +141,21 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if err != nil { return err } - pubKey, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) - if err != nil { - return err + if c.CertChain == "" { + pubKey, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) + if err != nil { + return err + } + } else { + // Verify certificate with chain + chain, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + if err != nil { + return err + } } } co.SigVerifier = pubKey @@ -296,3 +310,15 @@ func loadCertFromPEM(pems []byte) (*x509.Certificate, error) { } return certs[0], nil } + +func loadCertChainFromFileOrURL(path string) ([]*x509.Certificate, error) { + pems, err := blob.LoadFileOrURL(path) + if err != nil { + return nil, err + } + certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(pems)) + if err != nil { + return nil, err + } + return certs, nil +} diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 8d414df03639..63c8df468a2c 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -51,6 +51,7 @@ type VerifyAttestationCommand struct { CertRef string CertEmail string CertOidcIssuer string + CertChain string KeyRef string Sk bool Slot string @@ -121,9 +122,21 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e if err != nil { return errors.Wrap(err, "loading certificate from reference") } - co.SigVerifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) - if err != nil { - return errors.Wrap(err, "creating certificate verifier") + if c.CertChain == "" { + co.SigVerifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) + if err != nil { + return errors.Wrap(err, "creating certificate verifier") + } + } else { + // Verify certificate with chain + chain, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + if err != nil { + return errors.Wrap(err, "creating certificate verifier") + } } } diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 78e5ade6e38a..495a9320dfbb 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -60,7 +60,7 @@ func isb64(data []byte) bool { } // nolint -func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, certOidcIssuer, sigRef, blobRef string) error { +func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, certOidcIssuer, certChain, sigRef, blobRef string) error { var verifier signature.Verifier var cert *x509.Certificate @@ -104,9 +104,25 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer if err != nil { return err } - verifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) - if err != nil { - return err + if certChain == "" { + verifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) + if err != nil { + return err + } + } else { + // Verify certificate with chain + chain, err := loadCertChainFromFileOrURL(certChain) + if err != nil { + return err + } + co := &cosign.CheckOpts{ + CertEmail: certEmail, + CertOidcIssuer: certOidcIssuer, + } + verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + if err != nil { + return err + } } case ko.BundlePath != "": b, err := cosign.FetchLocalSignedPayloadFromPath(ko.BundlePath) diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index 0c812293baa0..bf0ae43bf8dc 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -58,6 +58,7 @@ cosign dockerfile verify [flags] --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --base-image-only only verify the base image (the last FROM image in the Dockerfile) --cert string path to the public certificate + --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 --check-claims whether to check the claims found (default true) diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index 13c31b1a93f9..a78e08bfb596 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -52,6 +52,7 @@ cosign manifest verify [flags] --attachment string related image attachment to sign (sbom), default none --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --cert string path to the public certificate + --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 --check-claims whether to check the claims found (default true) diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 5f12d0a2f016..d735194151d7 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -62,6 +62,7 @@ cosign verify-attestation [flags] --allow-insecure-registry whether to allow insecure connections to registries. Don't use this for anything but testing --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --cert string path to the public certificate + --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 --check-claims whether to check the claims found (default true) diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index 6086fa010174..17f88e603d04 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -65,6 +65,7 @@ cosign verify-blob [flags] --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --bundle string path to bundle FILE --cert string path to the public certificate + --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 -h, --help help for verify-blob diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index a4a21e0edc5b..dfcd33e3ece9 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -38,6 +38,9 @@ cosign verify [flags] # verify image with an on-disk signed image from 'cosign save' cosign verify --key cosign.pub --local-image + # verify image with local certificate and certificate chain + cosign verify --cert cosign.crt --cert-chain chain.crt + # verify image with public key provided by URL cosign verify --key https://host.for/[FILE] @@ -65,6 +68,7 @@ cosign verify [flags] --attachment string related image attachment to sign (sbom), default none --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --cert string path to the public certificate + --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 --check-claims whether to check the claims found (default true) diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index d9c2e2daf34b..f95cc152ae8c 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -181,6 +181,26 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver return verifier, nil } +// ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Veries that the certificate +// chains up to the provided root. Chain should start with the parent of the certificate and end with the root. +// Optionally verifies the subject of the certificate. +func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certificate, co *CheckOpts) (signature.Verifier, error) { + if len(chain) == 0 { + return nil, errors.New("no chain provided to validate certificate") + } + rootPool := x509.NewCertPool() + rootPool.AddCert(chain[len(chain)-1]) + co.RootCerts = rootPool + + subPool := x509.NewCertPool() + for _, c := range chain[:len(chain)-1] { + subPool.AddCert(c) + } + co.IntermediateCerts = subPool + + return ValidateAndUnpackCert(cert, co) +} + func tlogValidatePublicKey(ctx context.Context, rekorClient *client.Rekor, pub crypto.PublicKey, sig oci.Signature) error { pemBytes, err := cryptoutils.MarshalPublicKeyToPEM(pub) if err != nil { diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 946fbeb58c96..0ff80e7def6c 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -346,6 +346,80 @@ func TestValidateAndUnpackCertInvalidEmail(t *testing.T) { require.Contains(t, err.Error(), "expected email not found in certificate") } +func TestValidateAndUnpackCertWithChainSuccess(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, subCert, subKey) + + co := &CheckOpts{ + CertEmail: subject, + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{subCert, leafCert}, co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } +} + +func TestValidateAndUnpackCertWithChainSuccessWithRoot(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) + + co := &CheckOpts{ + CertEmail: subject, + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{rootCert}, co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } +} + +func TestValidateAndUnpackCertWithChainFailsWithoutChain(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) + + co := &CheckOpts{ + CertEmail: subject, + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{}, co) + if err == nil || err.Error() != "no chain provided to validate certificate" { + t.Errorf("expected error without chain, got %v", err) + } +} + +func TestValidateAndUnpackCertWithChainFailsWithInvalidChain(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) + rootCertOther, _, _ := test.GenerateRootCa() + + co := &CheckOpts{ + CertEmail: subject, + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{rootCertOther}, co) + if err == nil || !strings.Contains(err.Error(), "certificate signed by unknown authority") { + t.Errorf("expected error without valid chain, got %v", err) + } +} + func TestCompareSigs(t *testing.T) { // TODO(nsmith5): Add test cases for invalid signature, missing signature etc tests := []struct { diff --git a/test/e2e_test.go b/test/e2e_test.go index a817f8707f7c..40d5e82d0b4e 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*/, "badsig", blob), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "badsig", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), 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*/, string(sig), bp), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, string(sig), bp), t) + 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) } 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), 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), 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), t) } func TestGenerate(t *testing.T) {