720 lines
23 KiB
Go
720 lines
23 KiB
Go
package ca
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/beeker1121/goque"
|
|
cfsslConfig "github.com/cloudflare/cfssl/config"
|
|
cferr "github.com/cloudflare/cfssl/errors"
|
|
"github.com/cloudflare/cfssl/ocsp"
|
|
"github.com/cloudflare/cfssl/signer"
|
|
"github.com/cloudflare/cfssl/signer/local"
|
|
"github.com/google/certificate-transparency-go"
|
|
cttls "github.com/google/certificate-transparency-go/tls"
|
|
"github.com/jmhodges/clock"
|
|
"github.com/miekg/pkcs11"
|
|
|
|
"github.com/letsencrypt/boulder/ca/config"
|
|
caPB "github.com/letsencrypt/boulder/ca/proto"
|
|
"github.com/letsencrypt/boulder/core"
|
|
csrlib "github.com/letsencrypt/boulder/csr"
|
|
berrors "github.com/letsencrypt/boulder/errors"
|
|
"github.com/letsencrypt/boulder/goodkey"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
"github.com/letsencrypt/boulder/metrics"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
// Miscellaneous PKIX OIDs that we need to refer to
|
|
var (
|
|
// X.509 Extensions
|
|
oidAuthorityInfoAccess = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1}
|
|
oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
|
|
oidBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19}
|
|
oidCertificatePolicies = asn1.ObjectIdentifier{2, 5, 29, 32}
|
|
oidCrlDistributionPoints = asn1.ObjectIdentifier{2, 5, 29, 31}
|
|
oidExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
|
|
oidKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15}
|
|
oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
|
|
oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
|
|
oidTLSFeature = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
|
|
|
|
// CSR attribute requesting extensions
|
|
oidExtensionRequest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 14}
|
|
)
|
|
|
|
// OID and fixed value for the "must staple" variant of the TLS Feature
|
|
// extension:
|
|
//
|
|
// Features ::= SEQUENCE OF INTEGER [RFC7633]
|
|
// enum { ... status_request(5) ...} ExtensionType; [RFC6066]
|
|
//
|
|
// DER Encoding:
|
|
// 30 03 - SEQUENCE (3 octets)
|
|
// |-- 02 01 - INTEGER (1 octet)
|
|
// | |-- 05 - 5
|
|
var (
|
|
mustStapleFeatureValue = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
|
|
mustStapleExtension = signer.Extension{
|
|
ID: cfsslConfig.OID(oidTLSFeature),
|
|
Critical: false,
|
|
Value: hex.EncodeToString(mustStapleFeatureValue),
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc6962#section-3.1
|
|
ctPoisonExtension = signer.Extension{
|
|
ID: cfsslConfig.OID(signer.CTPoisonOID),
|
|
Critical: true,
|
|
Value: "0500", // ASN.1 DER NULL, Hex encoded.
|
|
}
|
|
)
|
|
|
|
// Metrics for CA statistics
|
|
const (
|
|
// Increments when CA observes an HSM or signing error
|
|
metricSigningError = "SigningError"
|
|
metricHSMError = metricSigningError + ".HSMError"
|
|
|
|
csrExtensionCategory = "category"
|
|
csrExtensionBasic = "basic"
|
|
csrExtensionTLSFeature = "tls-feature"
|
|
csrExtensionTLSFeatureInvalid = "tls-feature-invalid"
|
|
csrExtensionOther = "other"
|
|
)
|
|
|
|
type certificateStorage interface {
|
|
AddCertificate(context.Context, []byte, int64, []byte, *time.Time) (string, error)
|
|
}
|
|
|
|
type certificateType string
|
|
|
|
const (
|
|
precertType = certificateType("precertificate")
|
|
certType = certificateType("certificate")
|
|
)
|
|
|
|
// CertificateAuthorityImpl represents a CA that signs certificates, CRLs, and
|
|
// OCSP responses.
|
|
type CertificateAuthorityImpl struct {
|
|
rsaProfile string
|
|
ecdsaProfile string
|
|
// A map from issuer cert common name to an internalIssuer struct
|
|
issuers map[string]*internalIssuer
|
|
// The common name of the default issuer cert
|
|
defaultIssuer *internalIssuer
|
|
sa certificateStorage
|
|
pa core.PolicyAuthority
|
|
keyPolicy goodkey.KeyPolicy
|
|
clk clock.Clock
|
|
log blog.Logger
|
|
stats metrics.Scope
|
|
prefix int // Prepended to the serial number
|
|
validityPeriod time.Duration
|
|
backdate time.Duration
|
|
maxNames int
|
|
forceCNFromSAN bool
|
|
signatureCount *prometheus.CounterVec
|
|
csrExtensionCount *prometheus.CounterVec
|
|
orphanQueue *goque.Queue
|
|
}
|
|
|
|
// Issuer represents a single issuer certificate, along with its key.
|
|
type Issuer struct {
|
|
Signer crypto.Signer
|
|
Cert *x509.Certificate
|
|
}
|
|
|
|
// localSigner is an interface describing the functions of a cfssl.local.Signer
|
|
// that the Boulder CA uses. It allows mocking the local.Signer in unit tests.
|
|
type localSigner interface {
|
|
Sign(signer.SignRequest) ([]byte, error)
|
|
SignFromPrecert(*x509.Certificate, []ct.SignedCertificateTimestamp) ([]byte, error)
|
|
}
|
|
|
|
// internalIssuer represents the fully initialized internal state for a single
|
|
// issuer, including the cfssl signer and OCSP signer objects.
|
|
type internalIssuer struct {
|
|
cert *x509.Certificate
|
|
eeSigner localSigner
|
|
ocspSigner ocsp.Signer
|
|
}
|
|
|
|
func makeInternalIssuers(
|
|
issuers []Issuer,
|
|
policy *cfsslConfig.Signing,
|
|
lifespanOCSP time.Duration,
|
|
) (map[string]*internalIssuer, error) {
|
|
if len(issuers) == 0 {
|
|
return nil, errors.New("No issuers specified.")
|
|
}
|
|
internalIssuers := make(map[string]*internalIssuer)
|
|
for _, iss := range issuers {
|
|
if iss.Cert == nil || iss.Signer == nil {
|
|
return nil, errors.New("Issuer with nil cert or signer specified.")
|
|
}
|
|
eeSigner, err := local.NewSigner(iss.Signer, iss.Cert, x509.SHA256WithRSA, policy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set up our OCSP signer. Note this calls for both the issuer cert and the
|
|
// OCSP signing cert, which are the same in our case.
|
|
ocspSigner, err := ocsp.NewSigner(iss.Cert, iss.Cert, iss.Signer, lifespanOCSP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cn := iss.Cert.Subject.CommonName
|
|
if internalIssuers[cn] != nil {
|
|
return nil, errors.New("Multiple issuer certs with the same CommonName are not supported")
|
|
}
|
|
internalIssuers[cn] = &internalIssuer{
|
|
cert: iss.Cert,
|
|
eeSigner: eeSigner,
|
|
ocspSigner: ocspSigner,
|
|
}
|
|
}
|
|
return internalIssuers, nil
|
|
}
|
|
|
|
// NewCertificateAuthorityImpl creates a CA instance that can sign certificates
|
|
// from a single issuer (the first first in the issuers slice), and can sign OCSP
|
|
// for any of the issuer certificates provided.
|
|
func NewCertificateAuthorityImpl(
|
|
config ca_config.CAConfig,
|
|
sa certificateStorage,
|
|
pa core.PolicyAuthority,
|
|
clk clock.Clock,
|
|
stats metrics.Scope,
|
|
issuers []Issuer,
|
|
keyPolicy goodkey.KeyPolicy,
|
|
logger blog.Logger,
|
|
orphanQueue *goque.Queue,
|
|
) (*CertificateAuthorityImpl, error) {
|
|
var ca *CertificateAuthorityImpl
|
|
var err error
|
|
|
|
if config.SerialPrefix <= 0 || config.SerialPrefix >= 256 {
|
|
err = errors.New("Must have a positive non-zero serial prefix less than 256 for CA.")
|
|
return nil, err
|
|
}
|
|
|
|
// CFSSL requires processing JSON configs through its own LoadConfig, so we
|
|
// serialize and then deserialize.
|
|
cfsslJSON, err := json.Marshal(config.CFSSL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfsslConfigObj, err := cfsslConfig.LoadConfig(cfsslJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if config.LifespanOCSP.Duration == 0 {
|
|
return nil, errors.New("Config must specify an OCSP lifespan period.")
|
|
}
|
|
|
|
for _, profile := range cfsslConfigObj.Signing.Profiles {
|
|
if len(profile.IssuerURL) > 1 {
|
|
return nil, errors.New("only one issuer_url supported")
|
|
}
|
|
}
|
|
|
|
internalIssuers, err := makeInternalIssuers(
|
|
issuers,
|
|
cfsslConfigObj.Signing,
|
|
config.LifespanOCSP.Duration)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defaultIssuer := internalIssuers[issuers[0].Cert.Subject.CommonName]
|
|
|
|
rsaProfile := config.RSAProfile
|
|
ecdsaProfile := config.ECDSAProfile
|
|
|
|
if rsaProfile == "" || ecdsaProfile == "" {
|
|
return nil, errors.New("must specify rsaProfile and ecdsaProfile")
|
|
}
|
|
|
|
csrExtensionCount := prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "csrExtensions",
|
|
Help: "Number of CSRs with extensions of the given category",
|
|
},
|
|
[]string{csrExtensionCategory})
|
|
stats.MustRegister(csrExtensionCount)
|
|
|
|
signatureCount := prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "signatures",
|
|
Help: "Number of signatures",
|
|
},
|
|
[]string{"purpose"})
|
|
stats.MustRegister(signatureCount)
|
|
|
|
ca = &CertificateAuthorityImpl{
|
|
sa: sa,
|
|
pa: pa,
|
|
issuers: internalIssuers,
|
|
defaultIssuer: defaultIssuer,
|
|
rsaProfile: rsaProfile,
|
|
ecdsaProfile: ecdsaProfile,
|
|
prefix: config.SerialPrefix,
|
|
clk: clk,
|
|
log: logger,
|
|
stats: stats,
|
|
keyPolicy: keyPolicy,
|
|
forceCNFromSAN: !config.DoNotForceCN, // Note the inversion here
|
|
signatureCount: signatureCount,
|
|
csrExtensionCount: csrExtensionCount,
|
|
orphanQueue: orphanQueue,
|
|
}
|
|
|
|
if config.Expiry == "" {
|
|
return nil, errors.New("Config must specify an expiry period.")
|
|
}
|
|
ca.validityPeriod, err = time.ParseDuration(config.Expiry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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.
|
|
ca.backdate = config.Backdate.Duration
|
|
if ca.backdate == 0 {
|
|
ca.backdate = time.Hour
|
|
}
|
|
|
|
ca.maxNames = config.MaxNames
|
|
|
|
return ca, nil
|
|
}
|
|
|
|
// noteSignError is called after operations that may cause a CFSSL
|
|
// or PKCS11 signing error.
|
|
func (ca *CertificateAuthorityImpl) noteSignError(err error) {
|
|
if err != nil {
|
|
if _, ok := err.(*pkcs11.Error); ok {
|
|
ca.stats.Inc(metricHSMError, 1)
|
|
} else if cfErr, ok := err.(*cferr.Error); ok {
|
|
ca.stats.Inc(fmt.Sprintf("%s.%d", metricSigningError, cfErr.ErrorCode), 1)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Extract supported extensions from a CSR. The following extensions are
|
|
// currently supported:
|
|
//
|
|
// * 1.3.6.1.5.5.7.1.24 - TLS Feature [RFC7633], with the "must staple" value.
|
|
// Any other value will result in an error.
|
|
//
|
|
// Other requested extensions are silently ignored.
|
|
func (ca *CertificateAuthorityImpl) extensionsFromCSR(csr *x509.CertificateRequest) ([]signer.Extension, error) {
|
|
extensions := []signer.Extension{}
|
|
|
|
extensionSeen := map[string]bool{}
|
|
hasBasic := false
|
|
hasOther := false
|
|
|
|
for _, attr := range csr.Attributes {
|
|
if !attr.Type.Equal(oidExtensionRequest) {
|
|
continue
|
|
}
|
|
|
|
for _, extList := range attr.Value {
|
|
for _, ext := range extList {
|
|
if extensionSeen[ext.Type.String()] {
|
|
// Ignore duplicate certificate extensions
|
|
continue
|
|
}
|
|
extensionSeen[ext.Type.String()] = true
|
|
|
|
switch {
|
|
case ext.Type.Equal(oidTLSFeature):
|
|
ca.csrExtensionCount.With(prometheus.Labels{csrExtensionCategory: csrExtensionTLSFeature}).Inc()
|
|
value, ok := ext.Value.([]byte)
|
|
if !ok {
|
|
return nil, berrors.MalformedError("malformed extension with OID %v", ext.Type)
|
|
} else if !bytes.Equal(value, mustStapleFeatureValue) {
|
|
ca.csrExtensionCount.With(prometheus.Labels{csrExtensionCategory: csrExtensionTLSFeatureInvalid}).Inc()
|
|
return nil, berrors.MalformedError("unsupported value for extension with OID %v", ext.Type)
|
|
}
|
|
|
|
extensions = append(extensions, mustStapleExtension)
|
|
case ext.Type.Equal(oidAuthorityInfoAccess),
|
|
ext.Type.Equal(oidAuthorityKeyIdentifier),
|
|
ext.Type.Equal(oidBasicConstraints),
|
|
ext.Type.Equal(oidCertificatePolicies),
|
|
ext.Type.Equal(oidCrlDistributionPoints),
|
|
ext.Type.Equal(oidExtKeyUsage),
|
|
ext.Type.Equal(oidKeyUsage),
|
|
ext.Type.Equal(oidSubjectAltName),
|
|
ext.Type.Equal(oidSubjectKeyIdentifier):
|
|
hasBasic = true
|
|
default:
|
|
hasOther = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if hasBasic {
|
|
ca.csrExtensionCount.With(prometheus.Labels{csrExtensionCategory: csrExtensionBasic}).Inc()
|
|
}
|
|
|
|
if hasOther {
|
|
ca.csrExtensionCount.With(prometheus.Labels{csrExtensionCategory: csrExtensionOther}).Inc()
|
|
}
|
|
|
|
return extensions, nil
|
|
}
|
|
|
|
// GenerateOCSP produces a new OCSP response and returns it
|
|
func (ca *CertificateAuthorityImpl) GenerateOCSP(ctx context.Context, xferObj core.OCSPSigningRequest) ([]byte, error) {
|
|
cert, err := x509.ParseCertificate(xferObj.CertDER)
|
|
if err != nil {
|
|
ca.log.AuditErr(err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
signRequest := ocsp.SignRequest{
|
|
Certificate: cert,
|
|
Status: xferObj.Status,
|
|
Reason: int(xferObj.Reason),
|
|
RevokedAt: xferObj.RevokedAt,
|
|
}
|
|
|
|
cn := cert.Issuer.CommonName
|
|
issuer := ca.issuers[cn]
|
|
if issuer == nil {
|
|
return nil, fmt.Errorf("This CA doesn't have an issuer cert with CommonName %q", cn)
|
|
}
|
|
|
|
err = cert.CheckSignatureFrom(issuer.cert)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GenerateOCSP was asked to sign OCSP for cert "+
|
|
"%s from %q, but the cert's signature was not valid: %s.",
|
|
core.SerialToString(cert.SerialNumber), cn, err)
|
|
}
|
|
|
|
ocspResponse, err := issuer.ocspSigner.Sign(signRequest)
|
|
ca.noteSignError(err)
|
|
if err == nil {
|
|
ca.signatureCount.With(prometheus.Labels{"purpose": "ocsp"}).Inc()
|
|
}
|
|
return ocspResponse, err
|
|
}
|
|
|
|
func (ca *CertificateAuthorityImpl) IssuePrecertificate(ctx context.Context, issueReq *caPB.IssueCertificateRequest) (*caPB.IssuePrecertificateResponse, error) {
|
|
serialBigInt, validity, err := ca.generateSerialNumberAndValidity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
precertDER, err := ca.issuePrecertificateInner(ctx, issueReq, serialBigInt, validity, precertType)
|
|
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.
|
|
func (ca *CertificateAuthorityImpl) IssueCertificateForPrecertificate(ctx context.Context, req *caPB.IssueCertificateForPrecertificateRequest) (core.Certificate, error) {
|
|
emptyCert := core.Certificate{}
|
|
precert, err := x509.ParseCertificate(req.DER)
|
|
if err != nil {
|
|
return emptyCert, err
|
|
}
|
|
var scts []ct.SignedCertificateTimestamp
|
|
for _, sctBytes := range req.SCTs {
|
|
var sct ct.SignedCertificateTimestamp
|
|
_, err = cttls.Unmarshal(sctBytes, &sct)
|
|
if err != nil {
|
|
return emptyCert, err
|
|
}
|
|
scts = append(scts, sct)
|
|
}
|
|
certPEM, err := ca.defaultIssuer.eeSigner.SignFromPrecert(precert, scts)
|
|
if err != nil {
|
|
return emptyCert, err
|
|
}
|
|
serialHex := core.SerialToString(precert.SerialNumber)
|
|
block, _ := pem.Decode(certPEM)
|
|
if block == nil || block.Type != "CERTIFICATE" {
|
|
err = berrors.InternalServerError("invalid certificate value returned")
|
|
ca.log.AuditErrf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", serialHex, certPEM, err)
|
|
return emptyCert, err
|
|
}
|
|
certDER := block.Bytes
|
|
ca.log.AuditInfof("Signing success: serial=[%s] names=[%s] precertificate=[%s] certificate=[%s]",
|
|
serialHex, strings.Join(precert.DNSNames, ", "), hex.EncodeToString(req.DER),
|
|
hex.EncodeToString(certDER))
|
|
return ca.generateOCSPAndStoreCertificate(ctx, *req.RegistrationID, *req.OrderID, precert.SerialNumber, certDER)
|
|
}
|
|
|
|
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(-1 * ca.backdate)
|
|
validity := validity{
|
|
NotBefore: notBefore,
|
|
NotAfter: notBefore.Add(ca.validityPeriod),
|
|
}
|
|
|
|
return serialBigInt, validity, nil
|
|
}
|
|
|
|
func (ca *CertificateAuthorityImpl) issuePrecertificateInner(ctx context.Context, issueReq *caPB.IssueCertificateRequest, serialBigInt *big.Int, validity validity, certType certificateType) ([]byte, error) {
|
|
csr, err := x509.ParseCertificateRequest(issueReq.Csr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := csrlib.VerifyCSR(
|
|
csr,
|
|
ca.maxNames,
|
|
&ca.keyPolicy,
|
|
ca.pa,
|
|
ca.forceCNFromSAN,
|
|
*issueReq.RegistrationID,
|
|
); err != nil {
|
|
ca.log.AuditErr(err.Error())
|
|
return nil, berrors.MalformedError(err.Error())
|
|
}
|
|
|
|
extensions, err := ca.extensionsFromCSR(csr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
issuer := ca.defaultIssuer
|
|
|
|
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, err
|
|
}
|
|
|
|
// Convert the CSR to PEM
|
|
csrPEM := string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE REQUEST",
|
|
Bytes: csr.Raw,
|
|
}))
|
|
|
|
var profile string
|
|
switch csr.PublicKey.(type) {
|
|
case *rsa.PublicKey:
|
|
profile = ca.rsaProfile
|
|
case *ecdsa.PublicKey:
|
|
profile = ca.ecdsaProfile
|
|
default:
|
|
err = berrors.InternalServerError("unsupported key type %T", csr.PublicKey)
|
|
ca.log.AuditErr(err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
// Send the cert off for signing
|
|
req := signer.SignRequest{
|
|
Request: csrPEM,
|
|
Profile: profile,
|
|
Hosts: csr.DNSNames,
|
|
Subject: &signer.Subject{
|
|
CN: csr.Subject.CommonName,
|
|
},
|
|
Serial: serialBigInt,
|
|
Extensions: extensions,
|
|
NotBefore: validity.NotBefore,
|
|
NotAfter: validity.NotAfter,
|
|
}
|
|
|
|
if certType == precertType {
|
|
req.ReturnPrecert = true
|
|
}
|
|
|
|
serialHex := core.SerialToString(serialBigInt)
|
|
|
|
if !ca.forceCNFromSAN {
|
|
req.Subject.SerialNumber = serialHex
|
|
}
|
|
|
|
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
|
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
|
|
|
certPEM, err := issuer.eeSigner.Sign(req)
|
|
ca.noteSignError(err)
|
|
if err != nil {
|
|
// If the Signing error was a pre-issuance lint error then marshal the
|
|
// linting errors to include in the audit err msg.
|
|
if lErr, ok := err.(*local.LintError); ok {
|
|
// NOTE(@cpu): We throw away the JSON marshal error here. If marshaling
|
|
// fails for some reason it's acceptable to log an empty string for the
|
|
// JSON component.
|
|
lintErrsJSON, _ := json.Marshal(lErr.ErrorResults)
|
|
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v] lintErrors=%s",
|
|
serialHex, err, string(lintErrsJSON))
|
|
return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
|
|
}
|
|
|
|
err = berrors.InternalServerError("failed to sign certificate: %s", err)
|
|
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v]", serialHex, err)
|
|
return nil, err
|
|
}
|
|
ca.signatureCount.With(prometheus.Labels{"purpose": string(certType)}).Inc()
|
|
|
|
if len(certPEM) == 0 {
|
|
err = berrors.InternalServerError("no certificate returned by server")
|
|
ca.log.AuditErrf("PEM empty from Signer: serial=[%s] err=[%v]", serialHex, err)
|
|
return nil, err
|
|
}
|
|
|
|
block, _ := pem.Decode(certPEM)
|
|
if block == nil || block.Type != "CERTIFICATE" {
|
|
err = berrors.InternalServerError("invalid certificate value returned")
|
|
ca.log.AuditErrf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", serialHex, certPEM, err)
|
|
return nil, err
|
|
}
|
|
certDER := block.Bytes
|
|
|
|
ca.log.AuditInfof("Signing success: serial=[%s] names=[%s] csr=[%s] %s=[%s]",
|
|
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw), certType,
|
|
hex.EncodeToString(certDER))
|
|
|
|
return certDER, nil
|
|
}
|
|
|
|
func (ca *CertificateAuthorityImpl) generateOCSPAndStoreCertificate(
|
|
ctx context.Context,
|
|
regID int64,
|
|
orderID int64,
|
|
serialBigInt *big.Int,
|
|
certDER []byte) (core.Certificate, error) {
|
|
ocspResp, err := ca.GenerateOCSP(ctx, core.OCSPSigningRequest{
|
|
CertDER: certDER,
|
|
Status: "good",
|
|
})
|
|
if err != nil {
|
|
err = berrors.InternalServerError(err.Error())
|
|
ca.log.AuditInfof("OCSP Signing failure: serial=[%s] err=[%s]", core.SerialToString(serialBigInt), err)
|
|
// Ignore errors here to avoid orphaning the certificate. The
|
|
// ocsp-updater will look for certs with a zero ocspLastUpdated
|
|
// and generate the initial response in this case.
|
|
}
|
|
|
|
now := ca.clk.Now()
|
|
_, err = ca.sa.AddCertificate(ctx, certDER, regID, ocspResp, &now)
|
|
if err != nil {
|
|
err = berrors.InternalServerError(err.Error())
|
|
// Note: This log line is parsed by cmd/orphan-finder. If you make any
|
|
// changes here, you should make sure they are reflected in orphan-finder.
|
|
ca.log.AuditErrf("Failed RPC to store at SA, orphaning certificate: serial=[%s] cert=[%s] err=[%v], regID=[%d], orderID=[%d]",
|
|
core.SerialToString(serialBigInt), hex.EncodeToString(certDER), err, regID, orderID)
|
|
if ca.orphanQueue != nil {
|
|
ca.queueOrphan(&orphanedCert{
|
|
DER: certDER,
|
|
OCSPResp: ocspResp,
|
|
RegID: regID,
|
|
})
|
|
}
|
|
return core.Certificate{}, err
|
|
}
|
|
|
|
return core.Certificate{DER: certDER}, nil
|
|
}
|
|
|
|
type orphanedCert struct {
|
|
DER []byte
|
|
OCSPResp []byte
|
|
RegID int64
|
|
}
|
|
|
|
func (ca *CertificateAuthorityImpl) queueOrphan(o *orphanedCert) {
|
|
if _, err := ca.orphanQueue.EnqueueObject(o); err != nil {
|
|
ca.log.AuditErrf("failed to queue orphan for integration: %s", err)
|
|
}
|
|
}
|
|
|
|
// OrphanIntegrationLoop runs a loop executing integrateOrphans and then waiting a minute.
|
|
// It is split out into a separate function called directly by boulder-ca in order to make
|
|
// testing the orphan queue functionality somewhat more simple.
|
|
func (ca *CertificateAuthorityImpl) OrphanIntegrationLoop() {
|
|
for {
|
|
if err := ca.integrateOrphan(); err != nil {
|
|
if err == goque.ErrEmpty {
|
|
time.Sleep(time.Minute)
|
|
continue
|
|
}
|
|
ca.log.AuditErrf("failed to integrate orphaned certs: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// integrateOrpan removes an orphan from the queue and adds it to the database. The
|
|
// item isn't dequeued until it is actually added to the database to prevent items from
|
|
// being lost if the CA is restarted between the item being dequeued and being added to
|
|
// the database. It calculates the issuance time by subtracting the backdate period from
|
|
// the notBefore time.
|
|
func (ca *CertificateAuthorityImpl) integrateOrphan() error {
|
|
item, err := ca.orphanQueue.Peek()
|
|
if err != nil {
|
|
if err == goque.ErrEmpty {
|
|
return goque.ErrEmpty
|
|
}
|
|
return fmt.Errorf("failed to peek into orphan queue: %s", err)
|
|
}
|
|
var orphan orphanedCert
|
|
if err = item.ToObject(&orphan); err != nil {
|
|
return fmt.Errorf("failed to marshal orphan: %s", err)
|
|
}
|
|
cert, err := x509.ParseCertificate(orphan.DER)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse orphan: %s", err)
|
|
}
|
|
issued := cert.NotBefore.Add(-ca.backdate)
|
|
_, err = ca.sa.AddCertificate(context.Background(), orphan.DER, orphan.RegID, orphan.OCSPResp, &issued)
|
|
if err != nil && !berrors.Is(err, berrors.Duplicate) {
|
|
return fmt.Errorf("failed to store orphaned certificate: %s", err)
|
|
}
|
|
if _, err = ca.orphanQueue.Dequeue(); err != nil {
|
|
return fmt.Errorf("failed to dequeue integrated orphaned certificate: %s", err)
|
|
}
|
|
return nil
|
|
}
|