From 0fcdf1949397bef7aecb868c41e3fe6bc908a62c Mon Sep 17 00:00:00 2001 From: Hayden B <hblauzvern@gmail.com> Date: Sun, 3 Apr 2022 12:38:33 -0700 Subject: [PATCH] Add support for providing certificate chain for X509 signature types (#747) This allows you to create an entry for the entire certificate chain, not just the leaf certificate. The certificate chain will be verified before adding the entry. Signed-off-by: Hayden Blauzvern <hblauzvern@google.com> --- pkg/pki/x509/testutils/cert_test_utils.go | 146 ++++++++++++++++++++++ pkg/pki/x509/x509.go | 66 +++++++++- pkg/pki/x509/x509_test.go | 80 ++++++++++++ 3 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 pkg/pki/x509/testutils/cert_test_utils.go diff --git a/pkg/pki/x509/testutils/cert_test_utils.go b/pkg/pki/x509/testutils/cert_test_utils.go new file mode 100644 index 0000000..827505c --- /dev/null +++ b/pkg/pki/x509/testutils/cert_test_utils.go @@ -0,0 +1,146 @@ +// Copyright 2022 The Sigstore Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "math/big" + "time" +) + +/* +To use: + +rootCert, rootKey, _ := GenerateRootCa() +subCert, subKey, _ := GenerateSubordinateCa(rootCert, rootKey) +leafCert, _, _ := GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) + +roots := x509.NewCertPool() +subs := x509.NewCertPool() +roots.AddCert(rootCert) +subs.AddCert(subCert) +opts := x509.VerifyOptions{ + Roots: roots, + Intermediates: subs, + KeyUsages: []x509.ExtKeyUsage{ + x509.ExtKeyUsageCodeSigning, + }, +} +_, err := leafCert.Verify(opts) +*/ + +func createCertificate(template *x509.Certificate, parent *x509.Certificate, pub interface{}, priv crypto.Signer) (*x509.Certificate, error) { + certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, pub, priv) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, err + } + return cert, nil +} + +func GenerateRootCa() (*x509.Certificate, *ecdsa.PrivateKey, error) { + rootTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "sigstore", + Organization: []string{"sigstore.dev"}, + }, + NotBefore: time.Now().Add(-5 * time.Minute), + NotAfter: time.Now().Add(5 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + cert, err := createCertificate(rootTemplate, rootTemplate, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + + return cert, priv, nil +} + +func GenerateSubordinateCa(rootTemplate *x509.Certificate, rootPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) { + subTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "sigstore-sub", + Organization: []string{"sigstore.dev"}, + }, + NotBefore: time.Now().Add(-2 * time.Minute), + NotAfter: time.Now().Add(2 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + IsCA: true, + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + cert, err := createCertificate(subTemplate, rootTemplate, &priv.PublicKey, rootPriv) + if err != nil { + return nil, nil, err + } + + return cert, priv, nil +} + +func GenerateLeafCert(subject string, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) { + certTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + EmailAddresses: []string{subject}, + NotBefore: time.Now().Add(-1 * time.Minute), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + IsCA: false, + ExtraExtensions: []pkix.Extension{{ + // OID for OIDC Issuer extension + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + Critical: false, + Value: []byte(oidcIssuer), + }}, + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv) + if err != nil { + return nil, nil, err + } + + return cert, priv, nil +} diff --git a/pkg/pki/x509/x509.go b/pkg/pki/x509/x509.go index f12c081..a92d495 100644 --- a/pkg/pki/x509/x509.go +++ b/pkg/pki/x509/x509.go @@ -69,7 +69,17 @@ func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOptio p := key.key if p == nil { - p = key.cert.c.PublicKey + switch { + case key.cert != nil: + p = key.cert.c.PublicKey + case len(key.certs) > 0: + if err := verifyCertChain(key.certs); err != nil { + return err + } + p = key.certs[0].PublicKey + default: + return errors.New("no public key found") + } } verifier, err := sigsig.LoadVerifier(p, crypto.SHA256) @@ -81,8 +91,9 @@ func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOptio // PublicKey Public Key that follows the x509 standard type PublicKey struct { - key interface{} - cert *cert + key interface{} + cert *cert + certs []*x509.Certificate } type cert struct { @@ -97,11 +108,20 @@ func NewPublicKey(r io.Reader) (*PublicKey, error) { return nil, err } - block, _ := pem.Decode(rawPub) + block, rest := pem.Decode(rawPub) if block == nil { return nil, errors.New("invalid public key: failure decoding PEM") } + // Handle certificate chain, concatenated PEM-encoded certificates + if len(rest) > 0 { + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(rawPub) + if err != nil { + return nil, err + } + return &PublicKey{certs: certs}, nil + } + switch block.Type { case string(cryptoutils.PublicKeyPEMType): key, err := x509.ParsePKIXPublicKey(block.Bytes) @@ -131,6 +151,8 @@ func (k PublicKey) CanonicalValue() (encoded []byte, err error) { encoded, err = cryptoutils.MarshalPublicKeyToPEM(k.key) case k.cert != nil: encoded, err = cryptoutils.MarshalCertificateToPEM(k.cert.c) + case k.certs != nil: + encoded, err = cryptoutils.MarshalCertificatesToPEM(k.certs) default: err = fmt.Errorf("x509 public key has not been initialized") } @@ -142,14 +164,23 @@ func (k PublicKey) CryptoPubKey() crypto.PublicKey { if k.cert != nil { return k.cert.c.PublicKey } + if len(k.certs) > 0 { + return k.certs[0].PublicKey + } return k.key } // EmailAddresses implements the pki.PublicKey interface func (k PublicKey) EmailAddresses() []string { var names []string + var cert *x509.Certificate if k.cert != nil { - for _, name := range k.cert.c.EmailAddresses { + cert = k.cert.c + } else if len(k.certs) > 0 { + cert = k.certs[0] + } + if cert != nil { + for _, name := range cert.EmailAddresses { validate := validator.New() errs := validate.Var(name, "required,email") if errs == nil { @@ -160,6 +191,31 @@ func (k PublicKey) EmailAddresses() []string { return names } +func verifyCertChain(certChain []*x509.Certificate) error { + if len(certChain) == 0 { + return errors.New("no certificate chain provided") + } + // No certificate chain to verify + if len(certChain) == 1 { + return nil + } + rootPool := x509.NewCertPool() + rootPool.AddCert(certChain[len(certChain)-1]) + subPool := x509.NewCertPool() + for _, c := range certChain[1 : len(certChain)-1] { + subPool.AddCert(c) + } + if _, err := certChain[0].Verify(x509.VerifyOptions{ + Roots: rootPool, + Intermediates: subPool, + // Allow any key usage + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }); err != nil { + return err + } + return nil +} + func CertChainToPEM(certChain []*x509.Certificate) ([]byte, error) { var pemBytes bytes.Buffer for _, cert := range certChain { diff --git a/pkg/pki/x509/x509_test.go b/pkg/pki/x509/x509_test.go index c64dcb5..bb1f9ff 100644 --- a/pkg/pki/x509/x509_test.go +++ b/pkg/pki/x509/x509_test.go @@ -19,10 +19,13 @@ import ( "bytes" "context" "crypto" + "crypto/ecdsa" "crypto/x509" + "reflect" "strings" "testing" + "github.com/sigstore/rekor/pkg/pki/x509/testutils" "github.com/sigstore/rekor/pkg/signer" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" @@ -242,3 +245,80 @@ func TestCertChain_Verify(t *testing.T) { } } } + +func TestPublicKeyWithCertChain(t *testing.T) { + rootCert, rootKey, _ := testutils.GenerateRootCa() + subCert, subKey, _ := testutils.GenerateSubordinateCa(rootCert, rootKey) + leafCert, leafKey, _ := testutils.GenerateLeafCert("subject@example.com", "oidc-issuer", subCert, subKey) + + pemCertChain, err := cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, subCert, rootCert}) + if err != nil { + t.Fatalf("unexpected error marshalling cert chain: %v", err) + } + + pub, err := NewPublicKey(bytes.NewReader(pemCertChain)) + if err != nil { + t.Fatalf("unexpected error generating public key: %v", err) + } + if pub.certs == nil || !pub.certs[0].Equal(leafCert) || !pub.certs[1].Equal(subCert) || !pub.certs[2].Equal(rootCert) { + t.Fatal("expected certificate chain to match provided certificate chain") + } + + if !pub.CryptoPubKey().(*ecdsa.PublicKey).Equal(leafKey.Public()) { + t.Fatal("expected public keys to match") + } + + if !reflect.DeepEqual(pub.EmailAddresses(), leafCert.EmailAddresses) { + t.Fatalf("expected matching email addresses, expected %v, got %v", leafCert.EmailAddresses, pub.EmailAddresses()) + } + + canonicalValue, err := pub.CanonicalValue() + if err != nil { + t.Fatalf("unexpected error fetching canonical value: %v", err) + } + if !reflect.DeepEqual(canonicalValue, pemCertChain) { + t.Fatalf("expected canonical value %v, got %v", pemCertChain, canonicalValue) + } + + // Generate signature to verify + data := []byte("test") + signer, err := signature.LoadSigner(leafKey, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + sigBytes, err := signer.SignMessage(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + s, err := NewSignature(bytes.NewReader(sigBytes)) + if err != nil { + t.Fatalf("unexpected error generating signature: %v", err) + } + err = s.Verify(bytes.NewReader(data), pub) + if err != nil { + t.Fatalf("unexpected error verifying signature, %v", err) + } + + // Verify error with invalid chain + pemCertChain, _ = cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, rootCert}) + pub, _ = NewPublicKey(bytes.NewReader(pemCertChain)) + signer, _ = signature.LoadSigner(leafKey, crypto.SHA256) + sigBytes, _ = signer.SignMessage(bytes.NewReader(data)) + s, _ = NewSignature(bytes.NewReader(sigBytes)) + err = s.Verify(bytes.NewReader(data), pub) + if err == nil || !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { + t.Fatalf("expected error verifying signature, got %v", err) + } + + // Verify works with chain without intermediate + leafCert, leafKey, _ = testutils.GenerateLeafCert("subject@example.com", "oidc-issuer", rootCert, rootKey) + pemCertChain, _ = cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, rootCert}) + pub, _ = NewPublicKey(bytes.NewReader(pemCertChain)) + signer, _ = signature.LoadSigner(leafKey, crypto.SHA256) + sigBytes, _ = signer.SignMessage(bytes.NewReader(data)) + s, _ = NewSignature(bytes.NewReader(sigBytes)) + err = s.Verify(bytes.NewReader(data), pub) + if err != nil { + t.Fatalf("unexpected error verifying signature, %v", err) + } +} -- GitLab