issuance: split linting and issuing (#6788)

It's useful to be able to get a copy of the linting certificate out of
the process, so we can store it in the database for use by processes
that want a certificate-shaped object (for instance, scanning
possibly-issued public keys for a newly discovered weakness in key
generation).

To do this, while still ensuring that it's impossible to issue a
certificate without linting it, return an IssuanceToken from the linting
process. The IssuanceToken has a private field containing the template
that was used for linting. To issue a final certificate, the
IssuanceToken has to be redeemed, and only the template stored in it can
be used.
This commit is contained in:
Jacob Hoffman-Andrews 2023-04-06 13:24:19 -07:00 committed by GitHub
parent d6cd589795
commit b4bdd035ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 158 additions and 37 deletions

View File

@ -319,12 +319,19 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
ca.log.AuditInfof("Signing cert: serial=[%s] regID=[%d] names=[%s] precert=[%s]",
serialHex, req.RegistrationID, names, hex.EncodeToString(precert.Raw))
certDER, err := issuer.Issue(issuanceReq)
_, issuanceToken, err := issuer.Prepare(issuanceReq)
if err != nil {
ca.log.AuditErrf("Preparing cert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, req.RegistrationID, names, err)
return nil, berrors.InternalServerError("failed to prepare certificate signing: %s", err)
}
certDER, err := issuer.Issue(issuanceToken)
if err != nil {
ca.noteSignError(err)
ca.log.AuditErrf("Signing cert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, req.RegistrationID, names, err)
return nil, berrors.InternalServerError("failed to sign precertificate: %s", err)
return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
}
ca.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc()
@ -448,7 +455,14 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
NotAfter: validity.NotAfter,
}
certDER, err := issuer.Issue(req)
_, issuanceToken, err := issuer.Prepare(req)
if err != nil {
ca.log.AuditErrf("Preparing precert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), err)
return nil, nil, nil, berrors.InternalServerError("failed to prepare precertificate signing: %s", err)
}
certDER, err := issuer.Issue(issuanceToken)
if err != nil {
ca.noteSignError(err)
ca.log.AuditErrf("Signing precert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",

View File

@ -19,6 +19,7 @@ import (
"os"
"strconv"
"strings"
"sync"
"time"
ct "github.com/google/certificate-transparency-go"
@ -598,16 +599,30 @@ type IssuanceRequest struct {
SCTList []ct.SignedCertificateTimestamp
}
// Issue generates a certificate from the provided issuance request and
// signs it. Before signing the certificate with the issuer's private
// key, it is signed using a throwaway key so that it can be linted using
// zlint. If the linting fails, an error is returned and the certificate
// is not signed using the issuer's key.
func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
// An issuanceToken represents an assertion that Issuer.Lint has generated
// a linting certificate for a given input and run the linter over it with no
// errors. The token may be redeemed (at most once) to sign a certificate or
// precertificate with the same Issuer's private key, containing the same
// contents that were linted.
type issuanceToken struct {
mu sync.Mutex
template *x509.Certificate
pubKey any
// A pointer to the issuer that created this token. This token may only
// be redeemed by the same issuer.
issuer *Issuer
}
// Prepare applies this Issuer's profile to create a template certificate. It
// then generates a linting certificate from that template and runs the linter
// over it. If successful, returns both the linting certificate (which can be
// stored) and an issuanceToken. The issuanceToken can be used to sign a
// matching certificate with this Issuer's private key.
func (i *Issuer) Prepare(req *IssuanceRequest) ([]byte, *issuanceToken, error) {
// check request is valid according to the issuance profile
err := i.Profile.requestValid(i.Clk, req)
if err != nil {
return nil, err
return nil, nil, err
}
// generate template from the issuance profile
@ -623,7 +638,7 @@ func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
template.AuthorityKeyId = i.Cert.SubjectKeyId
skid, err := generateSKID(req.PublicKey)
if err != nil {
return nil, err
return nil, nil, err
}
template.SubjectKeyId = skid
switch req.PublicKey.(type) {
@ -638,7 +653,7 @@ func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
} else if req.SCTList != nil {
sctListExt, err := generateSCTListExt(req.SCTList)
if err != nil {
return nil, err
return nil, nil, err
}
template.ExtraExtensions = append(template.ExtraExtensions, sctListExt)
}
@ -649,12 +664,35 @@ func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
// check that the tbsCertificate is properly formed by signing it
// with a throwaway key and then linting it using zlint
err = i.Linter.Check(template, req.PublicKey)
lintCertBytes, err := i.Linter.Check(template, req.PublicKey)
if err != nil {
return nil, fmt.Errorf("tbsCertificate linting failed: %w", err)
return nil, nil, fmt.Errorf("tbsCertificate linting failed: %w", err)
}
return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, req.PublicKey, i.Signer)
token := &issuanceToken{sync.Mutex{}, template, req.PublicKey, i}
return lintCertBytes, token, nil
}
// Issue performs a real issuance using an issuanceToken resulting from a
// previous call to Prepare(). Call this at most once per token. Calls after
// the first will receive an error.
func (i *Issuer) Issue(token *issuanceToken) ([]byte, error) {
if token == nil {
return nil, errors.New("nil issuanceToken")
}
token.mu.Lock()
defer token.mu.Unlock()
if token.template == nil {
return nil, errors.New("issuance token already redeemed")
}
template := token.template
token.template = nil
if token.issuer != i {
return nil, errors.New("tried to redeem issuance token with the wrong issuer")
}
return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, token.pubKey, i.Signer)
}
func ContainsMustStaple(extensions []pkix.Extension) bool {

View File

@ -552,7 +552,7 @@ func TestIssue(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := tc.generateFunc()
test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{
lintCertBytes, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
CommonName: "example.com",
@ -560,6 +560,10 @@ func TestIssue(t *testing.T) {
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "Prepare failed")
_, err = x509.ParseCertificate(lintCertBytes)
test.AssertNotError(t, err, "failed to parse certificate")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
@ -591,14 +595,16 @@ func TestIssueRSA(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := rsa.GenerateKey(rand.Reader, 2048)
test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "Issue failed")
test.AssertNotError(t, err, "failed to parse lint certificate")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "failed to parse certificate")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
err = cert.CheckSignatureFrom(issuerCert.Certificate)
@ -635,18 +641,22 @@ func TestIssueCommonName(t *testing.T) {
NotAfter: fc.Now().Add(time.Hour - time.Second),
}
certBytes, err := signer.Issue(ir)
_, issuanceToken, err := signer.Prepare(ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
test.AssertEquals(t, cert.Subject.CommonName, "example.com")
signer.Profile.allowCommonName = false
_, err = signer.Issue(ir)
test.AssertError(t, err, "Issue should have failed")
_, _, err = signer.Prepare(ir)
test.AssertError(t, err, "Prepare should have failed")
ir.CommonName = ""
certBytes, err = signer.Issue(ir)
_, issuanceToken, err = signer.Prepare(ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err = signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err = x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
@ -670,7 +680,7 @@ func TestIssueCTPoison(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
@ -678,6 +688,8 @@ func TestIssueCTPoison(t *testing.T) {
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
@ -708,7 +720,7 @@ func TestIssueSCTList(t *testing.T) {
test.AssertNotError(t, err, "failed to decode ct log ID")
logID2, err := base64.StdEncoding.DecodeString("UtToynGEyMkkXDMQei8Ll54oMwWHI0IieDEKs12/Td4=")
test.AssertNotError(t, err, "failed to decode ct log ID")
certBytes, err := signer.Issue(&IssuanceRequest{
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
@ -725,6 +737,8 @@ func TestIssueSCTList(t *testing.T) {
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
@ -762,7 +776,7 @@ func TestIssueMustStaple(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
@ -770,6 +784,8 @@ func TestIssueMustStaple(t *testing.T) {
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
@ -790,14 +806,14 @@ func TestIssueBadLint(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, err = signer.Issue(&IssuanceRequest{
_, _, err = signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertError(t, err, "Issue didn't fail")
test.AssertError(t, err, "Prepare didn't fail")
test.AssertContains(t, err.Error(), "tbsCertificate linting failed: failed lints")
}
@ -856,3 +872,50 @@ func TestLoadChain_InvalidSig(t *testing.T) {
test.Assert(t, strings.Contains(err.Error(), "signature from \"CN=happy hacker fake CA\""),
fmt.Sprintf("Expected error to mention subject, got: %s", err))
}
func TestIssuanceToken(t *testing.T) {
fc := clock.NewFake()
linter, err := linter.New(issuerCert.Certificate, issuerSigner, []string{})
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
_, err = signer.Issue(&issuanceToken{})
test.AssertError(t, err, "expected issuance with a zero token to fail")
_, err = signer.Issue(nil)
test.AssertError(t, err, "expected issuance with a nil token to fail")
pk, err := rsa.GenerateKey(rand.Reader, 2048)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "expected Prepare to succeed")
_, err = signer.Issue(issuanceToken)
test.AssertNotError(t, err, "expected first issuance to succeed")
_, err = signer.Issue(issuanceToken)
test.AssertError(t, err, "expected second issuance with the same issuance token to fail")
test.AssertContains(t, err.Error(), "issuance token already redeemed")
_, issuanceToken, err = signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "expected Prepare to succeed")
signer2, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
_, err = signer2.Issue(issuanceToken)
test.AssertError(t, err, "expected redeeming an issuance token with the wrong issuer to fail")
test.AssertContains(t, err.Error(), "wrong issuer")
}

View File

@ -33,7 +33,8 @@ func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x5
if err != nil {
return err
}
return linter.Check(tbs, subjectPubKey)
_, err = linter.Check(tbs, subjectPubKey)
return err
}
// Linter is capable of linting a to-be-signed (TBS) certificate. It does so by
@ -69,14 +70,19 @@ func New(realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []str
// Check signs the given TBS certificate using the Linter's fake issuer cert and
// private key, then runs the resulting certificate through all non-filtered
// lints. It returns an error if any lint fails.
func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey) error {
cert, err := makeLintCert(tbs, subjectPubKey, l.issuer, l.signer)
// lints. It returns an error if any lint fails. On success it also returns the
// DER bytes of the linting certificate.
func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey) ([]byte, error) {
lintCertBytes, cert, err := makeLintCert(tbs, subjectPubKey, l.issuer, l.signer)
if err != nil {
return err
return nil, err
}
lintRes := zlint.LintCertificateEx(cert, l.registry)
return ProcessResultSet(lintRes)
err = ProcessResultSet(lintRes)
if err != nil {
return nil, err
}
return lintCertBytes, nil
}
// CheckCRL signs the given RevocationList template using the Linter's fake
@ -178,16 +184,16 @@ func makeRegistry(skipLints []string) (lint.Registry, error) {
return reg, nil
}
func makeLintCert(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, issuer *x509.Certificate, signer crypto.Signer) (*zlintx509.Certificate, error) {
func makeLintCert(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, issuer *x509.Certificate, signer crypto.Signer) ([]byte, *zlintx509.Certificate, error) {
lintCertBytes, err := x509.CreateCertificate(rand.Reader, tbs, issuer, subjectPubKey, signer)
if err != nil {
return nil, fmt.Errorf("failed to create lint certificate: %w", err)
return nil, nil, fmt.Errorf("failed to create lint certificate: %w", err)
}
lintCert, err := zlintx509.ParseCertificate(lintCertBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse lint certificate: %w", err)
return nil, nil, fmt.Errorf("failed to parse lint certificate: %w", err)
}
return lintCert, nil
return lintCertBytes, lintCert, nil
}
func ProcessResultSet(lintRes *zlint.ResultSet) error {