packages feed

acme-not-a-joke-wai-0.1.0.0: README.md

# acme-not-a-joke-wai

Lets a wai/warp application get its own TLS certificates with
[acme-not-a-joke](../README.md), 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`:

```hs
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.

```hs
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](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.

```console
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:

```hs
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](../acme-not-a-joke-warp/README.md) package has a
  certificate manager doing the scheduling (startup, renewals, retries) on
  top of this package.
- Certificate keys are RSA.