Issuing certificates¶
The Issuer interface is where the host CA signs certificates. This page explains what the server
guarantees before it calls the issuer, what the issuer must guarantee in return and what happens to
the result.
type Issuer interface {
Issue(ctx context.Context, req IssueRequest) (IssueResult, error)
}
What the server does first¶
Finalization accepts a CSR only when it is self-signed with an accepted key that is not the
account key and requests exactly the identifiers of the order. The worker then records the dispatch
decision in the store before the first CA call: an OperationID, the validation evidence and the
earliest expiry among the order and its authorizations as the Deadline. An optional
IssuancePolicy runs before that record is written.
From this point every call for the order presents the same OperationID, whether it is the first
attempt, a retry after a timeout or a recovery from another worker after a crash.
The request¶
| Field | Meaning |
|---|---|
OperationID |
Stable for one order across every attempt. Deduplicate on it |
AccountID, AccountURL, OrderID |
For audit records |
CSR, CSRDER |
The accepted request, already checked against the order |
Identifiers |
The normalized identifiers of the order |
NotBefore, NotAfter |
The requested validity. Zero means the CA decides. An authority token expiry narrows NotAfter |
Validations |
How each identifier was validated: challenge type, time and any CA grant |
Deadline |
After this time no new signing may start |
RecoveryOnly |
Set once the deadline has passed. Only an existing result may be returned |
The result¶
Exactly one of these is set:
| Outcome | Fields | What the server does |
|---|---|---|
| Issued | Chain and optionally CAReference |
Checks the chain and publishes the certificate |
| Pending | Pending and RetryAfter |
Asks again after RetryAfter, at least WorkerConfig.PollInterval later |
| Rejected | Rejected |
Makes the order invalid with that problem |
An error return means the outcome is unknown. The worker reschedules the task and calls again
with the same OperationID. There is no attempt limit on this path, because the CA may already have
issued the certificate. Return an error only when you do not know what happened. Return the
recorded result or a Rejected problem when you do.
Use problem types clients understand. ErrorRejectedIdentifier for a name the CA will not issue
for, ErrorBadCSR for a request the CA cannot honor and ErrorUnauthorized for a deadline that
passed. NewProblem and Problemf build them.
What the issuer must do¶
Deduplicate by operation ID. Store the result under OperationID before returning it and
return that stored result on every later call with the same ID. A CA that signs twice for one
order produces two valid certificates for one authorization.
Enforce the deadline. Do not start a new signing operation once Deadline has passed and never
when RecoveryOnly is set. Returning an existing result is always allowed.
Stay inside the requested validity. The CA may shorten the validity to its own policy. The
leaf's NotBefore must not be earlier than the requested one and its NotAfter must not be later.
The leaf must already be valid when it is returned unless the order asked for a future start.
Match the request. The leaf must carry the CSR's public key, exactly the order's identifiers as SANs and no CA basic constraint unless an Authority Token granted one, see Authority Token challenges.
Return the full chain. Chain holds the DER leaf followed by the DER issuer chain in signing
order. Each element must be signed by the next one.
A minimal implementation over a database looks like this:
func (ca *CA) Issue(ctx context.Context, req acmeserver.IssueRequest) (acmeserver.IssueResult, error) {
if chain, ok, err := ca.lookup(ctx, req.OperationID); err != nil {
return acmeserver.IssueResult{}, err // unknown outcome, the worker retries
} else if ok {
return acmeserver.IssueResult{Chain: chain}, nil
}
if req.RecoveryOnly || !req.Deadline.After(time.Now()) {
return acmeserver.IssueResult{Rejected: acmeserver.NewProblem(acmeserver.ErrorUnauthorized,
"the signing deadline has passed")}, nil
}
chain, err := ca.sign(ctx, req)
if err != nil {
return acmeserver.IssueResult{}, err
}
// Record the result under the operation ID before returning it. If another worker recorded
// one first, return that one instead.
return acmeserver.IssueResult{Chain: ca.recordOrExisting(ctx, req.OperationID, chain)}, nil
}
Publication checks¶
Before a chain reaches a client the server verifies that:
- the leaf public key equals the CSR public key
- the leaf is valid now and its
NotAfteris after itsNotBefore - the leaf
NotBeforeis not earlier than the accepted order value and not in the future unless the order asked for it - the leaf
NotAfterdoes not exceed the requested value or the authority token expiry - the leaf's SANs and, unless an authority list is the only identity, its common name equal the order identifiers
- the leaf's CA basic constraint matches what the authorizations granted, which is never for DNS and IP orders
- every chain element is signed by the following one
A chain that passes becomes a Certificate whose ID is the base64url SHA-256 digest of the leaf.
CAReference and Validations are stored with it for audit needs.
Unpublished results¶
A chain that fails a check, arrives together with an error or with Pending or Rejected, or
whose leaf digest or renewal identifier collides with a stored certificate is never served. The
server stores the whole IssueResult in Order.UnpublishedResult, marks the order invalid with a
serverInternal problem and logs an error naming the order and operation.
The library does not revoke these certificates. The host must find orders with an unpublished result and revoke or reconcile them in its CA. Add that query to your store and run it from a scheduled job or an alert.
Issuance policy¶
type IssuancePolicy interface {
AuthorizeIssuance(ctx context.Context, req IssueRequest) error
}
Config.IssuancePolicy runs once per order before the dispatch decision is stored, with the same
request the issuer would receive. It is the place for checks that need the accepted CSR or the
validation evidence, such as CAA-style rules, key policy or a final identifier check against a
current allow list. A returned *Problem makes the order invalid with that problem. Any other error
is retried up to WorkerConfig.MaxAttempts and then fails the order with serverInternal.
Policy that only needs the identifiers belongs in Policy.NewOrder, which refuses the order before
any validation work. See Accounts and policy.
Recovery scenarios¶
| Situation | Behavior |
|---|---|
Worker dies after Issue returned and before the result is stored |
Another worker claims the task after the lease and calls Issue with the same OperationID. The issuer returns the recorded chain |
| CA times out | The worker retries with backoff and the same OperationID, without an attempt limit |
| Deadline passes while the outcome is unknown | The request carries RecoveryOnly. The issuer returns an existing result or Rejected |
| Account deactivated after finalization | Recovery of an existing CA operation continues, because the certificate may exist |
| Authorization invalidated between finalization and dispatch | BeginIssuance fails and the order becomes invalid without a CA call |
| Same CSR finalized twice | The second finalization is accepted without a second issuance |