docs: add ISSUANCE-CYCLE.md (#7444)

Also update the CA and RA doccomments to link to it and describe the
roles of key functions a little better.

Remove outdated reference to generating OCSP at issuance time.
This commit is contained in:
Jacob Hoffman-Andrews 2024-05-06 13:01:08 -07:00 committed by GitHub
parent a9c2fa3f69
commit b7553f1290
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 78 additions and 8 deletions

View File

@ -291,6 +291,16 @@ var ocspStatusToCode = map[string]int{
"unknown": ocsp.Unknown,
}
// IssuePrecertificate is the first step in the [issuance cycle]. It allocates and stores a serial number,
// selects a certificate profile, generates and stores a linting certificate, sets the serial's status to
// "wait", signs and stores a precertificate, updates the serial's status to "good", then returns the
// precertificate.
//
// Subsequent final issuance based on this precertificate must happen at most once, and must use the same
// certificate profile. The certificate profile is identified by a hash to ensure an exact match even if
// the configuration for a specific profile _name_ changes.
//
// [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md
func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, issueReq *capb.IssueCertificateRequest) (*capb.IssuePrecertificateResponse, error) {
// issueReq.orderID may be zero, for ACMEv1 requests.
// issueReq.CertProfileName may be empty and will be populated in
@ -333,13 +343,13 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
}, 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
// IssueCertificateForPrecertificate final step in the [issuance cycle].
//
// Given a precertificate and a set of SCTs for that precertificate, it generates
// a linting final certificate, then signs a final certificate using a real issuer.
// The poison extension is removed from the precertificate 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.
// signature the final certificate exactly matches the precertificate.
//
// It's critical not to sign two different final certificates for the same
// precertificate. This can happen, for instance, if the caller provides a
@ -355,6 +365,8 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
// 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.
//
// [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md
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, req.CertProfileHash) {

51
docs/ISSUANCE-CYCLE.md Normal file
View File

@ -0,0 +1,51 @@
# The Issuance Cycle
What happens during an ACME finalize request?
At a high level:
1. Check that all authorizations are good.
2. Recheck CAA for hostnames that need it.
3. Allocate and store a serial number.
4. Select a certificate profile.
5. Generate and store linting certificate, set status to "wait" (precommit).
6. Sign, log (and don't store) precertificate, set status to "good".
7. Submit precertificate to CT.
8. Generate linting final certificate. Not logged or stored.
9. Sign, log, and store final certificate.
Revocation can happen at any time after (5), whether or not step (6) was successful. We do things this way so that even in the event of a power failure or error storing data, we have a record of what we planned to sign (the tbsCertificate bytes of the linting certificate).
Note that to avoid needing a migration, we chose to store the linting certificate from (5)in the "precertificates" table, which is now a bit of a misnomer.
# OCSP Status state machine:
wait -> good -> revoked
\
-> revoked
Serial numbers with a "wait" status recorded have not been submitted to CT,
because issuing the precertificate is a prerequisite to setting the status to
"good". And because they haven't been submitted to CT, they also haven't been
turned into a final certificate, nor have they been returned to a user.
OCSP requests for serial numbers in "wait" status will return 500, but we expect
not to serve any 500s in practice because these serial numbers never wind up in
users' hands. Serial numbers in "wait" status are not added to CRLs.
Note that "serial numbers never wind up in users' hands" does not relieve us of
any compliance duties. Our duties start from the moment of signing a
precertificate with trusted key material.
Since serial numbers in "wait" status _may_ have had a precertificate signed,
We need the ability to set revocation status for them. For instance if the public key
we planned to sign for turns out to be weak or compromised, we would want to serve
a revoked status for that serial. However since they also _may not_ have had a
Precertificate signed, we also can't serve an OCSP "good" status. That's why we
serve 500. A 500 is appropriate because the only way a serial number can have "wait"
status for any significant amount of time is if there was an internal error of some
sort: an error during or before signing, or an error storing a record of the
signing success in the database.
For clarity, "wait" is not an RFC 6960 status, but is an internal placeholder
value specific to Boulder.

View File

@ -1286,8 +1286,10 @@ type certProfileID struct {
hash []byte
}
// issueCertificateInner handles the heavy lifting aspects of certificate
// issuance.
// issueCertificateInner is part of the [issuance cycle].
//
// It gets a precertificate from the CA, submits it to CT logs to get SCTs,
// then sends the precertificate and the SCTs to the CA to get a final certificate.
//
// This function is responsible for ensuring that we never try to issue a final
// certificate twice for the same precertificate, because that has the potential
@ -1299,6 +1301,8 @@ type certProfileID struct {
// never have final certificates issued for them (because there is a possibility
// that the certificate was actually issued but there was an error returning
// it).
//
// [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md
func (ra *RegistrationAuthorityImpl) issueCertificateInner(
ctx context.Context,
csr *x509.CertificateRequest,
@ -1329,6 +1333,9 @@ func (ra *RegistrationAuthorityImpl) issueCertificateInner(
OrderID: int64(oID),
CertProfileName: profileName,
}
// Once we get a precert from IssuePrecertificate, we must attempt issuing
// a final certificate at most once. We achieve that by bailing on any error
// between here and IssueCertificateForPrecertificate.
precert, err := ra.CA.IssuePrecertificate(ctx, issueReq)
if err != nil {
return nil, nil, wrapError(err, "issuing precertificate")