Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ca/linter: Port safety checks from ceremony tool #7498

Merged
merged 6 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,13 +419,18 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
ca.log.AuditInfof("Signing cert: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] precert=[%s]",
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, hex.EncodeToString(precert.Raw))

_, issuanceToken, err := issuer.Prepare(certProfile.profile, issuanceReq)
lintCertBytes, issuanceToken, err := issuer.Prepare(certProfile.profile, issuanceReq)
if err != nil {
ca.log.AuditErrf("Preparing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
return nil, berrors.InternalServerError("failed to prepare certificate signing: %s", err)
}

lintCert, err := x509.ParseCertificate(lintCertBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse lint certificate bytes: %s", err)
}

certDER, err := issuer.Issue(issuanceToken)
if err != nil {
ca.noteSignError(err)
Expand All @@ -434,6 +439,16 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
}

cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate bytes: %s", err)
}

err = verifyTBSCertificateDeterminism(lintCert, cert)
if err != nil {
return nil, err
}

ca.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc()
ca.log.AuditInfof("Signing cert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
issuer.Name(), serialHex, req.RegistrationID, names, hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
Expand Down Expand Up @@ -590,6 +605,11 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
return nil, nil, berrors.InternalServerError("failed to prepare precertificate signing: %s", err)
}

lintCert, err := x509.ParseCertificate(lintCertBytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse lint precertificate bytes: %s", err)
}

_, err = ca.sa.AddPrecertificate(context.Background(), &sapb.AddCertificateRequest{
Der: lintCertBytes,
RegID: issueReq.RegistrationID,
Expand All @@ -609,9 +629,34 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
return nil, nil, berrors.InternalServerError("failed to sign precertificate: %s", err)
}

cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse precertificate bytes: %s", err)
}

err = verifyTBSCertificateDeterminism(lintCert, cert)
if err != nil {
return nil, nil, err
}

ca.signatureCount.With(prometheus.Labels{"purpose": string(precertType), "issuer": issuer.Name()}).Inc()
ca.log.AuditInfof("Signing precert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] precertificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash)

return certDER, &certProfileWithID{certProfile.name, certProfile.hash, nil}, nil
}

// verifyTBSCertificateDeterminism verifies that x509.CreateCertificate signing
// operation is deterministic and produced identical DER bytes between the given
// lintCert and leafCert. If this fails it's mississuance, but it's better to
// know about the problem sooner than later.
func verifyTBSCertificateDeterminism(lintCert *x509.Certificate, leafCert *x509.Certificate) error {
pgporada marked this conversation as resolved.
Show resolved Hide resolved
if core.IsAnyNilOrZero(lintCert, leafCert) {
return fmt.Errorf("lintCertBytes and certBytes were nil")
}
if !bytes.Equal(lintCert.RawTBSCertificate, leafCert.RawTBSCertificate) {
return fmt.Errorf("mismatch between lintCert and leafCert RawTBSCertificate DER bytes: \"%x\" != \"%x\"", lintCert.RawTBSCertificate, leafCert.RawTBSCertificate)
}

return nil
}
76 changes: 76 additions & 0 deletions ca/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/asn1"
"errors"
"fmt"
"math/big"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -1300,3 +1301,78 @@ func TestGenerateSKID(t *testing.T) {
test.AssertEquals(t, cap(sha256skid), 20)
features.Reset()
}

func TestVerifyTBSCertificateDeterminism(t *testing.T) {
t.Parallel()

// Create first keypair and cert
testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "unable to generate ECDSA private key")
template := &x509.Certificate{
NotAfter: time.Now().Add(1 * time.Hour),
DNSNames: []string{"example.com"},
SerialNumber: big.NewInt(1),
}
certDer1, err := x509.CreateCertificate(rand.Reader, template, template, &testKey.PublicKey, testKey)
test.AssertNotError(t, err, "unable to create certificate")
parsedCert1, err := x509.ParseCertificate(certDer1)
test.AssertNotError(t, err, "unable to parse DER bytes")

// Create second keypair and cert
testKey2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "unable to generate ECDSA private key")
template2 := &x509.Certificate{
NotAfter: time.Now().Add(2 * time.Hour),
DNSNames: []string{"example.net"},
SerialNumber: big.NewInt(2),
}
certDer2, err := x509.CreateCertificate(rand.Reader, template2, template2, &testKey2.PublicKey, testKey2)
test.AssertNotError(t, err, "unable to create certificate")
parsedCert2, err := x509.ParseCertificate(certDer2)
test.AssertNotError(t, err, "unable to parse DER bytes")

testCases := []struct {
name string
leafCert *x509.Certificate
lintCert *x509.Certificate
errorExpected bool
}{
{
name: "Both nil",
leafCert: nil,
lintCert: nil,
errorExpected: true,
},
{
name: "Missing a value",
leafCert: parsedCert1,
lintCert: nil,
errorExpected: true,
},
{
name: "Mismatch",
leafCert: parsedCert1,
lintCert: parsedCert2,
errorExpected: true,
},
{
name: "Valid",
leafCert: parsedCert1,
lintCert: parsedCert1,
},
}

for _, testCase := range testCases {
// TODO(#7454) Remove this rebinding
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
err := verifyTBSCertificateDeterminism(testCase.lintCert, testCase.leafCert)
if testCase.errorExpected {
test.AssertError(t, err, "your lack of errors is disturbing")
} else {
test.AssertNotError(t, err, "unexpected error")
}
})
}
}
11 changes: 0 additions & 11 deletions cmd/ceremony/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,10 +645,6 @@ func rootCeremony(configBytes []byte) error {
if err != nil {
return err
}
// Verify that the lintCert is self-signed.
if !bytes.Equal(lintCert.RawSubject, lintCert.RawIssuer) {
return fmt.Errorf("mismatch between self-signed lintCert RawSubject and RawIssuer DER bytes: \"%x\" != \"%x\"", lintCert.RawSubject, lintCert.RawIssuer)
}
finalCert, err := signAndWriteCert(template, template, lintCert, keyInfo.key, signer, config.Outputs.CertificatePath)
if err != nil {
return err
Expand Down Expand Up @@ -697,10 +693,6 @@ func intermediateCeremony(configBytes []byte, ct certType) error {
if err != nil {
return err
}
// Verify that the lintCert (and therefore the eventual finalCert) corresponds to the specified issuer certificate.
if !bytes.Equal(issuer.RawSubject, lintCert.RawIssuer) {
return fmt.Errorf("mismatch between issuer RawSubject and lintCert RawIssuer DER bytes: \"%x\" != \"%x\"", issuer.RawSubject, lintCert.RawIssuer)
}
finalCert, err := signAndWriteCert(template, issuer, lintCert, pub, signer, config.Outputs.CertificatePath)
if err != nil {
return err
Expand Down Expand Up @@ -780,9 +772,6 @@ func crossCertCeremony(configBytes []byte, ct certType) error {
if lintCert.NotBefore.Before(toBeCrossSigned.NotBefore) {
return fmt.Errorf("cross-signed subordinate CA's NotBefore predates the existing CA's NotBefore")
}
if !bytes.Equal(issuer.RawSubject, lintCert.RawIssuer) {
return fmt.Errorf("mismatch between issuer RawSubject and lintCert RawIssuer DER bytes: \"%x\" != \"%x\"", issuer.RawSubject, lintCert.RawIssuer)
}
// BR 7.1.2.2.3 Cross-Certified Subordinate CA Extensions
if !slices.Equal(lintCert.ExtKeyUsage, toBeCrossSigned.ExtKeyUsage) {
return fmt.Errorf("lint cert and toBeCrossSigned cert EKUs differ")
Expand Down
11 changes: 11 additions & 0 deletions linter/linter.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package linter

import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
Expand Down Expand Up @@ -239,6 +240,16 @@ func makeLintCert(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, issuer
if err != nil {
return nil, nil, fmt.Errorf("failed to parse lint certificate: %w", err)
}
// RFC 5280, Sections 4.1.2.6 and 8
//
// When the subject of the certificate is a CA, the subject
// field MUST be encoded in the same way as it is encoded in the
// issuer field (Section 4.1.2.4) in all certificates issued by
// the subject CA.
if !bytes.Equal(issuer.RawSubject, lintCert.RawIssuer) {
return nil, nil, fmt.Errorf("mismatch between lint issuer RawSubject and lintCert.RawIssuer DER bytes: \"%x\" != \"%x\"", issuer.RawSubject, lintCert.RawIssuer)
}

return lintCertBytes, lintCert, nil
}

Expand Down