Architecture
Implementation Guide — This section is for contributors working on the Akāmu source code. It covers architecture, internal module design, database schema, CA internals, and testing. If you are deploying or operating the server, see the Operator Guide. If you are building an ACME client or integrating with the API, see the API Reference.
This chapter describes the overall structure of Akāmu, the key modules, and the full request lifecycle from a TCP connection to an HTTP response.
System architecture
graph TB
subgraph clients["ACME Clients"]
certbot[certbot]
acmesh[acme.sh]
akamucli[akamu-cli]
custom[RFC 8555 library]
end
subgraph akamu["Akāmu Server"]
direction TB
tls["TLS layer<br/>rustls + tokio-rustls<br/>(optional)"]
acme["ACME endpoints<br/>new-account · new-order · finalize<br/>revoke · ARI · key-change"]
jose["akamu-jose<br/>JWK / JWS verification<br/>EAB HMAC check"]
ca["CA module<br/>· CSR validation · certificate issuance · CRL generation"]
db[("SQL database<br/>· accounts · orders · authzs · challenges · certs · nonces")]
val["Validators<br/>· http-01 · dns-01 · tls-alpn-01 · dns-persist-01 · onion-csr-01"]
mtc["MTC log<br/>synta-mtc<br/>(optional)"]
end
subgraph external["Applicant Infrastructure"]
httpserver["HTTP server<br/>· port 80"]
dns["DNS server<br/>· TXT records"]
tlsserver["TLS server<br/>· port 443<br/>· ALPN acme-tls/1"]
end
subgraph artifacts["Issued Artifacts"]
certchain["X.509 certificate chain<br/>PEM bundle"]
crlsvc["CRL / OCSP service<br/>(external, referenced by URL)"]
end
certbot -->|"HTTPS ACME requests (JWS-signed)"| tls
acmesh --> tls
akamucli --> tls
custom --> tls
tls --> acme
acme --> jose
acme --> ca
acme --> db
acme -->|"spawns tokio task"| val
acme --> mtc
val -->|"GET /.well-known/…"| httpserver
val -->|"TXT _acme-challenge.…"| dns
val -->|"TLS ALPN connect"| tlsserver
ca -->|"leaf + CA bundle"| certchain
ca -->|"revoked serial list"| crlsvc
Crate layout
The repository is organized as a Cargo workspace with eight members:
Cargo.toml <- workspace root (members: ., crates/*)
src/ <- akamu server binary
crates/
akamu-jose/ <- JWK/JWS primitives (no HTTP/DB deps)
akamu-client/ <- async ACME client library (tokio, hyper)
akamu-cli/ <- CLI binary wrapping akamu-client
akamu-cosigner/ <- standalone MTC cosigner daemon
akamu-ldap/ <- safe Rust wrapper around the OpenLDAP C library (libldap);
provides LdapConnection (sync) and AsyncLdapConnection
(tokio::task::spawn_blocking) with simple-bind and
SASL GSSAPI/Kerberos authentication
akamu-gssapi/ <- safe Rust GSSAPI/SPNEGO bindings (FFI to libgssapi_krb5);
provides GssServerCred and GssClientCred for keytab- and
ccache-based credential acquisition, plus accept_token /
init_token convenience wrappers; MIT Kerberos thread-safety
guarantees allow an Arc<GssServerCred> to be shared across
all handler threads without a mutex
akamuctl/ <- server administration CLI binary; talks to /admin/* endpoints
over mTLS or GSSAPI/Kerberos (ccache-based) authentication
Crate dependencies
graph LR
SERVER["akamu (server)"]
CLIENT["akamu-client"]
CLI["akamu-cli"]
COSIGNER["akamu-cosigner"]
JOSE["akamu-jose"]
SYNTA["synta-certificate"]
SYNTABASE["synta"]
SYNTAVERIF["synta-x509-verification"]
LDAP["akamu-ldap"]
GSSAPI["akamu-gssapi"]
AKAMUCTL["akamuctl"]
NATIVEOSSL["native-ossl"]
HYPERRUSTLS["hyper-rustls"]
SERVER --> JOSE
SERVER --> SYNTA
SERVER --> SYNTABASE
SERVER --> SYNTAVERIF
SERVER --> LDAP
SERVER --> GSSAPI
SERVER --> NATIVEOSSL
SERVER --> HYPERRUSTLS
CLIENT --> JOSE
CLIENT --> SYNTA
CLIENT --> GSSAPI
CLIENT --> HYPERRUSTLS
CLI --> CLIENT
COSIGNER --> CLIENT
COSIGNER --> SERVER
COSIGNER --> NATIVEOSSL
COSIGNER --> HYPERRUSTLS
JOSE --> SYNTA
SYNTA --> SYNTABASE
AKAMUCTL --> GSSAPI
AKAMUCTL --> SYNTA
AKAMUCTL --> NATIVEOSSL
The server and akamu-client both depend directly on akamu-jose and synta-certificate. The server additionally depends on:
akamu-ldapfor reading Dogtag and IPA certificate profiles from LDAP.akamu-gssapifor standalone SPNEGO authentication (gss_cred,admin_gss_credinAppState).native-ossldirectly for digest operations (SHA-256 fingerprinting, channel-binding hashes), HKDF key derivation (eab_derivation.rs), and asymmetric-key signature verification in the mTLS verifier.rustls-native-ossl(a thin rustls crypto-provider shim overnative-ossl) supplies the OpenSSL backend to rustls.synta(the base ASN.1 codec crate) forDecoder/Encoderand theIntegertype used in certificate serial number and OID handling;synta-certificateis built on top of it.synta-x509-verificationfor X.509 certification-path validation (trust anchor resolution, name constraints, key usage checking) used in mTLS client-certificate verification and OCSP response validation.hyper-rustlsas the HTTPS connector for outbound MTC cosigner requests; paired withrustls-native-certsto load the OS root CA store for verifying cosigner TLS certificates.
akamu-client uses akamu-gssapi for GSSAPI-authenticated ACME requests and hyper-rustls for all outbound HTTPS. akamu-cli depends only on akamu-client. akamu-cosigner depends on both akamu-client (for ACME EAB bootstrap) and akamu itself (to reuse TLS loader helpers and key generation utilities), and uses native-ossl directly for crypto operations. akamuctl is a standalone admin CLI that depends on akamu-gssapi for ccache-based Kerberos login to the /admin/* endpoints, on synta-certificate for certificate handling, and on native-ossl directly; it does not depend on akamu-client or akamu-jose.
Key external dependencies
The following external crates are direct server dependencies whose role is not otherwise obvious from the module descriptions above.
| Crate | Purpose |
|---|---|
uuid | Generates UUIDv4 identifiers for every ACME resource (orders, authorizations, challenges, certificates, nonces). |
getrandom | Cryptographically random byte source for anti-replay nonce generation (NonceBucket). |
subtle | Constant-time byte comparison (subtle::ConstantTimeEq) used in admin session-token lookup to prevent timing-based token recovery. |
zeroize | Zeroizing<T> wrapper that zeroes memory on drop; applied to the EAB master secret in AppState to satisfy FDP_RIP.1 (residual information protection). |
ipnet | CIDR range type (IpNet) used to match incoming request source addresses against the [server].trusted_proxies allow-list. |
regex | Compiled regular expressions for profile identifier authorization (auth.rs check_profile_auth — identifier pattern matching against the allowed_identifiers field). |
libc | flock(2) call that takes an exclusive advisory lock on the MTC log file, preventing two server processes from writing the same log concurrently. |
rustls-native-certs | Loads the operating-system root certificate store into rustls for verifying cosigner HTTPS connections. |
indexmap | IndexMap preserves config-file insertion order for the multi-CA registry (AppState::cas), ensuring deterministic directory listing and default-CA fallback. |
See Client Libraries for the standalone client API.
The server’s jose/ module
src/jose/jwk.rs and src/jose/jws.rs are thin re-exports:
#![allow(unused)]
fn main() {
// src/jose/jwk.rs
pub use akamu_jose::JwkPublic;
// src/jose/jws.rs
pub use akamu_jose::{JwsFlattened, JwsKeyRef, JwsProtectedHeader};
}
All JWK/JWS logic lives in crates/akamu-jose. The src/jose/ shim exists so the rest of the server can use short import paths without knowing about the crate boundary.
Server source layout
The src/ directory is organized as follows:
src/
main.rs Entry point; parses config, initializes subsystems, starts axum
lib.rs Re-exports public modules for integration tests
config.rs TOML configuration structs (Config, CaConfig, MtcConfig, ServerConfig,
ProfilesConfig, ProviderConfig, BuiltinProviderConfig, AdminConfig, …)
state.rs Shared application state (AppState, CaState, MtcState, NonceBucket,
CrlCache, TlsState, CachedAccount, OperatorRole, AdminSession, …)
error.rs AcmeError enum with HTTP mapping and problem+json serialization
audit.rs Structured audit trail (FAU family): AuditEvent, AuditState
(VecDeque-backed violation window), AuditPolicy, record_or_log,
track_record_result, overflow handling (FAU_STG.4)
dns.rs Thin DNS query helper (hickory-resolver; optional DNS-over-TLS)
eab_derivation.rs HKDF-SHA-256 (RFC 5869) credential derivation for /acme/eab
extract.rs Axum extractors: RemoteUser (proxy header or standalone GSSAPI)
star.rs RFC 8739 ACME STAR background reissuance task
delegation_upstream.rs RFC 9115 upstream CA polling task (see delegation/ module)
util.rs Shared utilities: unix_now, unix_to_rfc3339, certificate helpers
admin/
mod.rs Re-exports admin submodules
auth.rs Operator authentication (mTLS cert or GSSAPI/Kerberos session token),
OperatorContext extractor, session management (POST/DELETE /admin/session)
init.rs Admin operator bootstrap — seeds the first operator from config on startup
db/
mod.rs Database initialization (open, migrations, WAL mode)
schema.rs Row types mirroring database columns
accounts.rs CRUD for accounts table
(audit.rs removed — audit events are now written to the systemd journal namespace via src/journal.rs)
authz.rs CRUD for authorizations table
certs.rs CRUD for certificates table (includes mtc_standalone_der column)
challenges.rs CRUD for challenges table
checkpoints.rs CRUD for mtc_checkpoints table (upsert, get_latest, prune_oldest)
cosignatures.rs CRUD for mtc_cosignatures table
cross_certs.rs CRUD for cross_certs table (insert, get_by_id, list by issuer/subject CA)
eab.rs CRUD for eab_keys table
landmarks.rs CRUD for mtc_landmarks table (insert, get_by_seq, list, prune_oldest)
nonces.rs Anti-replay nonce management
operators.rs CRUD for operators table (insert, get_by_fingerprint/principal, update,
failed-attempt tracking and account locking — FIA_AFL.1)
orders.rs CRUD for orders table
delegations.rs CRUD for delegations table (insert, get_by_id, update, delete, list, list_by_account)
stats.rs Aggregate statistics queries for GET /admin/stats
routes/
mod.rs Router assembly (build_router, build_admin_router), CaId extractor,
shared helpers (parse_jws, acme_headers, json_response, acme_prefix)
directory.rs GET /acme/directory
nonce.rs HEAD/GET /acme/new-nonce
account.rs POST /acme/new-account, POST /acme/account/{id}
order.rs POST /acme/new-order, POST /acme/order/{id}
authz.rs POST /acme/new-authz, POST /acme/authz/{id}
challenge.rs POST /acme/chall/{authz_id}/{type}
finalize.rs POST /acme/order/{id}/finalize
certificate.rs GET /acme/cert/{id}, POST /acme/cert/{id}
(auto-detects MTC vs X.509 by PEM marker)
star_cert.rs GET/POST /acme/cert/star/{order_id} (RFC 8739 §3.3 STAR rolling cert URL)
revoke.rs POST /acme/revoke-cert
key_change.rs POST /acme/key-change
renewal_info.rs GET /acme/renewal-info/{cert_id}
mtc.rs GET /acme/mtc/tree-size, /root, /inclusion-proof/{id},
/cert/{id}/standalone, /landmarks, /landmarks/{seq}/cert;
C2SP tlog-tiles: /acme/mtc/tlog/checkpoint, /tlog/cosignature,
/tlog/tile/{*path}
crl.rs GET /ca/crl, GET /ca/{ca_id}/crl — serve DER-encoded CRLs (cached);
GET /ca/cross-certs, GET /ca/{ca_id}/cross-certs — PEM cross-cert bundles
ocsp.rs POST /ca/ocsp, GET /ca/ocsp/{request},
POST /ca/{ca_id}/ocsp, GET /ca/{ca_id}/ocsp/{request} (RFC 6960)
eab_identity.rs GET /acme/eab — derive and return EAB credentials for
the authenticated principal (proxy or standalone GSSAPI)
delegation.rs POST /acme/delegations/{account_id} (list; POST-as-GET),
POST /acme/delegation/{id} (fetch one; POST-as-GET) —
RFC 9115 NDC-facing delegation endpoints; active when
server.delegation_enabled = true
admin.rs All /admin/* endpoints served on the dedicated admin listener;
POST/DELETE /admin/session (auth);
GET/POST /admin/operators, GET/PUT/PATCH /admin/operators/{id},
POST /admin/operators/{id}/unlock;
GET/POST/PUT/DELETE /admin/account/{id}/profile-grants,
POST /admin/account/{id}/deactivate;
GET /admin/accounts, GET /admin/account/{id};
POST/GET/DELETE /admin/eab, GET/DELETE /admin/eab/{kid};
GET /admin/audit;
GET /admin/certs, GET /admin/certs/{id},
GET /admin/certs/{id}/download;
GET/POST /admin/profiles, PUT/DELETE /admin/profiles/{id};
GET /admin/orders, GET /admin/orders/{id};
GET /admin/config;
POST /admin/crl/force, POST /admin/revoke;
GET /admin/stats;
GET /admin/cas, GET /admin/cas/{id}, GET /admin/cas/{id}/cert;
POST /admin/ca/{id}/crl/force, POST /admin/ca/{id}/cross-sign;
GET /admin/cross-certs, GET /admin/cross-certs/{id};
GET/POST /admin/delegations, GET/PUT/DELETE /admin/delegations/{id}
(role-based access control; admin listener not started when [admin] absent)
ca/
mod.rs Re-exports ca submodules
init.rs CA key and certificate load-or-generate
key_loader.rs CaKeyLoader — routes key loading to PEM file or PKCS#11 token URI
csr.rs PKCS#10 CSR parsing and validation
issue.rs End-entity and CA certificate issuance (issue_certificate,
issue_with_params, issue_ca_cert, check_is_ca_cert)
revoke.rs CRL generation
profiles/
mod.rs ProfileRegistry — in-memory cache, background refresh, resolve()
builtin.rs Builtin provider: inline TOML profile declarations;
handles issue_as, allowed_identifiers, auth_hook, require_account_grant
auth.rs check_profile_auth — identifier pattern, external hook, account grant checks
cfg.rs Dogtag Java-properties .cfg parser and translator
dogtag.rs Dogtag PKI provider (filesystem or LDAP — simple bind and GSSAPI/Kerberos)
ipa.rs FreeIPA/IPAThinCA provider (filesystem or LDAP — simple bind and GSSAPI/Kerberos)
ldap_resolve.rs resolve_ldap_uris() — merges uri/uris/srv_domain into a space-separated
URI string for ldap_initialize; SRV records sorted per RFC 2782
ldap_session.rs Shared LDAP connect→bind→search→parse helper used by dogtag and ipa providers
tls/
mod.rs Re-exports tls submodules; module-level doc for standalone TLS support
channel_binding.rs TLS channel binding helpers (tls-unique / tls-exporter)
init.rs load_or_generate — loads or auto-generates the server TLS certificate
loader.rs Async TLS config reloader (hot-reload of cert/key without restart)
schemes.rs Custom rustls SignatureScheme negotiation for hybrid post-quantum keys
verifier.rs SyntaClientCertVerifier — mTLS client certificate verification
(CAB Forum or RFC 5280 profile; hybrid ML-DSA+classical chains)
validation/
mod.rs Challenge dispatch and DB state transitions (validate_challenge)
http01.rs http-01 validation (hyper HTTP client)
dns01.rs dns-01 validation (hickory-resolver)
tls_alpn01.rs tls-alpn-01 validation (rustls TLS client)
dns_persist_01.rs dns-persist-01 validation (hickory-resolver; persistent TXT records)
onion_csr_01.rs onion-csr-01 validation (CSR-based; .onion identifiers)
caa.rs CAA record validation (RFC 8659 + RFC 8657)
mtc/
mod.rs Re-exports mtc submodules
log.rs SharedLog type alias; tree_size, proof_and_tree_size async wrappers
checkpoint.rs Checkpoint production background task; signs and stores Merkle roots
cosign.rs CosignerClient; parallel cosignature gathering from external cosigners
landmark.rs Landmark background task; allocates and builds LandmarkCertificate DERs
standalone.rs StandaloneCertificate construction after each checkpoint
tlog.rs C2SP tlog-tiles and signed-note support: checkpoint format, key-ID
computation, cosignature production, hash tile computation for all
C2SP signature types (Ed25519, ECDSA, ML-DSA-44 cosignatures)
delegation/
mod.rs Re-exports delegation submodules
upstream.rs DelegationUpstreamTask — background tokio task that polls
processing delegation orders and drives the upstream ACME
flow (account registration, new-order, dns-01 deploy/poll,
finalize, cert retrieval) using [delegation_upstream] config;
not started when [delegation_upstream] is absent
jose/ Thin re-exports and helpers for crates/akamu-jose
mod.rs Module re-exports
jwk.rs Re-exports JwkPublic from akamu-jose
jws.rs Re-exports JwsFlattened, JwsKeyRef, JwsProtectedHeader from akamu-jose
eab.rs EAB (External Account Binding) HMAC verification helpers
kid.rs account_id_from_kid — extracts and validates the account ID from a JWS kid URL
Key types
AppState
Defined in src/state.rs. Every axum handler receives an Arc<AppState> via axum’s State extractor. It contains:
config: Arc<Config>— immutable configuration parsed at startup.db: crate::db::Db— write connection pool. All write transactions and most read queries use this pool.db_ro: crate::db::Db— read-only connection pool for SQLite file-backed databases (opened with?mode=roURI parameter). Pure-read handlers (get_order,get_authz,download_cert) route through this pool so WAL concurrent reads do not contend on the write lock. For:memory:databases and non-SQLite backends this is a clone ofdb.db_kind: DbKind— discriminant indicating the underlying database backend (SQLite, Postgres, MariaDB); used by a small number of queries that need backend-specific SQL.cas: Arc<IndexMap<String, Arc<CaState>>>— all configured CAs keyed by their ID, in config-file insertion order. Replaces the old singlecafield.default_ca_id: Arc<String>— the CA ID that serves the backward-compatible/acme/directoryand/ca/crlroutes. Set to the entry withis_default = truein[[ca]]config.crl_caches: Arc<HashMap<String, CrlCache>>— per-CA CRL cache keyed by CA ID. Each entry isNoneuntil the first CRL request for that CA. Replaces the old singlecrl_cachefield.link_headers: Arc<HashMap<String, Arc<HeaderValue>>>— per-CA precomputedLink: …; rel="index"header values keyed by CA ID.acme_headers(state, ca_id, nonce)looks up the header for the request’s CA, falling back to the default CA’s header. Replaces the old singlelink_headerfield.mtc: Arc<MtcState>— MTC log handle, signing key, and pre-built cosigner HTTPS clients.profiles: Arc<ProfileRegistry>— in-memory certificate profile cache; empty when no providers are configured, in which case every order falls back to CA defaults.tls: Option<Arc<TlsState>>— present when[tls]is enabled and client auth is configured; holds the client-auth config for introspection by handlers.nonces: Arc<NonceBucket>— in-memory anti-replay nonce store.spki_cache: Arc<RwLock<HashMap<String, CachedAccount>>>— per-account SPKI/thumbprint cache to avoid a DB round-trip per authenticated request after the first.validation_client: ValidationClient— shared hyper HTTP client for http-01 challenge validation; connection-pooled so TCP connections are reused across validations.gss_cred: Option<Arc<GssServerCred>>— server-side GSSAPI credential for standalone SPNEGO authentication.Nonewhen[server.gssapi]is absent.admin_gss_cred: Option<Arc<GssServerCred>>— admin-specific GSSAPI credential from[admin.gssapi]; takes precedence overgss_credfor admin SPNEGO.Nonewhen[admin.gssapi]is absent.eab_master_secret: Option<Arc<Zeroizing<Vec<u8>>>>— decoded master secret for HKDF-based EAB key derivation.Nonewhen[server].eab_master_secretis absent.journal: Arc<JournalWriter>— audit event writer. Connects to the systemd journal namespace socket, writes to a JSONL file ([server].audit_log_file), or uses an in-process store (development/CI). Always present.audit: Arc<AuditState>— shared in-memory audit state (overflow flag, FAU_ARP.1 alarm counter,VecDeque-backed violation timestamp window). Always present.audit_policy: Arc<AuditPolicy>— audit policy extracted from[admin]at startup.admin_sessions: Option<Arc<tokio::sync::Mutex<HashMap<String, AdminSession>>>>— opaque session token store for admin operator sessions.Nonewhen[admin]is absent.admin_auth_limiter: Option<AdminAuthLimiter>— per-source-IP credential-attempt timestamps for admin auth rate-limiting.Nonewhen[admin]is absent.startup_time: Instant— time the server process started; used for uptime reporting inGET /admin/stats.
AppState is Clone because Arc<T> is Clone and sqlx::AnyPool is Clone. Cloning is cheap (reference count bump). All mutable state (the database and MTC log) is protected at a lower level by sqlx’s internal pool management and a tokio::sync::Mutex<DiskBackedLog>, respectively.
CaState
Holds the key material and issuance policy for a single CA. Key fields:
id: String— the CA’s unique identifier, matchingCaConfig.idfrom the config file.key_type: String— the key algorithm string (e.g."ec:P-256","rsa:2048").key: BackendPrivateKey— CA private key used for certificate and CRL signing.cert_der: Vec<u8>— DER-encoded CA certificate.hash_alg: String— hash algorithm string for certificate and CRL signatures (e.g."sha256").validity_days: u32— default validity period for issued end-entity certificates.crl_url: Option<String>— optional CRL distribution point URL embedded in issued certificates.ocsp_url: Option<String>— optional OCSP responder URL embedded in issued certificates.aki_bytes: Vec<u8>— RFC 7093 §2 Method 1 key identifier bytes (leftmost 20 bytes of the SHA-256 of the CA public key BIT STRING). Used to validate the AKI component of ARI cert-ids (RFC 9773 §4.1).enforce_validity_cap: bool— whentrue,issue_with_paramsrejects issuance when the computed validity exceeds 200 days (CA/B Forum BR §6.3.2).crl_next_update_secs: u64— validity window for signed CRLs (determines cache TTL).caa_identities: Vec<String>— CAA domain identities specific to this CA; falls back to[server].caa_identitieswhen empty.
CaState is shared across all concurrent handler tasks via Arc<CaState>. The underlying BackendPrivateKey delegates to the OpenSSL backend, which serializes concurrent signing operations internally.
Two helpers on AppState provide access to CA instances:
AppState::get_ca(id: &str) -> Option<&Arc<CaState>>— look up a CA by ID; returnsNonefor unknown IDs.AppState::default_ca() -> &Arc<CaState>— return the default CA (the one designated byis_default = true). Panics only if the server was constructed incorrectly.
AcmeError
Defined in src/error.rs. Implements IntoResponse so it can be returned directly from axum handlers. Maps each variant to:
- An ACME problem type string (
urn:ietf:params:acme:error:*). - An HTTP status code.
- A human-readable
detailstring.
The response body is application/problem+json (RFC 7807).
Request lifecycle
1. TCP accept
The tokio runtime accepts a TCP connection on the configured listen_addr. axum’s serve function passes it to the hyper HTTP/1.1 or HTTP/2 codec.
2. HTTP parsing
hyper parses the HTTP request (method, URL, headers, body). Tower middleware is applied in order; currently only TraceLayer is configured, which emits a tracing span for each request.
3. Route dispatch
axum matches the request method and path against the router built in routes::build_router. Two route sets are registered:
- Legacy routes (
/acme/directory,/acme/new-account, etc.) — served by the same handlers, usingdefault_ca_idas the implicit CA context. - Per-CA routes (
/acme/{ca_id}/directory,/acme/{ca_id}/new-account, etc.) — the same handlers, but the{ca_id}path parameter selects the CA. Axum resolves static path segments before dynamic ones, so the legacy/acme/directoryroute is always matched before/acme/{ca_id}/directory; config validation ensures no CA ID collides with reserved ACME path segments ("directory","new-nonce", etc.).
The CaId extractor (src/routes/mod.rs) resolves the active CA for each request. When the {ca_id} path parameter is present it validates the value against state.cas and returns 404 Not Found for unknown IDs. On legacy routes without {ca_id}, it returns the default_ca_id. Every handler that is CA-aware accepts a CaId argument.
Each route maps to a handler function in the corresponding routes/ module. The handler receives the following extractors:
State(state): State<Arc<AppState>>— shared application state.CaId(ca_id): CaId— resolved CA identifier for this request.Path(...)— URL path parameters (e.g., order ID, authz ID).body: Bytes— raw request body for JWS verification.
The account_scope setting in [server] affects account creation and JWS validation. When set to "server" (the default), one account registration is valid for all CAs. When set to "ca", accounts are isolated per CA and each CA directory advertises its own /acme/{ca_id}/new-account endpoint; JWS kid validation additionally checks that the account’s ca_id matches the request’s CA.
4. JWS verification (POST endpoints)
Almost every POST endpoint calls routes::parse_jws before processing the payload:
- Parse: deserialize the
Bytesbody as a JWS flattened JSON serialization. - Decode header: base64url-decode the
protectedheader and parse the JSON. - URL check: compare
header.urlwith the expected full URL for this endpoint. A mismatch returnsunauthorized. - Nonce check: look up
header.noncein the in-memoryNonceBucketand atomically replace it with a fresh nonce. A missing or already-used nonce returnsbadNonce. The nonce store is in-memory (aMutex<HashMap>insideAppState::nonces); nonces issued before a server restart are silently dropped, and clients detect the resultingbadNonceand retry per RFC 8555 §6.5. - Key resolution: if the header uses
jwk, extract the SPKI DER from the JWK directly. If it useskid, look up the account in the database and fetch its stored SPKI DER. - Signature verification: verify the JWS signature over
protected || "." || payloadusing the resolved public key viasynta-certificate. Classical algorithms (RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA) useverify_signature. ML-DSA algorithms (ML-DSA-44,ML-DSA-65,ML-DSA-87) are dispatched first — their raw-byte signatures (not DER) are verified withverify_ml_dsa_with_contextusing an empty context string, as required by RFC 9964 §4. - Payload decode: base64url-decode the
payloadfield.
The result is a JwsContext struct containing the decoded header, payload bytes, SPKI DER, and optional account ID.
5. Business logic
Each handler implements the ACME protocol semantics for its endpoint: reading from and writing to the database, dispatching validation, invoking the CA, etc.
For write operations that span multiple tables (e.g., creating an order with its authorizations and challenges), the handler uses a single database transaction to ensure atomicity. On PostgreSQL, state-transition transactions (new-order, new-authz, challenge) additionally issue SET LOCAL synchronous_commit = off via db::pg_local_async_commit to eliminate per-commit WAL flush overhead; the certificate issuance transaction retains full durability.
6. Response construction
Handlers return Result<Response, AcmeError>. On success, they call routes::json_response, which:
- Generates a new anti-replay nonce.
- Stores it in the in-memory
NonceBucket. - Adds the
Replay-NonceandLink: <directory>; rel="index"headers, selecting the correctLinkvalue fromstate.link_headersfor the active CA. - Serializes the JSON body and sets
Content-Type: application/json.
On error, AcmeError::into_response builds a application/problem+json body.
7. Background tasks
Challenge validation does not block the HTTP response. When a challenge is triggered, the handler:
- Marks the challenge
processingin the database. - Spawns a
tokio::spawntask runningvalidation::validate_challenge. - Spawns a second observer task watching for panics via
JoinHandle::await. - Returns immediately with the
processingstatus.
Similarly, MTC log appends are spawned as background tasks after certificate issuance.
A third background task (ProfileRegistry::spawn_refresh_task) wakes every refresh_interval_secs seconds, re-loads all configured profile providers, and atomically replaces the in-memory ProfileCache under a write lock. It holds a weak Arc reference to the registry so that dropping the server’s strong reference causes the task to exit cleanly on shutdown.
A fourth background task (DelegationUpstreamTask) is started at server startup when [delegation_upstream] is configured. It wakes every poll_interval_secs seconds (default 10), queries the database for delegation orders in processing status, and drives each one through the upstream ACME flow: it contacts the upstream CA’s directory, registers an account (if not already registered), submits a new-order, triggers the dns-01 challenge by invoking challenge_deploy_script, polls until the upstream authorization is valid, calls challenge_cleanup_script (if configured), finalizes the order, and stores the resulting certificate URL back into the order row. On success the order transitions to valid; on any unrecoverable upstream failure it transitions to invalid.
Database access model
All database access goes through sqlx::AnyPool. Queries are async and run directly on the tokio runtime. This means:
sqlx::query!(...)/sqlx::query_as!(...)are the primary way to issue queries.- For multi-statement atomicity, acquire a transaction with
pool.begin().await?and commit withtx.commit().await?.
Foreign key enforcement is enabled at database open time (PRAGMA foreign_keys=ON). WAL journal mode is also enabled after migrations (PRAGMA journal_mode=WAL).
Async design
The server is fully async on the tokio runtime. All I/O — TCP, HTTP, DNS, TLS — is async. CPU-bound work (DER encoding for MTC) is offloaded to tokio::task::spawn_blocking.
The only shared mutable state in the async domain is the Mutex<DiskBackedLog> for the MTC log. All other state is either immutable after startup (AppState, Config, CaState) or encapsulated in the database background thread.