certmagic
Automatic TLS certificate acquisition, renewal, and maintenance for Rust servers.
Overview
CertMagic manages TLS certificates for you: it obtains certificates from ACME CAs (Let's Encrypt, ZeroSSL, …), renews them before expiry, staples OCSP responses, coordinates with other instances through shared storage, and can even obtain certificates on-demand during the TLS handshake.
The crate automates the full certificate lifecycle and ships an idiomatic
Rust API (async traits, typed events, thiserror, Send + Sync everywhere).
Feature highlights
- ACME v2 client, self-developed: directory discovery, replay-nonce pool, account management with External Account Binding, order state machine, revocation, ARI (renewal information) with suggested-window jitter.
- Four challenge solvers: HTTP-01 (reference-counted shared listener +
framework-agnostic handler), TLS-ALPN-01 (RFC 8737 challenge certificates),
DNS-01 (pluggable
DnsProvider+ propagation checks), and distributed solving through shared storage — cluster-wide issuance without sticky sessions. HTTP-01 helpers validate unpadded base64url tokens, tolerate query strings and one router-added trailing slash, and expose an opt-in blind-solving fallback for deployments that cannot retain challenge state. - Handshake-time issuance and on-demand TLS: serve a certificate the first
time a name is seen, gated by your
DecisionFuncor explicit host allowlist; if neither gate is configured, issuance is denied. - Certificate cache and maintenance loop: SAN-indexed in-memory cache, renewal windows (fixed ratio or ARI-driven with jitter), OCSP stapling with a hand-rolled RFC 6960 codec (request building, delegated-responder authorization, ECDSA/RSA/Ed25519 signature verification).
- Storage abstraction with distributed locking: default file storage with atomic writes and heartbeat lockfiles; bring-your-own backend (Redis, etcd, S3, …) for multi-instance clusters.
- Separate certificate store: certificates and private keys can use a
dedicated
CertStorebackend while ACME accounts, locks and OCSP data stay on the ground-truthStorage. - Production hygiene: retry with hand-tuned 30-day backoff budget, per-name job deduplication, sliding-window rate limiter, typed event hooks, panic-proof maintenance loop.
- Operational hooks: cache lifecycle callbacks, configurable HTTPS redirect host policy, custom DNS resolvers for propagation checks, and optional suppression of automatic replacement after an OCSP revocation report.
Installation
[]
= "0.1"
= { = "1", = ["full"] }
The default build uses the AWS-LC-RS crypto provider and also enables RSA key generation plus the ZeroSSL issuers. For a smaller, portable build using the Ring provider, disable default features and select the required capabilities explicitly:
[]
= { = "0.1", = false, = [
"ring", "file-storage", "http-01", "dns-01", "ocsp",
] }
The ring and aws-lc-rs provider features also select the matching
x509-parser signature-verification backend (verify and verify-aws,
respectively), so certificate verification uses the same backend as rustls,
rcgen, and reqwest. Select exactly one provider in a --no-default-features
build; optional runtime modules each have an independent feature flag.
The rsa feature is opt-in because it pulls in the RustCrypto rsa crate.
Deployments that only issue ECDSA or Ed25519 certificates should leave it off
to keep the dependency tree smaller and avoid RSA-specific timing-sidechannel
risk. RustSec RUSTSEC-2023-0071 remains explicitly tracked because the
upstream project has not published a fixed release yet.
Quick start
use Arc;
use CancellationToken;
async
Prefer a high-level manager? CertManager wraps the same machinery behind a
cancellation-free facade:
let manager = builder
.issuers
.build?; // try_build() is the explicit fallible alias
manager.manage.await?; // foreground obtain + renew
let acceptor = new;
Cache::new starts renewal/OCSP maintenance automatically. Applications that
need an explicit lifecycle can use Cache::new_without_maintenance, then call
start_maintenance; stop_and_wait cancels and joins the task without
consuming the cache. A stopped cache cannot be restarted.
For a one-shot setup, certmagic::manage(&domains).await returns a ready
rustls::ServerConfig. For reusable policy and backend wiring, use
ConfigBuilder::new().policy(Policy::default()).storage(storage).build().
For a framework-neutral HTTP/1.1 wrapper with HTTP-01 handling and HTTPS
redirects, use certmagic::https or certmagic::https_on.
More in examples/: basic_https,
on_demand (handshake-time issuance),
custom_dns01 (wildcard via a custom DNS provider).
Integration with rustls
Three paths, pick per deployment:
| Path | API | Behavior |
|---|---|---|
| Sync cache resolver | Config::tls_config() |
Serves from the in-memory cache, no IO |
| Async acceptor | Config::certmagic_acceptor() |
Full async resolution (storage load / on-demand issuance / maintenance) before the handshake completes |
| Background remediation | automatic in tls_config() when on-demand is on |
Cache miss spawns issuance in the background; this handshake fails, the next one succeeds |
Feature flags
| Feature | Default | Enables |
|---|---|---|
file-storage |
✔ | FileStorage backend (atomic writes, heartbeat lockfiles) |
http-01 |
✔ | HTTP-01 challenge listener (Tokio TCP) |
dns-01 |
✔ | DNS propagation checks (hickory-resolver) |
ocsp |
✔ | OCSP stapling lifecycle (hand-rolled RFC 6960 codec) |
zerossl |
✔ | ZeroSSL ACME/EAB and REST API issuers |
local-cache |
Node-local read-through storage cache | |
rsa |
Opt-in RSA 2048/4096/8192 key generation | |
ring |
Ring crypto provider and x509-parser/verify |
|
aws-lc-rs |
✔ | AWS-LC crypto provider and x509-parser/verify-aws (including P-521 CSR signing) |
integration-tests |
Pebble end-to-end tests |
Testing
# Real ACME end-to-end against Pebble (Let's Encrypt's reference server):
PEBBLE_CHALLENGE=http-01
PEBBLE_CHALLENGE=tls-alpn-01
# Deterministic boundary check (forces Cargo offline mode):
# Explicit local Pebble lane (never a production CA test):
# Listener/handler checks on ephemeral loopback ports (no CA or public DNS):
The Pebble test exercises the full lifecycle — account registration, selected
HTTP-01/TLS-ALPN-01/DNS-01 validation, issuance, storage persistence, and
forced renewal — and is the reason several protocol details (lowercase
application/jose+json, optional challenge tokens, order ready → finalize
ordering) are correct. HTTP-01 and TLS-ALPN-01 modes disable challtestsrv's
canned challenge responders so the certmagic solver itself serves Pebble's
validation request on the configured high port.
Project layout
src/
├── acme/ # ACME protocol layer: transport, JWS, directory, orders, issuer
├── solvers/ # http-01 / tls-alpn-01 / dns-01 / distributed challenge solvers
├── storage/ # Storage/Locker traits + FileStorage (atomic writes, locks)
├── ocsp/ # RFC 6960 codec + stapling lifecycle
├── certificate.rs # parsing, name matching, subject qualification, renewal math
├── cache.rs # in-memory certificate cache
├── config.rs # orchestration: obtain / renew / manage / revoke
├── handshake.rs # handshake-time issuance + on-demand gating
├── tls_integration # rustls glue (three integration paths)
└── runtime.rs # retry budget, job manager, single-flight
License
Apache-2.0 — see LICENSE.
Acknowledgments
- Special thanks to caddyserver/certmagic — this project follows its design.
- Thanks to salvo-rs/certon — its API naming inspired the compatibility aliases and high-level manager shipped alongside the native API.