Authentication¶
The router decides who may run an operation with two optional hooks and checks renewals itself.
/cacerts and /csrattrs are always open. The hooks run for
/simpleenroll, /simplereenroll, /serverkeygen, /tpmenroll and /healthcheck.
The hooks¶
CheckBearerToken runs for every request that carries Authorization: Bearer <token>. The
prefix Bearer is case-sensitive, so a request with bearer <token> counts as one without a token.
CheckBasicAuth runs for every other request, with empty credentials when the request has no
Basic credentials. That lets it accept a client certificate instead of a password, as the router
example in Embedding the server does with r.TLS.VerifiedChains.
| Hook result | Response |
|---|---|
nil |
The request continues |
An est.Error with a status other than 401, such as 403 or 429 |
That status, its message and its Retry-After value |
| Any other error | 401 with a WWW-Authenticate challenge and no body |
Without hooks, the enrollment operations are open to every client that completes the TLS handshake. The table shows what each combination requires:
| Hooks set | Request without credentials | Request with a bearer token | Request with Basic credentials |
|---|---|---|---|
| None | Accepted | Accepted, token ignored | Accepted, credentials ignored |
CheckBasicAuth |
CheckBasicAuth with empty credentials |
CheckBasicAuth with empty credentials |
CheckBasicAuth |
CheckBearerToken |
Accepted | CheckBearerToken |
Accepted, credentials ignored |
| Both | CheckBasicAuth with empty credentials |
CheckBearerToken |
CheckBasicAuth |
CheckBearerToken alone does not require a token. Set CheckBasicAuth too and return an
error from it to refuse requests that carry no token.
TLS-SRP clients reach the same hooks. SRPUsernameFromContext(ctx) returns their username, see
TLS-SRP.
The 401 challenge¶
A refused request gets 401 with no body, because some clients, such as Bouncy Castle, fail when
they do not read the content of a challenge. The WWW-Authenticate header names the realm
estserver@<host> with the host of the request:
Basic realm="estserver@est.example.com"whenCheckBasicAuthis set orCheckBearerTokenis notBearer realm="estserver@est.example.com"whenCheckBearerTokenis set, followed by, error="invalid_token"when the request carried a token
Each challenge is a separate header line. The header name is written as WWW-Authenticate, the
RFC 9110 spelling, because libest compares header names case-sensitively.
The health check¶
GET /healthcheck answers 200 with an empty body when a hook accepts the request. Unlike the EST
operations it is never open: without hooks it always answers 401. estserver accepts it with the
Basic user healthcheck and the configured password.
Renewal checks¶
For /simplereenroll the router checks the client certificate before it calls CA.Reenroll:
- A certificate must be present, unless the client signed in with TLS-SRP or
password renewal applies. Otherwise: 403
client certificate must be provided for /simplereenroll. - The Subject and the SubjectAltName extension of the request must equal those of the
certificate, byte for byte (RFC 7030 section 4.2.2). Otherwise: 403
Subject and SubjectAltName fields in CSR must be identical to certificate being renewed. - The certificate must verify against the certificates
CA.CACertsreturns for the same path segment, at the current time and for client authentication. Otherwise: 403invalid client certificate. IfCACertsitself fails, the request fails with that error.
The router uses the first certificate the client presented, so it works with listeners that request a certificate without verifying it.
Custom certificate verification¶
VerifyClientCertificate replaces step 3. Use it for certificates that crypto/x509 cannot
verify, such as certificates an SLH-DSA or composite ML-DSA CA signed. Steps 1 and 2 still run
first. A nil error accepts the certificate, an est.Error is sent as is and any other error
answers 403 invalid client certificate. The listener must use tls.RequestClientCert, see
Post-quantum algorithms.
Password renewal¶
RFC 7030 section 2.3 allows a renewal that a password signs in instead of the certificate being
renewed, for certificates that cannot authenticate TLS. PasswordRenewal turns this on:
cfg := &est.ServerConfig{
CheckBasicAuth: func(ctx context.Context, r *http.Request, aps, username, password string) error {
want, ok := passwords[username]
if !ok || subtle.ConstantTimeCompare([]byte(password), []byte(want)) != 1 {
return errors.New("unknown user or wrong password")
}
return nil
},
PasswordRenewal: true,
}
- A renewal without a client certificate is accepted when
CheckBasicAuthaccepted Basic credentials with a non-empty username orCheckBearerTokenaccepted a token. A bearer token counts only whenCheckBearerTokenis set. NewRouterrefusesPasswordRenewalwithout either hook.- A client that does present a certificate still goes through all three renewal checks.
CA.Reenrollreceives a nil certificate and must choose the certificate to renew by the signed-in identity, never by the Subject of the request. The CA in The CA interface compares the requested common name with the username.
PasswordRenewal is off by default.
Proof of possession¶
The router does not check the tls-unique challenge password that a client adds with
SigningKey. A CA that requires the binding compares the challenge password in the request with
the tls-unique value of the connection, which r.TLS.TLSUnique holds for TLS 1.2. Over TLS-SRP
r.TLS is nil, so the host passes the value of the go-tls-srp connection to the CA itself, for
example in the request context.