diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index 7c27fc01d47..fdcf524c49e 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 14fcfb7f173..07c213cbe58 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 782d6a8bb59..f801f4f801a 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 e518aada94b..680e30239a1 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 90770155177..42e24933528 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,6 +141,24 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if err != nil { return err } + // Verify certificate with chain + // First intermediate at chain[0], root at chain[n-1] + if c.CertChain != "" { + certs, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + rootPool := x509.NewCertPool() + rootPool.AddCert(certs[len(certs)-1]) + subPool := x509.NewCertPool() + for _, c := range certs[:len(certs)-1] { + subPool.AddCert(c) + } + err = cosign.TrustedCert(cert, rootPool, subPool) + if err != nil { + return err + } + } pubKey, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) if err != nil { return err @@ -296,3 +316,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 8d414df0363..ffda75251c5 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -19,6 +19,7 @@ import ( "context" "crypto" "crypto/ecdsa" + "crypto/x509" "encoding/base64" "encoding/json" "flag" @@ -51,6 +52,7 @@ type VerifyAttestationCommand struct { CertRef string CertEmail string CertOidcIssuer string + CertChain string KeyRef string Sk bool Slot string @@ -121,6 +123,24 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e if err != nil { return errors.Wrap(err, "loading certificate from reference") } + // Verify certificate with chain + // First intermediate at chain[0], root at chain[n-1] + if c.CertChain != "" { + certs, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + rootPool := x509.NewCertPool() + rootPool.AddCert(certs[len(certs)-1]) + subPool := x509.NewCertPool() + for _, c := range certs[:len(certs)-1] { + subPool.AddCert(c) + } + err = cosign.TrustedCert(cert, rootPool, subPool) + if err != nil { + return err + } + } co.SigVerifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) 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 78e5ade6e38..c19c0182f18 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,6 +104,24 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer if err != nil { return err } + // Verify certificate with chain + // First intermediate at chain[0], root at chain[n-1] + if certChain != "" { + certs, err := loadCertChainFromFileOrURL(certChain) + if err != nil { + return err + } + rootPool := x509.NewCertPool() + rootPool.AddCert(certs[len(certs)-1]) + subPool := x509.NewCertPool() + for _, c := range certs[:len(certs)-1] { + subPool.AddCert(c) + } + err = cosign.TrustedCert(cert, rootPool, subPool) + if err != nil { + return err + } + } verifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) if err != nil { return err diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index 0c812293baa..bf0ae43bf8d 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 13c31b1a93f..a78e08bfb59 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 5f12d0a2f01..d735194151d 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 6086fa01017..17f88e603d0 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 a4a21e0edc5..dfcd33e3ece 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/test/e2e_test.go b/test/e2e_test.go index cabe048e38c..d029a034008 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) {