<div align="center">
# certmagic
**Automatic TLS certificate acquisition, renewal, and maintenance for Rust servers.**
[](https://github.com/houseme/certmagic/actions/workflows/ci.yml)
[](https://github.com/houseme/certmagic/actions/workflows/audit.yml)
[](https://crates.io/crates/certmagic)
[](https://docs.rs/certmagic)
[](https://deps.rs/repo/github/houseme/certmagic)
[](https://crates.io/crates/certmagic)
[](https://crates.io/crates/certmagic)
</div>
---
## 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 `DecisionFunc` or 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 `CertStore` backend while ACME accounts, locks and OCSP data stay
on the ground-truth `Storage`.
- **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
```toml
[dependencies]
certmagic = "0.1"
tokio = { version = "1", features = ["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:
```toml
[dependencies]
certmagic = { version = "0.1", default-features = false, features = [
"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
```rust,no_run
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let domain = "example.com"; // must point at this server
let cache = certmagic::Cache::new(Default::default())?;
let options = certmagic::ConfigOptions {
issuers: vec![Arc::new(certmagic::AcmeIssuer::lets_encrypt())],
..Default::default()
};
let config = certmagic::Config::new(cache, options)?;
let ct = CancellationToken::new();
// Obtain now, renew automatically in the background.
config.manage_sync(&ct, &[domain.to_owned()]).await?;
// Serve HTTPS with rustls.
let acceptor = Arc::new(config.certmagic_acceptor()?);
let listener = tokio::net::TcpListener::bind("0.0.0.0:443").await?;
loop {
let (tcp, _) = listener.accept().await?;
let acceptor = Arc::clone(&acceptor);
tokio::spawn(async move {
if let Ok(tls) = acceptor.accept(tcp).await
&& let Ok(mut tls) = tls.await
{
// `tls` is a ready TLS stream; hand it to your HTTP stack.
}
});
}
}
```
Prefer a high-level manager? `CertManager` wraps the same machinery behind a
cancellation-free facade:
```rust,no_run
let manager = certmagic::CertManager::builder()
.issuers(vec![std::sync::Arc::new(certmagic::AcmeIssuer::lets_encrypt())])
.build()?; // try_build() is the explicit fallible alias
manager.manage(&["example.com".to_owned()]).await?; // foreground obtain + renew
let acceptor = std::sync::Arc::new(manager.config().certmagic_acceptor()?);
```
`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/`](examples/): [`basic_https`](examples/basic_https.rs),
[`on_demand`](examples/on_demand.rs) (handshake-time issuance),
[`custom_dns01`](examples/custom_dns01.rs) (wildcard via a custom DNS provider).
## Integration with rustls
Three paths, pick per deployment:
| 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
| `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
```sh
cargo test # unit tests (offline)
cargo test --lib --all-features # everything, all feature combinations
# Real ACME end-to-end against Pebble (Let's Encrypt's reference server):
./tests/run-pebble.sh # DNS-01 by default
PEBBLE_CHALLENGE=http-01 ./tests/run-pebble.sh
PEBBLE_CHALLENGE=tls-alpn-01 ./tests/run-pebble.sh
# Deterministic boundary check (forces Cargo offline mode):
./tests/external-validation.sh --offline
# Explicit local Pebble lane (never a production CA test):
./tests/external-validation.sh --pebble
# Listener/handler checks on ephemeral loopback ports (no CA or public DNS):
./tests/external-validation.sh --loopback-challenges
```
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](LICENSE).
## Acknowledgments
- Special thanks to [caddyserver/certmagic](https://github.com/caddyserver/certmagic) —
this project follows its design.
- Thanks to [salvo-rs/certon](https://github.com/salvo-rs/certon) — its API
naming inspired the compatibility aliases and high-level manager shipped
alongside the native API.