Skip to content

Commit

Permalink
Add support for certificate chain to verify certificate (sigstore#1659)
Browse files Browse the repository at this point in the history
* Add support for certificate chain to verify certificate

This adds a flag to provide a chain of CA certificates to verify a
certificate provided by flag. Callers should include a chain from the
parent of the certificate to the root.

While it'd be ideal to force the root to be specified out of band, by
TUF, that code is currently intertwined with expectations around Fulcio
and Rekor usage.

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Fix test

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Use function to validate certificate chain

This also checks if the certificate matches a subject provided via flag.

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
haydentherapper authored and mlieberman85 committed May 6, 2022
1 parent 3add96b commit a886b99
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 18 deletions.
1 change: 1 addition & 0 deletions cmd/cosign/cli/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions cmd/cosign/cli/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions cmd/cosign/cli/options/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type CertVerifyOptions struct {
Cert string
CertEmail string
CertOidcIssuer string
CertChain string
}

var _ Interface = (*RekorOptions)(nil)
Expand All @@ -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")
}
7 changes: 6 additions & 1 deletion cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PATH>
# verify image with local certificate and certificate chain
cosign verify --cert cosign.crt --cert-chain chain.crt <IMAGE>
# verify image with public key provided by URL
cosign verify --key https://host.for/[FILE] <IMAGE>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
32 changes: 29 additions & 3 deletions cmd/cosign/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package verify

import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
Expand Down Expand Up @@ -53,6 +54,7 @@ type VerifyCommand struct {
CertRef string
CertEmail string
CertOidcIssuer string
CertChain string
Sk bool
Slot string
Output string
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
19 changes: 16 additions & 3 deletions cmd/cosign/cli/verify/verify_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type VerifyAttestationCommand struct {
CertRef string
CertEmail string
CertOidcIssuer string
CertChain string
KeyRef string
Sk bool
Slot string
Expand Down Expand Up @@ -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")
}
}
}

Expand Down
24 changes: 20 additions & 4 deletions cmd/cosign/cli/verify/verify_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions doc/cosign_dockerfile_verify.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/cosign_manifest_verify.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/cosign_verify-attestation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/cosign_verify-blob.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions doc/cosign_verify.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions pkg/cosign/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
74 changes: 74 additions & 0 deletions pkg/cosign/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit a886b99

Please sign in to comment.