From 8709a62b671f1a48742ec384a44b6f652db2c61e Mon Sep 17 00:00:00 2001 From: Hayden B Date: Wed, 23 Mar 2022 04:21:52 -0700 Subject: [PATCH] Add support for intermediate certificates when verifiying (#1631) * Add support for intermediate certificates when verifiying This adds an intermediate CA certificate pool to CheckOpts, allowing for those using the Cosign library to pass intermediate CA certificates to validate a certificate chain. Signed-off-by: Hayden Blauzvern * Populate intermediate certs from the OCI chain annotation This adds support for verifying OCI signatures with chains that include more than one certificate in the chain, a root and subordinate. Signed-off-by: Hayden Blauzvern --- pkg/cosign/verify.go | 41 +++++++- pkg/cosign/verify_test.go | 195 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 4 deletions(-) diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 70846fd3af6d..d9c2e2daf34b 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -75,6 +75,8 @@ type CheckOpts struct { // RootCerts are the root CA certs used to verify a signature's chained certificate. RootCerts *x509.CertPool + // IntermediateCerts are the optional intermediate CA certs used to verify a certificate chain. + IntermediateCerts *x509.CertPool // CertEmail is the email expected for a certificate to be valid. The empty string means any certificate can be valid. CertEmail string // CertOidcIssuer is the OIDC issuer expected for a certificate to be valid. The empty string means any certificate can be valid. @@ -149,7 +151,7 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver } // Now verify the cert, then the signature. - if err := TrustedCert(cert, co.RootCerts); err != nil { + if err := TrustedCert(cert, co.RootCerts, co.IntermediateCerts); err != nil { return nil, err } if co.CertEmail != "" { @@ -350,6 +352,21 @@ func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co if cert == nil { return bundleVerified, errors.New("no certificate found on signature") } + // Create a certificate pool for intermediate CA certificates, excluding the root + chain, err := sig.Chain() + if err != nil { + return bundleVerified, err + } + // If the chain annotation is not present or there is only a root + if chain == nil || len(chain) <= 1 { + co.IntermediateCerts = nil + } else { + pool := x509.NewCertPool() + for _, cert := range chain[:len(chain)-1] { + pool.AddCert(cert) + } + co.IntermediateCerts = pool + } verifier, err = ValidateAndUnpackCert(cert, co) if err != nil { return bundleVerified, err @@ -513,6 +530,21 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash if cert == nil { return errors.New("no certificate found on attestation") } + // Create a certificate pool for intermediate CA certificates, excluding the root + chain, err := att.Chain() + if err != nil { + return err + } + // If the chain annotation is not present or there is only a root + if chain == nil || len(chain) <= 1 { + co.IntermediateCerts = nil + } else { + pool := x509.NewCertPool() + for _, cert := range chain[:len(chain)-1] { + pool.AddCert(cert) + } + co.IntermediateCerts = pool + } verifier, err = ValidateAndUnpackCert(cert, co) if err != nil { return err @@ -783,13 +815,14 @@ func VerifySET(bundlePayload cbundle.RekorPayload, signature []byte, pub *ecdsa. return nil } -func TrustedCert(cert *x509.Certificate, roots *x509.CertPool) error { +func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) error { if _, err := cert.Verify(x509.VerifyOptions{ // THIS IS IMPORTANT: WE DO NOT CHECK TIMES HERE // THE CERTIFICATE IS TREATED AS TRUSTED FOREVER // WE CHECK THAT THE SIGNATURES WERE CREATED DURING THIS WINDOW - CurrentTime: cert.NotBefore, - Roots: roots, + CurrentTime: cert.NotBefore, + Roots: roots, + Intermediates: intermediates, KeyUsages: []x509.ExtKeyUsage{ x509.ExtKeyUsageCodeSigning, }, diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index a9e9b52264bc..946fbeb58c96 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -17,12 +17,17 @@ package cosign import ( "context" "crypto" + "crypto/rand" + "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" + "encoding/pem" "io" + "strings" "testing" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/secure-systems-lab/go-securesystemslib/dsse" @@ -63,6 +68,15 @@ func (m *mockAttestation) Annotations() (map[string]string, error) { func (m *mockAttestation) Payload() ([]byte, error) { return json.Marshal(m.payload) } + +func appendSlices(slices [][]byte) []byte { + var tmp []byte + for _, s := range slices { + tmp = append(tmp, s...) + } + return tmp +} + func Test_verifyOCIAttestation(t *testing.T) { stmt, err := json.Marshal(in_toto.ProvenanceStatement{}) if err != nil { @@ -94,6 +108,140 @@ func Test_verifyOCIAttestation(t *testing.T) { } } +func TestVerifyImageSignature(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + payload := []byte{1, 2, 3, 4} + h := sha256.Sum256(payload) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + + ociSig, _ := static.NewSignature(payload, + base64.StdEncoding.EncodeToString(signature), + static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub, pemRoot}))) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool}) + if err != nil { + t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) + } + // TODO: Create fake bundle and test verification + if verified == true { + t.Fatalf("expected verified=false, got verified=true") + } +} + +func TestVerifyImageSignatureMultipleSubs(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + subCert1, subKey1, _ := test.GenerateSubordinateCa(rootCert, rootKey) + subCert2, subKey2, _ := test.GenerateSubordinateCa(subCert1, subKey1) + subCert3, subKey3, _ := test.GenerateSubordinateCa(subCert2, subKey2) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert3, subKey3) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert1.Raw}) + pemSub2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert2.Raw}) + pemSub3 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert3.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + payload := []byte{1, 2, 3, 4} + h := sha256.Sum256(payload) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + + ociSig, _ := static.NewSignature(payload, + base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub3, pemSub2, pemSub1, pemRoot}))) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool}) + if err != nil { + t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) + } + // TODO: Create fake bundle and test verification + if verified == true { + t.Fatalf("expected verified=false, got verified=true") + } +} + +func TestVerifyImageSignatureWithNoChain(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + payload := []byte{1, 2, 3, 4} + h := sha256.Sum256(payload) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + + ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, []byte{})) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool}) + if err != nil { + t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) + } + // TODO: Create fake bundle and test verification + if verified == true { + t.Fatalf("expected verified=false, got verified=true") + } +} + +func TestVerifyImageSignatureWithOnlyRoot(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + payload := []byte{1, 2, 3, 4} + h := sha256.Sum256(payload) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + + ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, pemRoot)) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool}) + if err != nil { + t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) + } + // TODO: Create fake bundle and test verification + if verified == true { + t.Fatalf("expected verified=false, got verified=true") + } +} + +func TestVerifyImageSignatureWithMissingSub(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + payload := []byte{1, 2, 3, 4} + h := sha256.Sum256(payload) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + + ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, pemRoot)) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool}) + if err == nil { + t.Fatal("expected error while verifying signature") + } + if !strings.Contains(err.Error(), "certificate signed by unknown authority") { + t.Fatal("expected error while verifying signature") + } + // TODO: Create fake bundle and test verification + if verified == true { + t.Fatalf("expected verified=false, got verified=true") + } +} + func TestValidateAndUnpackCertSuccess(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" @@ -234,3 +382,50 @@ func TestCompareSigs(t *testing.T) { }) } } + +func TestTrustedCertSuccess(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + leafCert, _, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + subPool := x509.NewCertPool() + subPool.AddCert(subCert) + + err := TrustedCert(leafCert, rootPool, subPool) + if err != nil { + t.Fatalf("expected no error verifying certificate, got %v", err) + } +} + +func TestTrustedCertSuccessNoIntermediates(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + err := TrustedCert(leafCert, rootPool, nil) + if err != nil { + t.Fatalf("expected no error verifying certificate, got %v", err) + } +} + +// Tests that verification succeeds if both a root and subordinate pool are +// present, but a chain is built with only the leaf and root certificates. +func TestTrustedCertSuccessChainFromRoot(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) + subCert, _, _ := test.GenerateSubordinateCa(rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + subPool := x509.NewCertPool() + subPool.AddCert(subCert) + + err := TrustedCert(leafCert, rootPool, subPool) + if err != nil { + t.Fatalf("expected no error verifying certificate, got %v", err) + } +}