certmagic 0.1.0

Automatic TLS certificate acquisition, renewal, and maintenance for Rust servers
Documentation

certmagic

Automatic TLS certificate acquisition, renewal, and maintenance for Rust servers.

CI Audit Crates Documentation Dependency status Crates.io Total Downloads Crates.io License

English | 简体中文


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

[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:

[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

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:

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/: 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

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.

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.