1046 lines
36 KiB
Go
1046 lines
36 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/signer"
|
|
"github.com/cloudflare/cfssl/signer/local"
|
|
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"
|
|
|
|
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"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
)
|
|
|
|
// 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 (
|
|
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)
|
|
GetCertificate(context.Context, string) (core.Certificate, error)
|
|
AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*corepb.Empty, error)
|
|
AddSerial(ctx context.Context, req *sapb.AddSerialRequest) (*corepb.Empty, error)
|
|
}
|
|
|
|
type certificateType string
|
|
|
|
const (
|
|
precertType = certificateType("precertificate")
|
|
certType = certificateType("certificate")
|
|
)
|
|
|
|
// Four maps of keys to internalIssuers. Lookup by PublicKeyAlgorithm is
|
|
// useful for determining which issuer to use to sign a given (pre)cert, based
|
|
// on its PublicKeyAlgorithm. Lookup by CommonName is useful for determining
|
|
// which issuer to use to sign an OCSP response, based on the cert's
|
|
// Issuer CN. Lookup by ID is useful for the same functionality, in cases
|
|
// where features.StoreIssuerInfo is true and the OCSP request is identified
|
|
// by Serial and IssuerID rather than by the full cert. Lookup by NameID is
|
|
// useful as a easier-to-compute replacement for both byName and byID lookups.
|
|
type issuerMaps struct {
|
|
byAlg map[x509.PublicKeyAlgorithm]*internalIssuer
|
|
byName map[string]*internalIssuer
|
|
byID map[issuance.IssuerID]*internalIssuer
|
|
byNameID map[issuance.IssuerNameID]*internalIssuer
|
|
}
|
|
|
|
// CertificateAuthorityImpl represents a CA that signs certificates, CRLs, and
|
|
// OCSP responses.
|
|
type CertificateAuthorityImpl struct {
|
|
sa certificateStorage
|
|
pa core.PolicyAuthority
|
|
issuers issuerMaps
|
|
ecdsaAllowedRegIDs map[int64]bool
|
|
cfsslRSAProfile string
|
|
cfsslECDSAProfile string
|
|
prefix int // Prepended to the serial number
|
|
validityPeriod time.Duration
|
|
backdate time.Duration
|
|
maxNames int
|
|
ocspLifetime time.Duration
|
|
keyPolicy goodkey.KeyPolicy
|
|
orphanQueue *goque.Queue
|
|
ocspLogQueue *ocspLogQueue
|
|
clk clock.Clock
|
|
log blog.Logger
|
|
signatureCount *prometheus.CounterVec
|
|
csrExtensionCount *prometheus.CounterVec
|
|
orphanCount *prometheus.CounterVec
|
|
adoptedOrphanCount *prometheus.CounterVec
|
|
signErrorCounter *prometheus.CounterVec
|
|
}
|
|
|
|
// Issuer represents a single issuer certificate, along with its key.
|
|
type Issuer struct {
|
|
Signer crypto.Signer
|
|
Cert *issuance.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.
|
|
// TODO(#5086): Remove the ocsp-specific pieces of this as we factor OCSP out.
|
|
type internalIssuer struct {
|
|
cert *issuance.Certificate
|
|
ocspSigner crypto.Signer
|
|
|
|
// Only one of cfsslSigner and boulderIssuer will be non-nill
|
|
cfsslSigner localSigner
|
|
boulderIssuer *issuance.Issuer
|
|
}
|
|
|
|
func makeInternalIssuers(issuers []*issuance.Issuer, lifespanOCSP time.Duration) (issuerMaps, error) {
|
|
issuersByAlg := make(map[x509.PublicKeyAlgorithm]*internalIssuer, 2)
|
|
issuersByName := make(map[string]*internalIssuer, len(issuers))
|
|
issuersByID := make(map[issuance.IssuerID]*internalIssuer, len(issuers))
|
|
issuersByNameID := make(map[issuance.IssuerNameID]*internalIssuer, len(issuers))
|
|
for _, issuer := range issuers {
|
|
ii := &internalIssuer{
|
|
cert: issuer.Cert,
|
|
ocspSigner: issuer.Signer,
|
|
boulderIssuer: issuer,
|
|
}
|
|
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] = ii
|
|
}
|
|
}
|
|
if issuersByName[issuer.Name()] != nil {
|
|
return issuerMaps{}, errors.New("Multiple issuer certs with the same CommonName are not supported")
|
|
}
|
|
issuersByName[issuer.Name()] = ii
|
|
issuersByID[issuer.ID()] = ii
|
|
issuersByNameID[issuer.Cert.NameID()] = ii
|
|
}
|
|
return issuerMaps{issuersByAlg, issuersByName, issuersByID, issuersByNameID}, nil
|
|
}
|
|
|
|
func makeCFSSLInternalIssuers(issuers []Issuer, policy *cfsslConfig.Signing, lifespanOCSP time.Duration) (issuerMaps, error) {
|
|
if len(issuers) == 0 {
|
|
return issuerMaps{}, errors.New("No issuers specified.")
|
|
}
|
|
issuersByAlg := make(map[x509.PublicKeyAlgorithm]*internalIssuer, len(issuers))
|
|
issuersByName := make(map[string]*internalIssuer, len(issuers))
|
|
issuersByID := make(map[issuance.IssuerID]*internalIssuer, len(issuers))
|
|
issuersByNameID := make(map[issuance.IssuerNameID]*internalIssuer, len(issuers))
|
|
for idx, iss := range issuers {
|
|
if iss.Cert == nil || iss.Signer == nil {
|
|
return issuerMaps{}, errors.New("Issuer with nil cert or signer specified.")
|
|
}
|
|
cfsslSigner, err := local.NewSigner(iss.Signer, iss.Cert.Certificate, x509.SHA256WithRSA, policy)
|
|
if err != nil {
|
|
return issuerMaps{}, err
|
|
}
|
|
cn := iss.Cert.Subject.CommonName
|
|
if issuersByName[cn] != nil {
|
|
return issuerMaps{}, errors.New("Multiple issuer certs with the same CommonName are not supported")
|
|
}
|
|
|
|
ii := &internalIssuer{
|
|
cert: iss.Cert,
|
|
cfsslSigner: cfsslSigner,
|
|
ocspSigner: iss.Signer,
|
|
}
|
|
|
|
// Rather than reading a config to pick which issuer to use for each alg,
|
|
// just fall back to our old behavior of "the first issuer is used by default
|
|
// for everything". Ensure that the first issuer is an RSA key so that signing
|
|
// with x509.SHA256WithRSA doesn't break.
|
|
if idx == 0 {
|
|
if iss.Cert.PublicKeyAlgorithm != x509.RSA {
|
|
return issuerMaps{}, errors.New("Default (first) issuer must be RSA when using CFSSL")
|
|
}
|
|
issuersByAlg[x509.RSA] = ii
|
|
issuersByAlg[x509.ECDSA] = ii
|
|
}
|
|
issuersByName[cn] = ii
|
|
issuersByID[iss.Cert.ID()] = ii
|
|
issuersByNameID[iss.Cert.NameID()] = ii
|
|
}
|
|
return issuerMaps{issuersByAlg, issuersByName, issuersByID, issuersByNameID}, 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(
|
|
sa certificateStorage,
|
|
pa core.PolicyAuthority,
|
|
cfsslProfiles cfsslConfig.Config,
|
|
cfsslRSAProfile string,
|
|
cfsslECDSAProfile string,
|
|
cfsslIssuers []Issuer,
|
|
boulderIssuers []*issuance.Issuer,
|
|
ecdsaAllowedRegIDs []int64,
|
|
certExpiry time.Duration,
|
|
certBackdate time.Duration,
|
|
serialPrefix int,
|
|
maxNames int,
|
|
ocspLifetime time.Duration,
|
|
keyPolicy goodkey.KeyPolicy,
|
|
orphanQueue *goque.Queue,
|
|
ocspLogMaxLength int,
|
|
ocspLogPeriod time.Duration,
|
|
logger blog.Logger,
|
|
stats prometheus.Registerer,
|
|
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
|
|
}
|
|
var issuers issuerMaps
|
|
if features.Enabled(features.NonCFSSLSigner) {
|
|
issuers, err = makeInternalIssuers(boulderIssuers, ocspLifetime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// CFSSL requires processing JSON configs through its own LoadConfig, so we
|
|
// serialize and then deserialize.
|
|
cfsslJSON, err := json.Marshal(cfsslProfiles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfsslConfigObj, err := cfsslConfig.LoadConfig(cfsslJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if ocspLifetime == 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")
|
|
}
|
|
}
|
|
|
|
issuers, err = makeCFSSLInternalIssuers(cfsslIssuers, cfsslConfigObj.Signing, ocspLifetime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cfsslRSAProfile == "" || cfsslECDSAProfile == "" {
|
|
return nil, errors.New("must specify rsaProfile and ecdsaProfile")
|
|
}
|
|
}
|
|
|
|
ecdsaAllowedRegIDsMap := make(map[int64]bool, len(ecdsaAllowedRegIDs))
|
|
for _, regID := range ecdsaAllowedRegIDs {
|
|
ecdsaAllowedRegIDsMap[regID] = true
|
|
}
|
|
|
|
csrExtensionCount := prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "csr_extensions",
|
|
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)
|
|
|
|
orphanCount := prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "orphans",
|
|
Help: "Number of orphaned certificates labelled by type (precert, cert)",
|
|
},
|
|
[]string{"type"})
|
|
stats.MustRegister(orphanCount)
|
|
|
|
adoptedOrphanCount := prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adopted_orphans",
|
|
Help: "Number of orphaned certificates adopted from the orphan queue by type (precert, cert)",
|
|
},
|
|
[]string{"type"})
|
|
stats.MustRegister(adoptedOrphanCount)
|
|
|
|
signErrorCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "signature_errors",
|
|
Help: "A counter of signature errors labelled by error type",
|
|
}, []string{"type"})
|
|
stats.MustRegister(signErrorCounter)
|
|
|
|
var ocspLogQueue *ocspLogQueue
|
|
if ocspLogMaxLength > 0 {
|
|
ocspLogQueue = newOCSPLogQueue(ocspLogMaxLength, ocspLogPeriod, stats, logger)
|
|
}
|
|
|
|
ca = &CertificateAuthorityImpl{
|
|
sa: sa,
|
|
pa: pa,
|
|
issuers: issuers,
|
|
cfsslRSAProfile: cfsslRSAProfile,
|
|
cfsslECDSAProfile: cfsslECDSAProfile,
|
|
ecdsaAllowedRegIDs: ecdsaAllowedRegIDsMap,
|
|
validityPeriod: certExpiry,
|
|
backdate: certBackdate,
|
|
prefix: serialPrefix,
|
|
maxNames: maxNames,
|
|
ocspLifetime: ocspLifetime,
|
|
keyPolicy: keyPolicy,
|
|
orphanQueue: orphanQueue,
|
|
ocspLogQueue: ocspLogQueue,
|
|
log: logger,
|
|
signatureCount: signatureCount,
|
|
csrExtensionCount: csrExtensionCount,
|
|
orphanCount: orphanCount,
|
|
adoptedOrphanCount: adoptedOrphanCount,
|
|
signErrorCounter: signErrorCounter,
|
|
clk: clk,
|
|
}
|
|
|
|
return ca, nil
|
|
}
|
|
|
|
// noteSignError is called after operations that may cause a CFSSL
|
|
// or PKCS11 signing error.
|
|
func (ca *CertificateAuthorityImpl) noteSignError(err error) {
|
|
var pkcs11Error *pkcs11.Error
|
|
var cfErr *cferr.Error
|
|
if errors.As(err, &pkcs11Error) {
|
|
ca.signErrorCounter.WithLabelValues("HSM").Inc()
|
|
} else if errors.As(err, &cfErr) {
|
|
ca.signErrorCounter.WithLabelValues(fmt.Sprintf("CFSSL %d", cfErr.ErrorCode)).Inc()
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var ocspStatusToCode = map[string]int{
|
|
"good": ocsp.Good,
|
|
"revoked": ocsp.Revoked,
|
|
"unknown": ocsp.Unknown,
|
|
}
|
|
|
|
// GenerateOCSP produces a new OCSP response and returns it
|
|
func (ca *CertificateAuthorityImpl) GenerateOCSP(ctx context.Context, req *capb.GenerateOCSPRequest) (*capb.OCSPResponse, error) {
|
|
// req.Status, req.Reason, and req.RevokedAt are often 0, for non-revoked certs.
|
|
// Either CertDER or both (Serial and IssuerID) must be non-zero.
|
|
if core.IsAnyNilOrZero(req, req.CertDER) && core.IsAnyNilOrZero(req, req.Serial, req.IssuerID) {
|
|
return nil, berrors.InternalServerError("Incomplete generate OCSP request")
|
|
}
|
|
|
|
var issuer *internalIssuer
|
|
var serial *big.Int
|
|
// Once the feature is enabled we need to support both RPCs that include
|
|
// IssuerID and those that don't as we still need to be able to update rows
|
|
// that didn't have an IssuerID set when they were created. Once this feature
|
|
// has been enabled for a full OCSP lifetime cycle we can remove this
|
|
// functionality.
|
|
if features.Enabled(features.StoreIssuerInfo) && req.IssuerID != 0 {
|
|
serialInt, err := core.StringToSerial(req.Serial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
serial = serialInt
|
|
var ok bool
|
|
issuer, ok = ca.issuers.byID[issuance.IssuerID(req.IssuerID)]
|
|
if !ok {
|
|
return nil, fmt.Errorf("This CA doesn't have an issuer cert with ID %d", req.IssuerID)
|
|
}
|
|
} else {
|
|
cert, err := x509.ParseCertificate(req.CertDER)
|
|
if err != nil {
|
|
err := fmt.Errorf("parsing certificate for GenerateOCSP: %w", err)
|
|
ca.log.AuditErr(err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
serial = cert.SerialNumber
|
|
cn := cert.Issuer.CommonName
|
|
issuer = ca.issuers.byName[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.Certificate)
|
|
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)
|
|
}
|
|
}
|
|
|
|
now := ca.clk.Now().Truncate(time.Hour)
|
|
tbsResponse := ocsp.Response{
|
|
Status: ocspStatusToCode[req.Status],
|
|
SerialNumber: serial,
|
|
ThisUpdate: now,
|
|
NextUpdate: now.Add(ca.ocspLifetime),
|
|
}
|
|
if tbsResponse.Status == ocsp.Revoked {
|
|
tbsResponse.RevokedAt = time.Unix(0, req.RevokedAt)
|
|
tbsResponse.RevocationReason = int(req.Reason)
|
|
}
|
|
|
|
if ca.ocspLogQueue != nil {
|
|
ca.ocspLogQueue.enqueue(serial.Bytes(), now, ocsp.ResponseStatus(tbsResponse.Status))
|
|
}
|
|
|
|
ocspResponse, err := ocsp.CreateResponse(issuer.cert.Certificate, issuer.cert.Certificate, tbsResponse, issuer.ocspSigner)
|
|
ca.noteSignError(err)
|
|
if err == nil {
|
|
ca.signatureCount.With(prometheus.Labels{"purpose": "ocsp"}).Inc()
|
|
}
|
|
return &capb.OCSPResponse{Response: ocspResponse}, err
|
|
}
|
|
|
|
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
|
|
nowNanos := ca.clk.Now().UnixNano()
|
|
expiresNanos := validity.NotAfter.UnixNano()
|
|
_, err = ca.sa.AddSerial(ctx, &sapb.AddSerialRequest{
|
|
Serial: serialHex,
|
|
RegID: regID,
|
|
Created: nowNanos,
|
|
Expires: expiresNanos,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
precertDER, issuer, err := ca.issuePrecertificateInner(ctx, issueReq, serialBigInt, validity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ocspResp, err := ca.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
|
CertDER: precertDER,
|
|
Status: string(core.OCSPStatusGood),
|
|
})
|
|
if err != nil {
|
|
err = berrors.InternalServerError(err.Error())
|
|
ca.log.AuditInfof("OCSP Signing failure: serial=[%s] err=[%s]", serialHex, err)
|
|
return nil, err
|
|
}
|
|
|
|
issuerID := issuer.cert.ID()
|
|
|
|
req := &sapb.AddCertificateRequest{
|
|
Der: precertDER,
|
|
RegID: regID,
|
|
Ocsp: ocspResp.Response,
|
|
Issued: nowNanos,
|
|
IssuerID: int64(issuerID),
|
|
}
|
|
|
|
_, err = ca.sa.AddPrecertificate(ctx, req)
|
|
if err != nil {
|
|
ca.orphanCount.With(prometheus.Labels{"type": "precert"}).Inc()
|
|
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 precertificate: serial=[%s], cert=[%s], issuerID=[%d], regID=[%d], orderID=[%d], err=[%v]",
|
|
serialHex, hex.EncodeToString(precertDER), issuerID, issueReq.RegistrationID, issueReq.OrderID, err)
|
|
if ca.orphanQueue != nil {
|
|
ca.queueOrphan(&orphanedCert{
|
|
DER: precertDER,
|
|
RegID: regID,
|
|
OCSPResp: ocspResp.Response,
|
|
Precert: true,
|
|
IssuerID: int64(issuerID),
|
|
})
|
|
}
|
|
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, 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)
|
|
}
|
|
|
|
var certDER []byte
|
|
if features.Enabled(features.NonCFSSLSigner) {
|
|
issuanceReq, err := issuance.RequestFromPrecert(precert, scts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certDER, err = issuer.boulderIssuer.Issue(issuanceReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
certPEM, err := issuer.cfsslSigner.SignFromPrecert(precert, scts)
|
|
if err != nil {
|
|
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.signatureCount.WithLabelValues(string(certType)).Inc()
|
|
ca.log.AuditInfof("Signing success: serial=[%s] names=[%s] csr=[%s] certificate=[%s]",
|
|
serialHex, strings.Join(precert.DNSNames, ", "), hex.EncodeToString(req.DER),
|
|
hex.EncodeToString(certDER))
|
|
err = ca.storeCertificate(ctx, req.RegistrationID, req.OrderID, precert.SerialNumber, certDER, int64(issuer.cert.ID()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &corepb.Certificate{
|
|
RegistrationID: req.RegistrationID,
|
|
Serial: core.SerialToString(precert.SerialNumber),
|
|
Der: certDER,
|
|
Digest: core.Fingerprint256(certDER),
|
|
Issued: precert.NotBefore.UnixNano(),
|
|
Expires: precert.NotAfter.UnixNano(),
|
|
}, 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(-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) ([]byte, *internalIssuer, error) {
|
|
csr, err := x509.ParseCertificateRequest(issueReq.Csr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if err := csrlib.VerifyCSR(
|
|
ctx,
|
|
csr,
|
|
ca.maxNames,
|
|
&ca.keyPolicy,
|
|
ca.pa,
|
|
issueReq.RegistrationID,
|
|
); err != nil {
|
|
ca.log.AuditErr(err.Error())
|
|
// VerifyCSR returns berror instances that can be passed through as-is
|
|
// without wrapping.
|
|
return nil, nil, err
|
|
}
|
|
|
|
extensions, err := ca.extensionsFromCSR(csr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var issuer *internalIssuer
|
|
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.ecdsaAllowedRegIDs[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)
|
|
|
|
var certDER []byte
|
|
if features.Enabled(features.NonCFSSLSigner) {
|
|
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
|
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
|
certDER, err = issuer.boulderIssuer.Issue(&issuance.IssuanceRequest{
|
|
PublicKey: csr.PublicKey,
|
|
Serial: serialBigInt.Bytes(),
|
|
CommonName: csr.Subject.CommonName,
|
|
DNSNames: csr.DNSNames,
|
|
IncludeCTPoison: true,
|
|
IncludeMustStaple: issuance.ContainsMustStaple(csr.Extensions),
|
|
NotBefore: validity.NotBefore,
|
|
NotAfter: validity.NotAfter,
|
|
})
|
|
ca.noteSignError(err)
|
|
if err != nil {
|
|
err = berrors.InternalServerError("failed to sign certificate: %s", err)
|
|
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v]", serialHex, err)
|
|
return nil, nil, err
|
|
}
|
|
} else {
|
|
// 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.cfsslRSAProfile
|
|
case *ecdsa.PublicKey:
|
|
profile = ca.cfsslECDSAProfile
|
|
default:
|
|
err = berrors.InternalServerError("unsupported key type %T", csr.PublicKey)
|
|
ca.log.AuditErr(err.Error())
|
|
return nil, 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,
|
|
ReturnPrecert: true,
|
|
}
|
|
|
|
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
|
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
|
|
|
certPEM, err := issuer.cfsslSigner.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, 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, nil, err
|
|
}
|
|
|
|
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, 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, nil, err
|
|
}
|
|
certDER = block.Bytes
|
|
}
|
|
ca.signatureCount.WithLabelValues(string(precertType)).Inc()
|
|
|
|
ca.log.AuditInfof("Signing success: serial=[%s] names=[%s] csr=[%s] precertificate=[%s]",
|
|
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw),
|
|
hex.EncodeToString(certDER))
|
|
|
|
return certDER, issuer, nil
|
|
}
|
|
|
|
func (ca *CertificateAuthorityImpl) storeCertificate(
|
|
ctx context.Context,
|
|
regID int64,
|
|
orderID int64,
|
|
serialBigInt *big.Int,
|
|
certDER []byte,
|
|
issuerID int64) error {
|
|
var err error
|
|
now := ca.clk.Now()
|
|
_, err = ca.sa.AddCertificate(ctx, certDER, regID, nil, &now)
|
|
if err != nil {
|
|
ca.orphanCount.With(prometheus.Labels{"type": "cert"}).Inc()
|
|
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,
|
|
RegID: regID,
|
|
IssuerID: issuerID,
|
|
})
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type orphanedCert struct {
|
|
DER []byte
|
|
OCSPResp []byte
|
|
RegID int64
|
|
Precert bool
|
|
IssuerID 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)
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
// LogOCSPLoop collects OCSP generation log events into bundles, and logs
|
|
// them periodically.
|
|
func (ca *CertificateAuthorityImpl) LogOCSPLoop() {
|
|
if ca.ocspLogQueue != nil {
|
|
ca.ocspLogQueue.loop()
|
|
}
|
|
}
|
|
|
|
// Stop asks this CertificateAuthorityImpl to shut down. It must be called
|
|
// after the corresponding RPC service is shut down and there are no longer
|
|
// any inflight RPCs. It will attempt to drain any logging queues (which may
|
|
// block), and will return only when done.
|
|
func (ca *CertificateAuthorityImpl) Stop() {
|
|
if ca.ocspLogQueue != nil {
|
|
ca.ocspLogQueue.stop()
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
// When calculating the `NotBefore` at issuance time, we subtracted
|
|
// ca.backdate. Now, to calculate the actual issuance time from the NotBefore,
|
|
// we reverse the process and add ca.backdate.
|
|
issued := cert.NotBefore.Add(ca.backdate)
|
|
if orphan.Precert {
|
|
issuedNanos := issued.UnixNano()
|
|
_, err = ca.sa.AddPrecertificate(context.Background(), &sapb.AddCertificateRequest{
|
|
Der: orphan.DER,
|
|
RegID: orphan.RegID,
|
|
Ocsp: orphan.OCSPResp,
|
|
Issued: issuedNanos,
|
|
IssuerID: orphan.IssuerID,
|
|
})
|
|
if err != nil && !errors.Is(err, berrors.Duplicate) {
|
|
return fmt.Errorf("failed to store orphaned precertificate: %s", err)
|
|
}
|
|
} else {
|
|
_, err = ca.sa.AddCertificate(context.Background(), orphan.DER, orphan.RegID, nil, &issued)
|
|
if err != nil && !errors.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)
|
|
}
|
|
ca.log.AuditInfof("Incorporated orphaned certificate: serial=[%s] cert=[%s] regID=[%d]",
|
|
core.SerialToString(cert.SerialNumber), hex.EncodeToString(orphan.DER), orphan.RegID)
|
|
typ := "cert"
|
|
if orphan.Precert {
|
|
typ = "precert"
|
|
}
|
|
ca.adoptedOrphanCount.With(prometheus.Labels{"type": typ}).Inc()
|
|
return nil
|
|
}
|