Pending requests and throttling¶
A CA does not have to decide at once. It can keep an enrollment pending until an operator or a backend approves it. It can also ask a client to come back later.
Keeping a request pending¶
When Enroll returns scepserver.ErrPending, the handler answers PENDING. The client then sends
CertPoll messages with the transaction ID of the enrollment. The handler passes each one to
Poll. Poll answers like Enroll:
Poll returns |
Answer |
|---|---|
| The issued certificate, optionally followed by its chain | SUCCESS |
scepserver.ErrPending |
PENDING. The client polls again |
*scepserver.Failure, such as scep.BadCertID for an unknown transaction |
FAILURE |
A CertPoll carries no CSR. It names the enrollment with these fields:
| Field | Content |
|---|---|
Message.TransactionID |
The transaction ID of the enrollment |
Message.PollRequest.Issuer |
The DER encoded name of the CA |
Message.PollRequest.Subject |
The DER encoded subject of the CSR |
Message.SignerCertificate |
The certificate that signed the CertPoll, which the answer is encrypted for |
Keep the CSR with the transaction ID and check the subject when the client polls. The answer is
encrypted for the certificate that signs the CertPoll, so also check that its key signed the
enrollment. This CA keeps every authorized enrollment until approve is called. ca.authorize
and ca.issue stand for the checks and the issuing of your CA:
// pendingRequest is an enrollment that waits for approval.
type pendingRequest struct {
csr *x509.CertificateRequest
signerKey *rsa.PublicKey
approved bool
}
// Enroll keeps every authorized enrollment pending. The handler has checked that the signer key
// is RSA.
func (ca *pendingCA) Enroll(_ context.Context, req *scepserver.Request) ([]*x509.Certificate, error) {
msg := req.Message
if err := ca.authorize(msg); err != nil {
return nil, err
}
ca.mu.Lock()
defer ca.mu.Unlock()
ca.pending[msg.TransactionID] = &pendingRequest{
csr: msg.CSR,
signerKey: msg.SignerCertificate.PublicKey.(*rsa.PublicKey),
}
return nil, scepserver.ErrPending
}
// Poll issues the certificate of an approved enrollment.
func (ca *pendingCA) Poll(_ context.Context, req *scepserver.Request) ([]*x509.Certificate, error) {
msg := req.Message
ca.mu.Lock()
p, ok := ca.pending[msg.TransactionID]
approved := ok && p.approved
ca.mu.Unlock()
switch {
case !ok || !bytes.Equal(msg.PollRequest.Subject, p.csr.RawSubject) || !p.signerKey.Equal(msg.SignerCertificate.PublicKey):
return nil, &scepserver.Failure{Info: scep.BadCertID, Err: errors.New("unknown transaction")}
case !approved:
return nil, scepserver.ErrPending
}
return ca.issue(p.csr)
}
// approve lets the next CertPoll of the enrollment collect its certificate.
func (ca *pendingCA) approve(id scep.TransactionID) {
ca.mu.Lock()
defer ca.mu.Unlock()
if p, ok := ca.pending[id]; ok {
p.approved = true
}
}
A real CA keeps pending requests in durable storage and removes them once they are issued, refused or expired.
Transaction IDs¶
The client chooses the transaction ID and repeats it in every CertPoll. Treat it as an opaque
string. The parser accepts 1 to 256 characters. scepclient and scep.NewCSRRequest derive it
from the CSR public key with scep.CSRTransactionID: the hex encoded leftmost 160 bits of the
SHA-256 hash of the key, method 1 of RFC 7093. Other clients use other derivations, so do not
recompute it on the server.
Throttling with Busy¶
Return *scepserver.Busy to ask the client to send the same request again later, for example
when it polls more often than the CA allows. Here p.lastPoll is a time.Time field that Poll
sets on the pending request:
if time.Since(p.lastPoll) < time.Minute {
return nil, &scepserver.Busy{RetryAfter: time.Minute - time.Since(p.lastPoll)}
}
The handler answers HTTP 503 with RetryAfter rounded up to whole seconds in a Retry-After
header. It leaves the header out when RetryAfter is zero. It logs the deferral at the Info level
as scep request deferred.
scepclient sends the same message again after the delay, as many times as
Options.BusyRetries allows. A CA that returns *Busy must therefore not have acted on the
request. To refuse it for good, return a *Failure instead.
*Busy throttles one transaction. Rate limits per client address or per device belong in HTTP
middleware in front of the handler, which sees every request before it is parsed.