Skip to content

Commit

Permalink
Implement unpredictable issuance from similar intermediates (letsencr…
Browse files Browse the repository at this point in the history
…ypt#7418)

Replace the CA's "useForRSA" and "useForECDSA" config keys with a single
"active" boolean. When the CA starts up, all active RSA issuers will be
used to issue precerts with RSA pubkeys, and all ECDSA issuers will be
used to issue precerts with ECDSA pubkeys (if the ECDSAForAll flag is
true; otherwise just those that are on the allow-list). All "inactive"
issuers can still issue OCSP responses, CRLs, and (notably) final
certificates.

Instead of using the "useForRSA" and "useForECDSA" flags, plus implicit
config ordering, to determine which issuer to use to handle a given
issuance, simply use the issuer's public key algorithm to determine
which issuances it should be handling. All implicit ordering
considerations are removed, because the "active" certificates now just
form a pool that is sampled from randomly.

To facilitate this, update some unit and integration tests to be more
flexible and try multiple potential issuing intermediates, particularly
when constructing OCSP requests.

For this change to be safe to deploy with no user-visible behavior
changes, the CA configs must contain:
- Exactly one RSA-keyed intermediate with "useForRSALeaves" set to true;
and
- Exactly one ECDSA-keyed intermediate with "useForECDSALeaves" set to
true.

If the configs contain more than one intermediate meeting one of the
bullets above, then randomized issuance will begin immediately.

Fixes letsencrypt#7291
Fixes letsencrypt#7290
  • Loading branch information
aarongable authored and alina.dmitrieva committed Jul 29, 2024
1 parent 1c39101 commit 13d5573
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 366 deletions.
176 changes: 31 additions & 145 deletions ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,57 +107,6 @@ type caMetrics struct {
lintErrorCount prometheus.Counter
}

func NewCAMetrics(stats prometheus.Registerer) *caMetrics {
signatureCount := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "signatures",
Help: "Number of signatures",
},
[]string{"purpose", "issuer"})
stats.MustRegister(signatureCount)

signErrorCount := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "signature_errors",
Help: "A counter of signature errors labelled by error type",
}, []string{"type"})
stats.MustRegister(signErrorCount)

lintErrorCount := prometheus.NewCounter(
prometheus.CounterOpts{
Name: "lint_errors",
Help: "Number of issuances that were halted by linting errors",
})
stats.MustRegister(lintErrorCount)

return &caMetrics{signatureCount, signErrorCount, lintErrorCount}
}

func (m *caMetrics) noteSignError(err error) {
var pkcs11Error pkcs11.Error
if errors.As(err, &pkcs11Error) {
m.signErrorCount.WithLabelValues("HSM").Inc()
}
}

// 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.UnsafeCertificateAuthorityServer
sa sapb.StorageAuthorityCertificateClient
pa core.PolicyAuthority
issuers issuerMaps
certProfiles certProfilesMaps

prefix int // Prepended to the serial number
maxNames int
keyPolicy goodkey.KeyPolicy
clk clock.Clock
log blog.Logger
metrics *caMetrics
}

var _ capb.CertificateAuthorityServer = (*certificateAuthorityImpl)(nil)

// makeIssuerMaps processes a list of issuers into a set of maps for easy
// lookup either by key algorithm (useful for picking an issuer for a precert)
// or by unique ID (useful for final certs, OCSP, and CRLs). If two issuers with
Expand All @@ -183,78 +132,6 @@ func makeIssuerMaps(issuers []*issuance.Issuer) (issuerMaps, error) {
return issuerMaps{issuersByAlg, issuersByNameID}, nil
}

// makeCertificateProfilesMap processes a set of named certificate issuance
// profile configs into a two pre-computed maps: 1) a human-readable name to the
// profile and 2) a unique hash over contents of the profile to the profile
// itself. It returns the maps or an error if a duplicate name or hash is found.
// It also associates the given lint registry with each profile.
//
// The unique hash is used in the case of
// - RA instructs CA1 to issue a precertificate
// - CA1 returns the precertificate DER bytes and profile hash to the RA
// - RA instructs CA2 to issue a final certificate, but CA2 does not contain a
// profile corresponding to that hash and an issuance is prevented.
func makeCertificateProfilesMap(defaultName string, profiles map[string]issuance.ProfileConfig, lints lint.Registry) (certProfilesMaps, error) {
if len(profiles) <= 0 {
return certProfilesMaps{}, fmt.Errorf("must pass at least one certificate profile")
}

// Check that a profile exists with the configured default profile name.
_, ok := profiles[defaultName]
if !ok {
return certProfilesMaps{}, fmt.Errorf("defaultCertificateProfileName:\"%s\" was configured, but a profile object was not found for that name", defaultName)
}

profileByName := make(map[string]*certProfileWithID, len(profiles))
profileByHash := make(map[[32]byte]*certProfileWithID, len(profiles))

for name, profileConfig := range profiles {
profile, err := issuance.NewProfile(profileConfig, lints)
if err != nil {
return certProfilesMaps{}, err
}

// gob can only encode exported fields, of which an issuance.Profile has
// none. However, since we're already in a loop iteration having access
// to the issuance.ProfileConfig used to generate the issuance.Profile,
// we'll generate the hash from that.
var encodedProfile bytes.Buffer
enc := gob.NewEncoder(&encodedProfile)
err = enc.Encode(profileConfig)
if err != nil {
return certProfilesMaps{}, err
}
if len(encodedProfile.Bytes()) <= 0 {
return certProfilesMaps{}, fmt.Errorf("certificate profile encoding returned 0 bytes")
}
hash := sha256.Sum256(encodedProfile.Bytes())

_, ok := profileByName[name]
if !ok {
profileByName[name] = &certProfileWithID{
name: name,
hash: hash,
profile: profile,
}
} else {
return certProfilesMaps{}, fmt.Errorf("duplicate certificate profile name %s", name)
}

_, ok = profileByHash[hash]
if !ok {
profileByHash[hash] = &certProfileWithID{
name: name,
hash: hash,
profile: profile,
}
} else {
return certProfilesMaps{}, fmt.Errorf("duplicate certificate profile hash %d", hash)
}
}

return certProfilesMaps{defaultName, profileByHash, profileByName}, nil
}

// makeCertificateProfilesMap processes a list of certificate issuance profile
// configs and an option slice of zlint lint names to ignore into a set of
// pre-computed maps: 1) a human-readable name to the profile and 2) a unique
Expand Down Expand Up @@ -369,6 +246,13 @@ func NewCertificateAuthorityImpl(
return nil, err
}

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,
Expand Down Expand Up @@ -535,36 +419,36 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
}

names := strings.Join(issuanceReq.DNSNames, ", ")
ca.log.AuditInfof("Signing cert: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] precert=[%s]",
serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, hex.EncodeToString(precert.Raw))
ca.log.AuditInfof("Signing cert: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] precert=[%s]",
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, hex.EncodeToString(precert.Raw))

_, issuanceToken, err := issuer.Prepare(certProfile.profile, issuanceReq)
if err != nil {
ca.log.AuditErrf("Preparing cert failed: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Preparing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
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] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Signing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
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] certProfileName=[%s] certProfileHash=[%x]",
serialHex, req.RegistrationID, names, hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
ca.log.AuditInfof("Signing cert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
issuer.Name(), serialHex, req.RegistrationID, names, hex.EncodeToString(certDER), certProfile.name, certProfile.hash)

_, 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] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, hex.EncodeToString(certDER), issuer.NameID(), req.RegistrationID, req.OrderID, certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Failed RPC to store at SA: issuer=[%s] serial=[%s] cert=[%s] regID=[%d] orderID=[%d] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, hex.EncodeToString(certDER), req.RegistrationID, req.OrderID, certProfile.name, certProfile.hash, err)
return nil, err
}

Expand Down Expand Up @@ -647,17 +531,19 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
return nil, nil, err
}

// 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.
// Select which pool of issuers to use, based on the to-be-issued cert's key
// type and whether we're using the ECDSA Allow List.
alg := csr.PublicKeyAlgorithm
if alg == x509.ECDSA && !features.Get().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)

// Select a random issuer from among the active issuers of this key type.
issuerPool, ok := ca.issuers.byAlg[alg]
if !ok || len(issuerPool) == 0 {
return nil, nil, berrors.InternalServerError("no issuers found for public key algorithm %s", csr.PublicKeyAlgorithm)
}
issuer := issuerPool[mrand.Intn(len(issuerPool))]

// Select a random issuer from among the active issuers of this key type.
issuerPool, ok := ca.issuers.byAlg[alg]
Expand Down Expand Up @@ -697,8 +583,8 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context

lintCertBytes, issuanceToken, err := issuer.Prepare(certProfile.profile, req)
if err != nil {
ca.log.AuditErrf("Preparing precert failed: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Preparing precert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
if errors.Is(err, linter.ErrLinting) {
ca.metrics.lintErrorCount.Inc()
}
Expand All @@ -719,14 +605,14 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
certDER, err := issuer.Issue(issuanceToken)
if err != nil {
ca.noteSignError(err)
ca.log.AuditErrf("Signing precert failed: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Signing precert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, 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] certProfileName=[%s] certProfileHash=[%x]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
ca.log.AuditInfof("Signing precert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] precertificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash)

return certDER, certProfile.hash[:], nil
}
Loading

0 comments on commit 13d5573

Please sign in to comment.