acme-not-a-joke-wai
Lets a wai/warp application get its own TLS certificates with
acme-not-a-joke, using HTTP-01 challenges (RFC-8555 section
8.3) or TLS-ALPN-01 challenges (RFC-8737), and serve them without restarting.
Three modules, which can be used separately:
Acme.NotAJoke.Wai.Http01: a wai Middleware answering the HTTP-01
challenge, and a dance that tells the middleware what to answer.
Acme.NotAJoke.Wai.TlsAlpn01: TLS settings making a warp-tls server answer
the TLS-ALPN-01 challenge on port 443 (no port 80 needed), and a dance that
tells the server what to answer.
Acme.NotAJoke.Wai.WarpTLS: a certificate store that a warp-tls server
reads at each TLS handshake, so that a new certificate is served live.
who initiates what
Nothing is automatic: your program is the ACME client. It decides when to
get a certificate by calling runAcmeDance_http01_wai, which blocks until the
dance is over. The library has no background thread and no renewal timer.
your program ACME server (e.g. Let's Encrypt)
| |
| 1. account, new order, authorization |
|--------------- HTTPS, outgoing ------------->|
| |
| 2. key authorization put in ChallengeStore |
| "please validate the challenge" |
|--------------- HTTPS, outgoing ------------->|
| |
| 3. GET /.well-known/acme-challenge/{token} |
|<---------- HTTP on port 80, incoming --------|
| answered by http01Middleware |
| |
| 4. poll the order, send the CSR, |
| download the certificate chain |
|--------------- HTTPS, outgoing ------------->|
| |
| 5. handleStep (Done _ certificate) |
Consequences:
- the HTTP server wrapped with
http01Middleware must already be running on
port 80, and reachable from the Internet under the domain of the order,
before the dance starts;
- step 3 is the only incoming request, and it is plain HTTP: if your HTTP
server redirects everything to HTTPS, the middleware must wrap the
redirection;
- the dance is over in a few seconds when all goes well. Your
handleStep
decides how long to wait between two polls (the WaitingForValidation n
step, n being the number of polls so far);
- renewing is running the dance again, e.g. from a thread that sleeps until
some weeks before the certificate expires.
where things are stored
The library writes no file on its own. Both stores (ChallengeStore,
CertificateStore) are in memory. What goes to disk, and where, is decided by
the functions your program calls:
| what |
role |
who creates it |
typical helper |
| account key (JWK) |
signs the requests to the ACME server, identifies your ACME account |
you, once |
loadOrCreateJWKFile "state/account.jwk" |
| certificate key (RSA, PEM) |
the private key of the TLS server; signs the CSR; never leaves the machine |
you, once (or at each renewal) |
loadOrCreateRSAKeyPEM "state/key.pem" |
| CSR |
tells the ACME server which names and which public key to certify |
you, before each dance |
createCSR key names, in memory; writeCSRPEM only if you want a copy |
| key authorization |
the answer to the challenge |
the dance |
in memory, in the ChallengeStore, removed when the dance ends |
| certificate chain (PEM) |
what the TLS server presents |
the ACME server; given to your handleStep in the Done step |
storeCert "state/certificate.pem" cert and installCertificate store key cert |
The helpers are in Acme.NotAJoke.KeyManagement,
Acme.NotAJoke.CertManagement and Acme.NotAJoke.Api.Certificate of the
acme-not-a-joke package. The paths are yours: nothing has a default
location. Keep the account key and the certificate key private (the helpers
create files with the default permissions of the process).
certificate.pem (leaf certificate first, then the intermediates) and
key.pem are in the formats that warp-tls, nginx, haproxy etc. read.
serving the certificate live with warp-tls
Network.Wai.Handler.WarpTLS.tlsSettings reads the certificate files once,
when the server starts. Instead, start the HTTPS server on a
CertificateStore:
certificates <- newCertificateStore
forkIO $ runTLS (liveTlsSettings certificates) (setPort 443 defaultSettings) application
and put a credential in the store whenever you have one:
installCertificate certificates key cert in the Done step of a dance;
setCredential certificates =<< loadCredential "state/certificate.pem" "state/key.pem"
(modulo the Either) at startup, to reuse the certificate of a previous run.
The store is read at each TLS handshake: the next connection gets the new
certificate, established connections are left alone, and the server is not
restarted. While the store is empty, TLS handshakes fail (there is nothing to
present); the HTTP server on port 80 is not affected, so the first dance can
run.
A server with several certificates uses setCredentialFor store "name" cred:
the store then picks the credential from the server name (SNI) asked by the
client, and falls back to the one set with setCredential.
liveTlsSettings is tlsSettingsSni from warp-tls (3.4.13 or later) applied to
sniCredentials, which you can call from your own TLS hooks.
tls-alpn-01 challenges, without port 80
With a TLS-ALPN-01 challenge, the ACME server opens a TLS connection to port
443 of the domain asking for the acme-tls/1 protocol (ALPN), and expects a
self-signed certificate carrying the digest of the key authorization. The
HTTPS server answers it itself: no HTTP server is needed.
challenges <- newTlsAlpn01Store
certificates <- newCertificateStore
forkIO $ runTLS (withTlsAlpn01 challenges (liveTlsSettings certificates)) (setPort 443 defaultSettings) application
runAcmeDance_tlsalpn01_wai challenges dancer
withTlsAlpn01 wraps the server hooks: handshakes asking for acme-tls/1
get the challenge certificate of the store for their server name, the other
ones are untouched (they get the certificates of the live store, and h2 or
http/1.1). runAcmeDance_tlsalpn01_wai creates the challenge certificate
(with a throw-away RSA-2048 key) when the ACME server gives the challenge, and
removes it when the dance is over. challengeCredential, insertTlsAlpn01
and deleteTlsAlpn01 do the same by hand.
The server must keep its TLS 1.3 support enabled (the default of warp-tls):
the tls library chooses the certificate before the protocol with TLS 1.2,
so the challenge is only answered over TLS 1.3, which ACME servers use.
complete example
example/Main.hs is a warp server doing all of the above:
keys created on the first run, HTTP on port 80 with the middleware, HTTPS on
port 443 with the live store, certificate requested when there is none on disk,
then written to disk and served.
cabal build -fexample acme-not-a-joke-wai-example
sudo $(cabal list-bin -fexample acme-not-a-joke-wai-example) example.dicioccio.fr certmaster@dicioccio.fr ./state
It uses the staging environment of Let's Encrypt. The essential part:
accountKey <- orDie =<< loadOrCreateJWKFile "state/account.jwk"
key <- orDie =<< loadOrCreateRSAKeyPEM "state/key.pem"
csr <- orDie =<< createCSR key ("example.dicioccio.fr" :| [])
challenges <- newChallengeStore
certificates <- newCertificateStore
forkIO $ run 80 (http01Middleware challenges application)
forkIO $ runTLS (liveTlsSettings certificates) (setPort 443 defaultSettings) application
let handle step = case step of
WaitingForValidation n -> threadDelay (n * 1000000)
Done _ cert -> do
storeCert "state/certificate.pem" cert
orDie =<< installCertificate certificates key cert
_ -> pure ()
runAcmeDance_http01_wai challenges $
AcmeDancer
staging_letsencryptv2
accountKey
(fetchAccount1 True False ["mailto:certmaster@dicioccio.fr"])
csr
(createOrder (Nothing, Nothing) [OrderIdentifier DNSOrder "example.dicioccio.fr"])
handle
fetchAccount1 True False creates the ACME account if the account key has
none yet (and agrees to the terms of service of the ACME server); fetchAccount
only accepts an existing account.
limits
- HTTP-01 and TLS-ALPN-01 cannot validate wildcard identifiers: these need
DNS-01.
- TLS-ALPN-01 challenges are answered over TLS 1.3 only.
- The dance validates the first authorization of an order only: one
certificate per name, rather than one certificate for several names.
- No renewal scheduling, no rate-limit handling: failures are reported to
handleStep (InvalidOrder, OtherError, AcmeFailure) and the dance
stops. The
acme-not-a-joke-warp package has a
certificate manager doing the scheduling (startup, renewals, retries) on
top of this package.
- Certificate keys are RSA.