- OCaml 99%
- Raku <1%
acme #
Warning. This is a work-in-progress experimental fork of ocaml-letsencrypt. Do not use it in production; use ocaml-letsencrypt instead.
A server that terminates its own TLS needs a certificate, and a certificate from Let's Encrypt lasts 90 days. ACME is the protocol that issues and renews it without a person in the loop: the server holds an account key, orders a certificate for its domain names, proves it controls each name by answering a challenge, sends a certificate signing request and downloads the chain.
acmeis the protocol without I/O (Acme): the server's objects and their JSON codecs, the JWS-signed requests, the key authorization a challenge answers with, the signing request, the chain check, the time a certificate is due for renewal, andAcme.Client, the state machine that finds an account and orders a certificate one action at a time (send this request, put this challenge answer in place, wait this long) without performing any of them.acme-eioperforms those actions over the requests HTTP client (Acme_eio). It answers HTTP-01 challenges by writing each key authorization into a directory a web server serves at/.well-known/acme-challenge/, and its renewal loop issues a new certificate two thirds of the way through the lifetime of the last, handing the chain and key to a callback. Theacmecommand issues and renews from the command line.
DNS-01 and TLS-ALPN-01 challenges, external account binding, key rollover and revocation are not implemented.
Installation #
$ opam install acme acme-eio
If opam cannot find the packages, they may not yet be released in the public
opam-repository. Add the overlay repository, then install them:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install acme acme-eio
Usage #
Issuing a certificate #
Acme_eio.v makes a client for the account of a key and sends nothing;
Acme_eio.issue orders a certificate, finding the account first and creating
it the first time the key is used. The web server must serve the files of
challenges at http://<name>/.well-known/acme-challenge/<file> for each
name while the order is processed.
let issue env ~challenges names =
Eio.Switch.run @@ fun sw ->
let clock = Eio.Stdenv.clock env in
let requests =
Requests_eio.v ~sw ~mono:(Eio.Stdenv.mono_clock env) ~clock
(Eio.Stdenv.net env)
in
let account_key = X509.Private_key.generate `P256 in
match
Acme_eio.v ~contact:[ "mailto:admin@example.com" ]
~terms_of_service_agreed:true ~requests ~clock ~key:account_key
Acme.letsencrypt_staging
with
| Error e -> Error e
| Ok account ->
let key = X509.Private_key.generate `P256 in
Result.map
(fun chain -> (chain, key))
(Acme_eio.issue account ~http_01:challenges ~key names)
The account key is the account: keep it, and pass the same one next time.
Keeping it current #
Acme_eio.renew never returns. It issues a certificate at once when given
none, then again two thirds of the way through the lifetime of each, with a
fresh key, and hands each chain and key to the callback, which installs them.
A failed issuance is tried again after a delay that doubles from one minute
to one hour, and so is the account while the server cannot be reached, so a
server can start its renewal loop before the ACME server answers.
let keep_current account ~challenges ~install names =
Acme_eio.renew account ~http_01:challenges names (fun chain key ->
install ~chain ~key)
From the command line #
$ acme renew --directory https://acme-v02.api.letsencrypt.org/directory \
--agree-terms --contact mailto:admin@example.com \
--account-key /srv/tls/account.pem --key /srv/tls/key.pem \
--chain /srv/tls/chain.pem --http-01 /srv/tls/acme-challenge \
example.com www.example.com
acme renew does nothing until the chain it wrote before is due, so it can
run daily; acme issue always orders. The account key is created on first
use, and the key and chain are each replaced in a single rename.
Credits #
acme is a fork of ocaml-letsencrypt
(opam package letsencrypt, formerly mmaker/ocaml-acme), by Michele Mu
maker@tumbolandia.net and Hannes Mehnert hannes@mehnert.org, with
Romain Calascibetta and Reynir Björnsson among its contributors, under the
BSD-2-Clause licence. It forks commit
af9bcb4366161badb160fbd761ddcab4c7ce7a5a (release 2.1.1, 13 September
2026). The protocol objects and their error table, the order and
authorization flow, the nonce handling with its badNonce retry and the key
authorization come from there.
What changed:
- the client runs in direct style on Eio, over the requests HTTP client, where upstream's was a functor over a monad and an HTTP client signature;
- the JSON codecs use nox-json instead of jsont and lun, the JWS and JWK code is nox-jwt's instead of the jws library's, and certificates, keys and signing requests are nox-x509's and nox-crypto's;
- the order and authorization flow is an I/O-free state machine,
Acme.Client, in its own package,acme, whichacme-eiodrives and which the tests drive with scripted answers; - HTTP-01 is answered by writing into a directory, and an authorization is polled until it settles after its challenge is answered;
- a badNonce answer is retried a bounded number of times, with the same signer (upstream retried without limit, and a retried newAccount was signed with an empty account URL instead of the key);
- the chain is checked to certify the key and the names ordered;
- a renewal loop and the
acmecommand are new; - letsencrypt-mirage, the DNS-01 solver (letsencrypt-dns), the TLS-ALPN-01 solver and the oacmel command are dropped.
References #
- RFC 8555 - Automatic Certificate Management Environment (ACME)
- RFC 7638 - JSON Web Key (JWK) Thumbprint
- Pebble - Let's Encrypt's ACME test
server, which the tests run against when
PEBBLEnames its directory URL
License #
BSD-2-Clause for the code derived from ocaml-letsencrypt, ISC for the rest. See LICENSE.md.