440 lines
16 KiB
Go
440 lines
16 KiB
Go
package ca
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
ct "github.com/google/certificate-transparency-go"
|
|
cttls "github.com/google/certificate-transparency-go/tls"
|
|
"github.com/jmhodges/clock"
|
|
"github.com/miekg/pkcs11"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"golang.org/x/crypto/ocsp"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
capb "github.com/letsencrypt/boulder/ca/proto"
|
|
"github.com/letsencrypt/boulder/core"
|
|
corepb "github.com/letsencrypt/boulder/core/proto"
|
|
csrlib "github.com/letsencrypt/boulder/csr"
|
|
berrors "github.com/letsencrypt/boulder/errors"
|
|
"github.com/letsencrypt/boulder/features"
|
|
"github.com/letsencrypt/boulder/goodkey"
|
|
"github.com/letsencrypt/boulder/issuance"
|
|
"github.com/letsencrypt/boulder/linter"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
)
|
|
|
|
type certificateType string
|
|
|
|
const (
|
|
precertType = certificateType("precertificate")
|
|
certType = certificateType("certificate")
|
|
)
|
|
|
|
// Two maps of keys to Issuers. Lookup by PublicKeyAlgorithm is useful for
|
|
// determining which issuer to use to sign a given (pre)cert, based on its
|
|
// PublicKeyAlgorithm. Lookup by NameID is useful for looking up the appropriate
|
|
// issuer based on the issuer of a given (pre)certificate.
|
|
type issuerMaps struct {
|
|
byAlg map[x509.PublicKeyAlgorithm]*issuance.Issuer
|
|
byNameID map[issuance.IssuerNameID]*issuance.Issuer
|
|
}
|
|
|
|
// certificateAuthorityImpl represents a CA that signs certificates.
|
|
// It can sign OCSP responses as well, but only via delegation to an ocspImpl.
|
|
type certificateAuthorityImpl struct {
|
|
capb.UnimplementedCertificateAuthorityServer
|
|
sa sapb.StorageAuthorityCertificateClient
|
|
pa core.PolicyAuthority
|
|
issuers issuerMaps
|
|
|
|
// This is temporary, and will be used for testing and slow roll-out
|
|
// of ECDSA issuance, but will then be removed.
|
|
ecdsaAllowList *ECDSAAllowList
|
|
prefix int // Prepended to the serial number
|
|
validityPeriod time.Duration
|
|
backdate time.Duration
|
|
maxNames int
|
|
keyPolicy goodkey.KeyPolicy
|
|
clk clock.Clock
|
|
log blog.Logger
|
|
signatureCount *prometheus.CounterVec
|
|
signErrorCount *prometheus.CounterVec
|
|
lintErrorCount prometheus.Counter
|
|
}
|
|
|
|
// makeIssuerMaps processes a list of issuers into a set of maps, mapping
|
|
// nearly-unique identifiers of those issuers to the issuers themselves. Note
|
|
// that, if two issuers have the same nearly-unique ID, the *latter* one in
|
|
// the input list "wins".
|
|
func makeIssuerMaps(issuers []*issuance.Issuer) issuerMaps {
|
|
issuersByAlg := make(map[x509.PublicKeyAlgorithm]*issuance.Issuer, 2)
|
|
issuersByNameID := make(map[issuance.IssuerNameID]*issuance.Issuer, len(issuers))
|
|
for _, issuer := range issuers {
|
|
for _, alg := range issuer.Algs() {
|
|
// TODO(#5259): Enforce that there is only one issuer for each algorithm,
|
|
// instead of taking the first issuer for each algorithm type.
|
|
if issuersByAlg[alg] == nil {
|
|
issuersByAlg[alg] = issuer
|
|
}
|
|
}
|
|
issuersByNameID[issuer.Cert.NameID()] = issuer
|
|
}
|
|
return issuerMaps{issuersByAlg, issuersByNameID}
|
|
}
|
|
|
|
// NewCertificateAuthorityImpl creates a CA instance that can sign certificates
|
|
// from any number of issuance.Issuers according to their profiles, and can sign
|
|
// OCSP (via delegation to an ocspImpl and its issuers).
|
|
func NewCertificateAuthorityImpl(
|
|
sa sapb.StorageAuthorityCertificateClient,
|
|
pa core.PolicyAuthority,
|
|
boulderIssuers []*issuance.Issuer,
|
|
ecdsaAllowList *ECDSAAllowList,
|
|
certExpiry time.Duration,
|
|
certBackdate time.Duration,
|
|
serialPrefix int,
|
|
maxNames int,
|
|
keyPolicy goodkey.KeyPolicy,
|
|
logger blog.Logger,
|
|
stats prometheus.Registerer,
|
|
signatureCount *prometheus.CounterVec,
|
|
signErrorCount *prometheus.CounterVec,
|
|
clk clock.Clock,
|
|
) (*certificateAuthorityImpl, error) {
|
|
var ca *certificateAuthorityImpl
|
|
var err error
|
|
|
|
// TODO(briansmith): Make the backdate setting mandatory after the
|
|
// production ca.json has been updated to include it. Until then, manually
|
|
// default to 1h, which is the backdating duration we currently use.
|
|
if certBackdate == 0 {
|
|
certBackdate = time.Hour
|
|
}
|
|
|
|
if serialPrefix <= 0 || serialPrefix >= 256 {
|
|
err = errors.New("Must have a positive non-zero serial prefix less than 256 for CA.")
|
|
return nil, err
|
|
}
|
|
|
|
if len(boulderIssuers) == 0 {
|
|
return nil, errors.New("must have at least one issuer")
|
|
}
|
|
|
|
issuers := makeIssuerMaps(boulderIssuers)
|
|
|
|
lintErrorCount := prometheus.NewCounter(
|
|
prometheus.CounterOpts{
|
|
Name: "lint_errors",
|
|
Help: "Number of issuances that were halted by linting errors",
|
|
})
|
|
stats.MustRegister(lintErrorCount)
|
|
|
|
ca = &certificateAuthorityImpl{
|
|
sa: sa,
|
|
pa: pa,
|
|
issuers: issuers,
|
|
validityPeriod: certExpiry,
|
|
backdate: certBackdate,
|
|
prefix: serialPrefix,
|
|
maxNames: maxNames,
|
|
keyPolicy: keyPolicy,
|
|
log: logger,
|
|
signatureCount: signatureCount,
|
|
signErrorCount: signErrorCount,
|
|
lintErrorCount: lintErrorCount,
|
|
clk: clk,
|
|
ecdsaAllowList: ecdsaAllowList,
|
|
}
|
|
|
|
return ca, nil
|
|
}
|
|
|
|
// noteSignError is called after operations that may cause a PKCS11 signing error.
|
|
func (ca *certificateAuthorityImpl) noteSignError(err error) {
|
|
var pkcs11Error *pkcs11.Error
|
|
if errors.As(err, &pkcs11Error) {
|
|
ca.signErrorCount.WithLabelValues("HSM").Inc()
|
|
}
|
|
}
|
|
|
|
var ocspStatusToCode = map[string]int{
|
|
"good": ocsp.Good,
|
|
"revoked": ocsp.Revoked,
|
|
"unknown": ocsp.Unknown,
|
|
}
|
|
|
|
func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, issueReq *capb.IssueCertificateRequest) (*capb.IssuePrecertificateResponse, error) {
|
|
// issueReq.orderID may be zero, for ACMEv1 requests.
|
|
if core.IsAnyNilOrZero(issueReq, issueReq.Csr, issueReq.RegistrationID) {
|
|
return nil, berrors.InternalServerError("Incomplete issue certificate request")
|
|
}
|
|
|
|
serialBigInt, validity, err := ca.generateSerialNumberAndValidity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serialHex := core.SerialToString(serialBigInt)
|
|
regID := issueReq.RegistrationID
|
|
_, err = ca.sa.AddSerial(ctx, &sapb.AddSerialRequest{
|
|
Serial: serialHex,
|
|
RegID: regID,
|
|
Created: timestamppb.New(ca.clk.Now()),
|
|
Expires: timestamppb.New(validity.NotAfter),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
precertDER, _, err := ca.issuePrecertificateInner(ctx, issueReq, serialBigInt, validity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = ca.sa.SetCertificateStatusReady(ctx, &sapb.Serial{Serial: serialHex})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &capb.IssuePrecertificateResponse{
|
|
DER: precertDER,
|
|
}, nil
|
|
}
|
|
|
|
// IssueCertificateForPrecertificate takes a precertificate and a set
|
|
// of SCTs for that precertificate and uses the signer to create and
|
|
// sign a certificate from them. The poison extension is removed and a
|
|
// SCT list extension is inserted in its place. Except for this and the
|
|
// signature the certificate exactly matches the precertificate. After
|
|
// the certificate is signed a OCSP response is generated and the
|
|
// response and certificate are stored in the database.
|
|
//
|
|
// It's critical not to sign two different final certificates for the same
|
|
// precertificate. This can happen, for instance, if the caller provides a
|
|
// different set of SCTs on subsequent calls to IssueCertificateForPrecertificate.
|
|
// We rely on the RA not to call IssueCertificateForPrecertificate twice for the
|
|
// same serial. This is accomplished by the fact that
|
|
// IssueCertificateForPrecertificate is only ever called in a straight-through
|
|
// RPC path without retries. If there is any error, including a networking
|
|
// error, the whole certificate issuance attempt fails and any subsequent
|
|
// issuance will use a different serial number.
|
|
//
|
|
// We also check that the provided serial number does not already exist as a
|
|
// final certificate, but this is just a belt-and-suspenders measure, since
|
|
// there could be race conditions where two goroutines are issuing for the same
|
|
// serial number at the same time.
|
|
func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx context.Context, req *capb.IssueCertificateForPrecertificateRequest) (*corepb.Certificate, error) {
|
|
// issueReq.orderID may be zero, for ACMEv1 requests.
|
|
if core.IsAnyNilOrZero(req, req.DER, req.SCTs, req.RegistrationID) {
|
|
return nil, berrors.InternalServerError("Incomplete cert for precertificate request")
|
|
}
|
|
|
|
precert, err := x509.ParseCertificate(req.DER)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serialHex := core.SerialToString(precert.SerialNumber)
|
|
if _, err = ca.sa.GetCertificate(ctx, &sapb.Serial{Serial: serialHex}); err == nil {
|
|
err = berrors.InternalServerError("issuance of duplicate final certificate requested: %s", serialHex)
|
|
ca.log.AuditErr(err.Error())
|
|
return nil, err
|
|
} else if !errors.Is(err, berrors.NotFound) {
|
|
return nil, fmt.Errorf("error checking for duplicate issuance of %s: %s", serialHex, err)
|
|
}
|
|
var scts []ct.SignedCertificateTimestamp
|
|
for _, sctBytes := range req.SCTs {
|
|
var sct ct.SignedCertificateTimestamp
|
|
_, err = cttls.Unmarshal(sctBytes, &sct)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scts = append(scts, sct)
|
|
}
|
|
|
|
issuer, ok := ca.issuers.byNameID[issuance.GetIssuerNameID(precert)]
|
|
if !ok {
|
|
return nil, berrors.InternalServerError("no issuer found for Issuer Name %s", precert.Issuer)
|
|
}
|
|
|
|
issuanceReq, err := issuance.RequestFromPrecert(precert, scts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
names := strings.Join(issuanceReq.DNSNames, ", ")
|
|
|
|
ca.log.AuditInfof("Signing cert: serial=[%s] regID=[%d] names=[%s] precert=[%s]",
|
|
serialHex, req.RegistrationID, names, hex.EncodeToString(precert.Raw))
|
|
|
|
_, 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 certificate: %s", err)
|
|
}
|
|
|
|
ca.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc()
|
|
ca.log.AuditInfof("Signing cert success: serial=[%s] regID=[%d] names=[%s] certificate=[%s]",
|
|
serialHex, req.RegistrationID, names, hex.EncodeToString(certDER))
|
|
|
|
_, err = ca.sa.AddCertificate(ctx, &sapb.AddCertificateRequest{
|
|
Der: certDER,
|
|
RegID: req.RegistrationID,
|
|
Issued: timestamppb.New(ca.clk.Now()),
|
|
})
|
|
if err != nil {
|
|
ca.log.AuditErrf("Failed RPC to store at SA: serial=[%s], cert=[%s], issuerID=[%d], regID=[%d], orderID=[%d], err=[%v]",
|
|
serialHex, hex.EncodeToString(certDER), int64(issuer.Cert.NameID()), req.RegistrationID, req.OrderID, err)
|
|
return nil, err
|
|
}
|
|
|
|
return &corepb.Certificate{
|
|
RegistrationID: req.RegistrationID,
|
|
Serial: core.SerialToString(precert.SerialNumber),
|
|
Der: certDER,
|
|
Digest: core.Fingerprint256(certDER),
|
|
Issued: timestamppb.New(precert.NotBefore),
|
|
Expires: timestamppb.New(precert.NotAfter),
|
|
}, nil
|
|
}
|
|
|
|
type validity struct {
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
}
|
|
|
|
func (ca *certificateAuthorityImpl) generateSerialNumberAndValidity() (*big.Int, validity, error) {
|
|
// We want 136 bits of random number, plus an 8-bit instance id prefix.
|
|
const randBits = 136
|
|
serialBytes := make([]byte, randBits/8+1)
|
|
serialBytes[0] = byte(ca.prefix)
|
|
_, err := rand.Read(serialBytes[1:])
|
|
if err != nil {
|
|
err = berrors.InternalServerError("failed to generate serial: %s", err)
|
|
ca.log.AuditErrf("Serial randomness failed, err=[%v]", err)
|
|
return nil, validity{}, err
|
|
}
|
|
serialBigInt := big.NewInt(0)
|
|
serialBigInt = serialBigInt.SetBytes(serialBytes)
|
|
|
|
notBefore := ca.clk.Now().Add(-ca.backdate)
|
|
validity := validity{
|
|
NotBefore: notBefore,
|
|
NotAfter: notBefore.Add(ca.validityPeriod - time.Second),
|
|
}
|
|
|
|
return serialBigInt, validity, nil
|
|
}
|
|
|
|
func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context, issueReq *capb.IssueCertificateRequest, serialBigInt *big.Int, validity validity) ([]byte, *issuance.Issuer, error) {
|
|
csr, err := x509.ParseCertificateRequest(issueReq.Csr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = csrlib.VerifyCSR(ctx, csr, ca.maxNames, &ca.keyPolicy, ca.pa)
|
|
if err != nil {
|
|
ca.log.AuditErr(err.Error())
|
|
// VerifyCSR returns berror instances that can be passed through as-is
|
|
// without wrapping.
|
|
return nil, nil, err
|
|
}
|
|
|
|
var issuer *issuance.Issuer
|
|
var ok bool
|
|
if issueReq.IssuerNameID == 0 {
|
|
// Use the issuer which corresponds to the algorithm of the public key
|
|
// contained in the CSR, unless we have an allowlist of registration IDs
|
|
// for ECDSA, in which case switch all not-allowed accounts to RSA issuance.
|
|
alg := csr.PublicKeyAlgorithm
|
|
if alg == x509.ECDSA && !features.Enabled(features.ECDSAForAll) && ca.ecdsaAllowList != nil && !ca.ecdsaAllowList.permitted(issueReq.RegistrationID) {
|
|
alg = x509.RSA
|
|
}
|
|
issuer, ok = ca.issuers.byAlg[alg]
|
|
if !ok {
|
|
return nil, nil, berrors.InternalServerError("no issuer found for public key algorithm %s", csr.PublicKeyAlgorithm)
|
|
}
|
|
} else {
|
|
issuer, ok = ca.issuers.byNameID[issuance.IssuerNameID(issueReq.IssuerNameID)]
|
|
if !ok {
|
|
return nil, nil, berrors.InternalServerError("no issuer found for IssuerNameID %d", issueReq.IssuerNameID)
|
|
}
|
|
}
|
|
|
|
if issuer.Cert.NotAfter.Before(validity.NotAfter) {
|
|
err = berrors.InternalServerError("cannot issue a certificate that expires after the issuer certificate")
|
|
ca.log.AuditErr(err.Error())
|
|
return nil, nil, err
|
|
}
|
|
|
|
serialHex := core.SerialToString(serialBigInt)
|
|
|
|
ca.log.AuditInfof("Signing precert: serial=[%s] regID=[%d] names=[%s] csr=[%s]",
|
|
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
|
|
|
names := csrlib.NamesFromCSR(csr)
|
|
req := &issuance.IssuanceRequest{
|
|
PublicKey: csr.PublicKey,
|
|
Serial: serialBigInt.Bytes(),
|
|
DNSNames: names.SANs,
|
|
CommonName: names.CN,
|
|
IncludeCTPoison: true,
|
|
IncludeMustStaple: issuance.ContainsMustStaple(csr.Extensions),
|
|
NotBefore: validity.NotBefore,
|
|
NotAfter: validity.NotAfter,
|
|
}
|
|
|
|
lintCertBytes, 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)
|
|
if errors.Is(err, linter.ErrLinting) {
|
|
ca.lintErrorCount.Inc()
|
|
}
|
|
return nil, nil, berrors.InternalServerError("failed to prepare precertificate signing: %s", err)
|
|
}
|
|
|
|
_, err = ca.sa.AddPrecertificate(context.Background(), &sapb.AddCertificateRequest{
|
|
Der: lintCertBytes,
|
|
RegID: issueReq.RegistrationID,
|
|
Issued: timestamppb.New(ca.clk.Now()),
|
|
IssuerNameID: int64(issuer.Cert.NameID()),
|
|
OcspNotReady: true,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, 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]",
|
|
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), err)
|
|
return nil, nil, berrors.InternalServerError("failed to sign precertificate: %s", err)
|
|
}
|
|
|
|
ca.signatureCount.With(prometheus.Labels{"purpose": string(precertType), "issuer": issuer.Name()}).Inc()
|
|
ca.log.AuditInfof("Signing precert success: serial=[%s] regID=[%d] names=[%s] precertificate=[%s]",
|
|
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER))
|
|
|
|
return certDER, issuer, nil
|
|
}
|