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 3 commits
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
84 changes: 83 additions & 1 deletion ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"github.com/miekg/pkcs11"
"github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"golang.org/x/crypto/cryptobyte"
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
"golang.org/x/crypto/ocsp"
"google.golang.org/protobuf/types/known/timestamppb"

Expand Down Expand Up @@ -419,7 +421,7 @@ 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)
Expand All @@ -434,6 +436,11 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
}

err = tbsCertIsDeterministic(lintCertBytes, certDER)
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 @@ -609,9 +616,84 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
return nil, nil, berrors.InternalServerError("failed to sign precertificate: %s", err)
}

err = tbsCertIsDeterministic(lintCertBytes, certDER)
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
}

// verifyTBSCertIsDeterministic verifies that x509.CreateCertificate signing
// operation is deterministic and produced identical DER bytes between the given
// lint certificate and leaf certificate. If the DER byte equality check fails
// it's mississuance, but it's better to know about the problem sooner than
// later. The caller is responsible for passing the appropriate valid
// certificate bytes in the correct position.
func tbsCertIsDeterministic(lintCertBytes []byte, leafCertBytes []byte) error {
if core.IsAnyNilOrZero(lintCertBytes, leafCertBytes) {
return fmt.Errorf("lintCertBytes of leafCertBytes were nil")
}

// parseTBSCert is a partial copy of //crypto/x509/parser.go to extract the
// RawTBSCertificate field from given DER bytes. It the RawTBSCertificate
// field bytes or an error if the given bytes cannot be parsed. This is far
// more performant than parsing the entire *Certificate structure with
// x509.ParseCertificate().
parseTBSCert := func(inputDERBytes *[]byte) ([]byte, error) {
pgporada marked this conversation as resolved.
Show resolved Hide resolved
if inputDERBytes == nil {
return nil, errors.New("inputDERBytes was nil")
}

input := cryptobyte.String(*inputDERBytes)
// we read the SEQUENCE including length and tag bytes so that
// we can populate Certificate.Raw, before unwrapping the
// SEQUENCE so it can be operated on
if !input.ReadASN1Element(&input, cryptobyte_asn1.SEQUENCE) {
return nil, errors.New("x509: malformed certificate")
pgporada marked this conversation as resolved.
Show resolved Hide resolved
}
pgporada marked this conversation as resolved.
Show resolved Hide resolved
if !input.ReadASN1(&input, cryptobyte_asn1.SEQUENCE) {
return nil, errors.New("x509: malformed certificate")
}

var tbs cryptobyte.String
// do the same trick again as above to extract the raw
// bytes for Certificate.RawTBSCertificate
if !input.ReadASN1Element(&tbs, cryptobyte_asn1.SEQUENCE) {
return nil, errors.New("x509: malformed tbs certificate")
}
if !tbs.ReadASN1(&tbs, cryptobyte_asn1.SEQUENCE) {
pgporada marked this conversation as resolved.
Show resolved Hide resolved
return nil, errors.New("x509: malformed tbs certificate")
}

if tbs.Empty() {
return nil, errors.New("parsed RawTBSCertificate field was empty")
}

return tbs, nil
}

lintRawTBSCert, err := parseTBSCert(&lintCertBytes)
if err != nil {
return err
pgporada marked this conversation as resolved.
Show resolved Hide resolved
}

leafRawTBSCert, err := parseTBSCert(&leafCertBytes)
if err != nil {
return err
}

if lintRawTBSCert == nil || leafRawTBSCert == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a) don't directly check for nil here, instead check len(foo) == 0
b) no need for this since the anonymous function does the same check on line 673; pick one or the other

Copy link
Member Author

@pgporada pgporada May 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the functional difference between checking for nil or len 0 on a []byte? The zero value of a []byte is going to be nil.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, the true zero value of []byte is nil. But generally the purpose of checks like this is not simply to check for the zero value, but rather to check for obviously invalid inputs, so that more helpful error messages can be displayed and so that we can avoid doing expensive checks if we already know they're going to fail. So a len(foo) == 0 check also catches the case where the byte slice exists but has an obviously-invalid (zero) length.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can get rid of this because tbs.Empty() on line 670 is handling the len(foo) == 0 case anyways.

return errors.New("lintRawTBSCert or leafRawTBSCert nil bytes")
}

if !bytes.Equal(lintRawTBSCert, leafRawTBSCert) {
return fmt.Errorf("mismatch between lintCert and leafCert RawTBSCertificate DER bytes: \"%x\" != \"%x\"", lintRawTBSCert, leafRawTBSCert)
}

return nil
}
102 changes: 102 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,104 @@ func TestGenerateSKID(t *testing.T) {
test.AssertEquals(t, cap(sha256skid), 20)
features.Reset()
}

func TestVerifyTBSCertIsDeterministic(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")

// 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")

testCases := []struct {
name string
lintCertBytes []byte
leafCertBytes []byte
errorExpected bool
errorSubstr string
pgporada marked this conversation as resolved.
Show resolved Hide resolved
}{
{
name: "Both nil",
lintCertBytes: nil,
leafCertBytes: nil,
errorExpected: true,
errorSubstr: "were nil",
},
{
name: "Missing a value, invalid input",
lintCertBytes: nil,
leafCertBytes: []byte{0x6, 0x6, 0x6},
errorExpected: true,
errorSubstr: "were nil",
},
{
name: "Missing a value, valid input",
lintCertBytes: nil,
leafCertBytes: certDer1,
errorExpected: true,
errorSubstr: "were nil",
},
{
name: "Mismatched bytes, invalid input",
lintCertBytes: []byte{0x6, 0x6, 0x6},
leafCertBytes: []byte{0x1, 0x2, 0x3},
errorExpected: true,
errorSubstr: "malformed certificate",
},
{
// This case is an example of when a linting cert's DER bytes are
// mismatched compared to then precert or final cert created from
// that linting cert's DER bytes.
name: "Mismatched bytes, valid input",
lintCertBytes: certDer1,
leafCertBytes: certDer2,
errorExpected: true,
errorSubstr: "mismatch between",
},
{
// Take this with a grain of salt since this test is not actually
// creating a linting certificate and performing two
// x509.CreateCertificate() calls like
// ca.IssueCertificateForPrecertificate and
// ca.issuePrecertificateInner do. However, we're still going to
// verify the equality.
name: "Valid",
lintCertBytes: certDer1,
leafCertBytes: certDer1,
},
}

for _, testCase := range testCases {
// TODO(#7454) Remove this rebinding
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
err := tbsCertIsDeterministic(testCase.lintCertBytes, testCase.leafCertBytes)
if testCase.errorExpected {
test.AssertError(t, err, "your lack of errors is disturbing")
if testCase.errorSubstr != "" {
test.AssertContains(t, fmt.Sprint(err), testCase.errorSubstr)
}
} 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