docs(arch): TlsError shape — single enum, owned by alknet-tls (ADR-088, resolves OQ-63)

Grounded in the actual error-producing call sites (endpoint.rs server
side, call_client.rs client side) and the dependency-crate sources read
from the cargo cache (rustls 0.23.41, rustls-pemfile 2.2.0, rcgen 0.13.2,
quinn-proto 0.11.15, rustls-acme 0.12.1).

Decision: single #[non_exhaustive] enum, one variant per failure
category, owned by alknet-tls (not re-exported from core). Six variants:
CertLoad(io::Error), SelfSigned(rcgen::Error), Rustls(rustls::Error),
VerifierBuild(VerifierBuilderError), QuinnWrap(NoInitialCipherSuite)
[quinn-gated], AcmeConfig(String).

Three findings drove single-enum over thin wrapper: (1) for_quinn()
fails with NoInitialCipherSuite, not rustls::Error — a rustls::Error
wrapper cannot represent the for_quinn() failure; (2) rustls_pemfile::Error
is not a std::error::Error (no Display, no Error impl) so #[from] would
not compile — pemfile BufRead APIs return io::Error; (3)
WebPkiServerVerifier::build() returns VerifierBuilderError, not
rustls::Error — a thin wrapper cannot represent empty-CA-root-store as
a first-class failure.

Deliberately NOT variants: ACME EventError/OrderError (stream events,
logged not returned from new); unknown-raw-key fail-closed (handshake-
time rejection at dial time, not a config-construction error —
corrects OQ-63's original framing); provider init (infallible); resolver
construction (infallible).
This commit is contained in:
glm-5.2 committed 2026-07-15 09:22:24 +00:00
1 parent 5941280bca
commit 43b8179304
5 files changed
+573 -23

No files matched your search

@@ -0,0 +1,438 @@
# ADR-088: `TlsError` Shape — Single Enum, Owned by `alknet-tls`
## Status
Accepted (resolves OQ-63)
## Context
OQ-63 flagged that `TlsError` is referenced in the public signatures of
`TlsServerConfig::new`, `TlsClientConfig::new`, and `for_quinn()` (ADR-082,
ADR-087) but was never sketched. An implementer cannot write the crate
without deciding the shape: a thin wrapper around `rustls::Error` /
`io::Error` (less code, no matchable context) or a single enum with variants
per failure category (more code, the assembly layer can distinguish
"cert file missing" from "quinn rejected the rustls config" from
"verifier build failed because the root store is empty"). This ADR makes
that decision, grounded in the actual error-producing call sites and the
actual error types the dependency crates return.
### What was verified (the grounding)
The decision is grounded in two sources, not memory:
1. **The current code** — `crates/alknet-core/src/endpoint.rs`
(server-side: `build_rustls_server_config`, `TlsSetup::new`,
`TlsSetup::new_acme`, `build_quinn_server_config_from_rustls`,
`load_cert_chain`, `load_private_key`, `generate_self_signed_cert`)
and `crates/alknet-call/src/client/call_client.rs` (client-side:
`build_quinn_client_config`, `build_client_auth`,
`select_server_verifier`, `load_platform_root_cert_store`). Both
currently funnel every error into `io::Error` (server) or `String`
(client), losing all context.
2. **The dependency-crate sources** (rustls 0.23.41, rustls-pemfile 2.2.0,
rcgen 0.13.2, quinn-proto 0.11.15, rustls-acme 0.12.1, rustls-pki-types
1.14.1) — read from the cargo cache to confirm the exact error type
each call site returns, whether it implements `std::error::Error`, and
whether it is `Send + Sync + 'static`. Also confirmed: `rustls::Error`'s
`std::error::Error` impl is empty (no `source()` — the detail is in
`Display`, not a source chain); `rustls_pemfile::Error` has no
`Display` and no `Error` impl (only `From<Error> for io::Error`); the
`quinn` crate re-exports `quinn_proto::crypto` as `quinn::crypto`, so
`quinn::crypto::rustls::NoInitialCipherSuite` is the public path. The
full mapping is below.
### The actual error-producing call sites
**Server side (`TlsServerConfig::new` + `for_quinn`):**
| Call site | Error type | Notes |
|-----------|-----------|-------|
| `std::fs::read(cert_path)` / `read(key_path)` | `io::Error` | file read; cert/key load |
| `rustls_pemfile::certs()` / `private_key()` | `io::Error` | pemfile funnels its own `pemfile::Error` into `io::Error` (it does not impl `std::error::Error` — see "Gotchas") |
| "no private key found in file" | `io::Error(InvalidData)` | the `Ok(None)` case from `private_key()` |
| `rcgen::KeyPair::generate()` / `CertificateParams::self_signed()` | `rcgen::Error` | self-signed cert generation |
| `ServerConfig::builder_with_provider(...).with_safe_default_protocol_versions()` | `rustls::Error` | protocol-version / cipher-suite selection |
| `.with_single_cert(cert_chain, key)` | `rustls::Error` | key load via provider + `keys_match` |
| `RawKeyCertResolver::new(...)` | **infallible** | builds `CertifiedKey` in-memory; `ResolvesServerCert` has no construction error |
| `QuicServerConfig::try_from(rustls::ServerConfig)` (in `for_quinn`) | **`NoInitialCipherSuite`** | NOT `rustls::Error` — a distinct struct (re-exported at `quinn::crypto::rustls::NoInitialCipherSuite`, defined in `quinn_proto::crypto::rustls`); the one path where `for_quinn()` fails |
| "ACME feature not enabled but `TlsIdentity::Acme` configured" | `io::Error(Unsupported)` | the `#[cfg(not(feature = "acme"))]` guard |
| `AcmeConfig::new()` / `AcmeConfig::state()` | **infallible** | ACME state-machine construction does not fail; errors are stream events (`EventError`), logged not returned (see "ACME") |
**Client side (`TlsClientConfig::new`):**
| Call site | Error type | Notes |
|-----------|-----------|-------|
| `ClientConfig::builder_with_provider(...).with_safe_default_protocol_versions()` | `rustls::Error` | same as server-side |
| `FingerprintPinVerifier::new(...)` | **infallible** | stores the fingerprint + supported algorithms; the known-peer path |
| `WebPkiServerVerifier::builder_with_provider(...).build()` | **`VerifierBuilderError`** | `rustls::webpki::VerifierBuilderError` — `NoRootAnchors`, `InvalidCrl`; the unknown-X.509-remote path |
| `rustls::RootCertStore::add(cert)` | `rustls::Error` | adding a native root cert (maps `webpki::Error` → `InvalidCertificate(...)`) |
| `rustls-native-certs::load_native_certs()` errors | logged, not returned | the current client code logs and continues; an empty store falls back to built-in webpki-roots |
| Client-auth cert resolver — RawKey | **infallible** | builds `CertifiedKey` in-memory |
| Client-auth cert resolver — X.509 | `io::Error` (load) + `rustls::Error` (`CertifiedKey::from_der`) | cert/key file load + key parse |
| "ACME TLS identity is server-only; cannot be used for client auth" | `io::Error`-shape | the `TlsIdentity::Acme` as client-identity guard |
| `aws_lc_rs::default_provider()` | **infallible** | returns `CryptoProvider` by value; provider init cannot fail |
### Gotchas that drive the enum-not-wrapper decision
Three findings from the dependency-crate source make a thin wrapper
**actively wrong** for this crate:
1. **`for_quinn()` fails with `NoInitialCipherSuite`, not `rustls::Error`.**
These are two distinct, non-overlapping types from two different crates.
A thin wrapper around `rustls::Error` cannot represent the `for_quinn()`
failure at all; a thin wrapper around `io::Error` (the current
server-side pattern) erases it into "other I/O error." The assembly
layer that calls `for_quinn()` needs to distinguish "the rustls config
was built fine but quinn rejected it for lacking a TLS 1.3 AES-128-GCM
initial cipher suite" from "the rustls config build failed" — those
have different remediations (the first is a provider/cipher-suite
config issue; the second is a cert/key issue).
2. **`rustls_pemfile::Error` is not a `std::error::Error`.** It has no
`Display` impl and no `Error` trait impl — it only has
`From<Error> for io::Error`. This is why every BufRead-based pemfile
API returns `io::Error`, not `pemfile::Error`. A `TlsError` that tried
to `#[from] rustls_pemfile::Error` would not compile. The crate's
`io::Error`-returning APIs are the correct surface to wrap.
3. **`WebPkiServerVerifier::build()` returns `VerifierBuilderError`,
not `rustls::Error`.** This is the client-side unknown-X.509-remote
path (ADR-034 §3). Its variants (`NoRootAnchors`, `InvalidCrl`) are
distinct from `rustls::Error::InvalidCertificate(...)`. A thin
`rustls::Error` wrapper cannot represent "the CA root store was
empty" as a first-class failure; the enum can.
A single enum with one variant per failure category gives the assembly
layer matchable context — the one thing the current `io::Error` /
`String` funneling destroys — and correctly represents the four
distinct error types (`rustls::Error`, `NoInitialCipherSuite`,
`VerifierBuilderError`, `rcgen::Error`, plus `io::Error` for file I/O
and the ACME-disabled guard) as separate variants instead of erasing
them into one.
### What the thin wrapper would lose
The current server-side code (`endpoint.rs`) wraps everything in
`EndpointError::TlsConfig(io::Error::other(e))`. The current client-side
code (`call_client.rs`) converts everything to `String`. Both lose the
matchable context. Concretely, an operator who sees
"tls config error: No such file or directory" cannot tell from the
error alone whether the cert path was wrong, the key path was wrong, or
the ACME cache dir was wrong — and an operator who sees
"tls config error: invalid cipher suite specified" cannot tell whether
that came from `with_safe_default_protocol_versions()` (a rustls
config issue) or from `QuicServerConfig::try_from` (a quinn-wrap issue).
The enum names the category; the wrapped source keeps the detail.
## Decision
### 1. `TlsError` is a single enum with variants per failure category
`TlsError` is a `#[non_exhaustive]` enum, one variant per failure
category, each variant wrapping the underlying error as its `#[source]`.
The decision is the **variant names and their wrapped types**; the
`#[from]` vs. explicit-field style and the `AcmeConfig(String)` vs.
structured-enum choice are two-way implementation details (see "Door
type"). The shape:
```rust
/// Errors produced by `TlsServerConfig::new`, `TlsClientConfig::new`,
/// and the transport accessors (`for_quinn`, `for_tcp_tls` is infallible).
///
/// One variant per failure category. The wrapped error is the
/// `#[source]` — match on the variant for the category, inspect the
/// source for the detail.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TlsError {
/// Cert or key file could not be read or parsed.
/// Source: `io::Error` (file read + pemfile parse, which pemfile
/// funnels into `io::Error`; see ADR-088 §"Gotchas" #2).
#[error("loading cert/key material: {0}")]
CertLoad(#[from] std::io::Error),
/// Self-signed certificate generation failed.
/// Source: `rcgen::Error`.
#[error("generating self-signed cert: {0}")]
SelfSigned(#[from] rcgen::Error),
/// rustls server or client config construction failed
/// (`with_safe_default_protocol_versions`, `with_single_cert`,
/// `CertifiedKey::from_der`, `RootCertStore::add`).
/// Source: `rustls::Error`.
#[error("building rustls config: {0}")]
Rustls(#[from] rustls::Error),
/// `WebPkiServerVerifier::build()` failed (the unknown-X.509-remote
/// client path — empty root store, invalid CRL). Distinct from
/// `Rustls` because the type is distinct and the remediation is
/// different (root-store / CRL config, not cert/key config).
/// Source: `rustls::webpki::VerifierBuilderError`.
#[error("building webpki verifier: {0}")]
VerifierBuild(#[from] rustls::webpki::VerifierBuilderError),
/// `QuicServerConfig::try_from(rustls::ServerConfig)` failed — the
/// one path where `for_quinn()` can fail. The rustls config was
/// built fine; quinn rejected it (no TLS 1.3 AES-128-GCM initial
/// cipher suite in the provider). Distinct from `Rustls` because
/// the type is `NoInitialCipherSuite`, not `rustls::Error`, and the
/// remediation is provider/cipher-suite config, not cert/key.
/// Source: `quinn_proto::crypto::rustls::NoInitialCipherSuite`.
#[cfg(feature = "quinn")]
#[error("wrapping rustls config for quinn: {0}")]
QuinnWrap(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
/// ACME feature not enabled but `TlsIdentity::Acme` configured, or
/// ACME identity used where a client identity is required.
/// Not a wrapped error — a configuration mismatch detected by the
/// crate itself.
#[error("ACME configuration error: {0}")]
AcmeConfig(String),
}
```
**The variant set, in words:**
- `CertLoad(io::Error)` — file read + PEM parse (cert, key, and via the
shared `load_cert_chain`/`load_private_key` helpers). Covers the
server X.509 path, the client X.509 client-auth path, and the
"no private key found" case. `#[from] io::Error` because `io::Error` is
the type the pemfile BufRead APIs actually return.
- `SelfSigned(rcgen::Error)` — self-signed cert generation (server
`SelfSigned` identity).
- `Rustls(rustls::Error)` — rustls server or client config construction
(`with_safe_default_protocol_versions`, `with_single_cert`,
`CertifiedKey::from_der`, `RootCertStore::add`). The largest failure
surface; both server and client paths.
- `VerifierBuild(VerifierBuilderError)` —
`WebPkiServerVerifier::build()` (the unknown-X.509-remote client path).
Distinct from `Rustls` because the type is distinct and the
remediation is root-store/CRL config, not cert/key config.
- `QuinnWrap(NoInitialCipherSuite)` — `for_quinn()`'s one failure path.
Feature-gated on `quinn`. Distinct from `Rustls` because the type is
distinct and the remediation is provider/cipher-suite config.
- `AcmeConfig(String)` — "ACME feature not enabled but
`TlsIdentity::Acme` configured" (server) and "ACME identity is
server-only; cannot be used for client auth" (client). A config
mismatch detected by the crate, not a wrapped third-party error.
### 2. What is NOT a variant (and why)
These are deliberately absent because they cannot be produced by the
crate's public API:
- **`rustls_pemfile::Error`** — not a `std::error::Error` (no `Display`,
no `Error` impl); pemfile's BufRead APIs return `io::Error`. Wrapped
as `CertLoad(io::Error)`. See §"Gotchas" #2.
- **ACME `EventError` / `OrderError` / `CertParseError`** — these are
stream events from the ACME state machine, not errors returned from
`TlsServerConfig::new`. `new` spawns the state machine and returns
immediately (ADR-082 §"`async fn new` — lifecycle semantics"); the
state machine's errors surface via the event stream and are logged
(the current code logs them in the spawned task, ADR-082 §"Behavior
preservation"). They do not flow through `TlsError`. See §"ACME" below.
- **Provider init error** — `aws_lc_rs::default_provider()` is
infallible (returns `CryptoProvider` by value). There is no
`ProviderInit` variant because provider init cannot fail.
- **`rustls::server::ResolvesServerCert` / `client::ResolvesClientCert`
construction** — infallible; the resolver traits have no construction
error. `RawKeyCertResolver::new` and the client-side
`RawKeyClientCertResolver::new` build `CertifiedKey` in-memory and
cannot fail.
- **`FingerprintPinVerifier::new`** — infallible; stores the fingerprint
+ supported algorithms. The known-peer client path has no construction
error.
- **The unknown-raw-key fail-closed** — this is NOT an error returned
from `TlsClientConfig::new`. It is a `Result::Err` the *caller* gets
when *trying to connect* to an unknown raw-key remote (the
`WebPkiServerVerifier` fails the handshake at dial time). It is not in
`TlsError`'s scope — `TlsError` is for config construction, not
handshake outcomes. OQ-63's framing listed it as a "client verifier
construction" failure mode; the research corrects this: the
fail-closed is a handshake-time rejection, not a config-construction
error. See §"The fail-closed distinction" below.
### 3. `TlsError` lives in `alknet-tls` (owned by the crate that produces it)
`TlsError` is defined in `alknet-tls` and is the sole TLS error surface
for the crate. It is not re-exported from `alknet-core`. Rationale:
- `alknet-core`'s `EndpointError` no longer has a `TlsConfig` variant
after ADR-083 (the endpoint takes no TLS config; the assembly layer
builds `TlsServerConfig` and hands pre-built transports to the
endpoint). Core does not need to know about `TlsError`.
- `alknet-tls` is the crate that produces the errors; it owns the type.
Re-exporting from core would invert the ownership (core re-exporting a
type from a crate that depends on it).
- The assembly layer (hub/worker) depends on `alknet-tls` directly (it
calls `TlsServerConfig::new` / `TlsClientConfig::new`); it gets
`TlsError` from that dependency, not from core.
This resolves the subsidiary question in OQ-63 ("does `TlsError` live in
`alknet-tls` or is it re-exported from `alknet-core`?"): **`alknet-tls`,
owned by the crate that produces it.**
### 4. Feature gates
`QuinnWrap` is gated on the `quinn` feature (it wraps a quinn type).
`VerifierBuild` wraps a `rustls::webpki` type that is always present
when `rustls` is present — it is not feature-gated. The ACME
config-mismatch variant (`AcmeConfig`) is present regardless of the
`acme` feature, because the "ACME configured but feature not enabled"
guard must exist even when `acme` is off (the `#[cfg(not(feature =
"acme"))]` branch returns `AcmeConfig("ACME feature not enabled...")`).
The enum is `#[non_exhaustive]` so adding variants (e.g., a future
`TcpWrap` if `for_tcp_tls` ever gains a failure path — it does not today)
is not a breaking change.
### 5. ACME — errors are stream events, not `TlsError` variants
`AcmeConfig::new()` and `AcmeConfig::state()` are infallible (confirmed
in the rustls-acme 0.12.1 source). `TlsServerConfig::new`'s ACME branch
spawns the state machine and returns immediately; the state machine's
errors (`EventError` — `CertCacheLoad`, `AccountCacheLoad`, `Order`,
`NewCertParse`, etc.) arrive asynchronously through the event stream
and are logged in the spawned task (the current code's pattern, preserved
by ADR-082 §"Behavior-preservation invariants"). They do not flow through
`TlsError`.
The only ACME-related `TlsError` is `AcmeConfig(String)` — the
configuration-mismatch guard ("ACME feature not enabled but
`TlsIdentity::Acme` configured" on the server; "ACME identity is
server-only; cannot be used for client auth" on the client). This is a
synchronous config error detected at `new` time, not an ACME-protocol
error.
If a future requirement wants to surface ACME state-machine errors to
the caller (rather than log them), that is a new ADR — it changes the
`TlsServerConfig` API (the ACME handle would need an error channel) and
is out of scope for the error-shape decision.
### 6. The fail-closed distinction
OQ-63's framing listed "unknown-remote fail-closed (not an error to
return — it's a `Result::Err` the caller gets for trying to connect to
an unknown raw-key remote)" as a client verifier construction failure
mode. The research corrects this: the fail-closed is **not** a
`TlsClientConfig::new` error. It is a handshake-time rejection produced
by the verifier at dial time, not a config-construction error. The
distinction:
- `TlsClientConfig::new` builds the config. If the *inputs* are bad
(empty root store → `VerifierBuilderError`, bad fingerprint format →
not currently a construction error because `FingerprintPinVerifier`
stores the string as-is and rejects at handshake), `new` returns
`TlsError`.
- The *handshake* (dial time) can fail with `rustls::Error` (the
verifier returns `InvalidCertificate(...)`). That error flows through
the transport's connector (`quinn::Endpoint::connect_with` →
`quinn::Connection` error; `TlsConnector::connect` → `io::Error`), not
through `TlsError`.
`TlsError` is the **config-construction** error type. Handshake-time
errors are the transport's error type. This keeps `TlsError` scoped to
what `new` and `for_quinn` can actually fail on, and avoids pretending
handshake outcomes are config-construction errors. A future ADR that
introduces a dial helper (the OQ-55 dial seam) would define how
handshake errors are surfaced then; that is not this ADR.
## Consequences
**Positive:**
- The assembly layer can match on `TlsError` to distinguish failure
categories — "cert file missing" (`CertLoad`) from "quinn rejected the
rustls config" (`QuinnWrap`) from "empty CA root store"
(`VerifierBuild`) from "self-signed generation failed" (`SelfSigned`).
The current `io::Error` / `String` funneling destroys this context; the
enum restores it.
- The four distinct underlying error types (`rustls::Error`,
`NoInitialCipherSuite`, `VerifierBuilderError`, `rcgen::Error`) are
represented as separate variants instead of erased into one —
correctly reflecting that they come from different crates, have
different remediations, and (in the `NoInitialCipherSuite` case) are
not even the same type as `rustls::Error`.
- `#[from]` conversions make the implementation terse (`?` propagates
`io::Error`, `rustls::Error`, `rcgen::Error`,
`VerifierBuilderError`, `NoInitialCipherSuite` directly) while keeping
the category explicit at the variant level.
- `TlsError` lives in `alknet-tls`, owned by the crate that produces it;
core's `EndpointError` is unaffected (and no longer has a `TlsConfig`
variant after ADR-083 anyway).
- `#[non_exhaustive]` lets future variants be added without breaking
downstream matches.
- The ACME boundary is clear: `TlsError::AcmeConfig` is the
config-mismatch guard; ACME state-machine errors are stream events,
logged, not `TlsError` variants.
**Negative:**
- Six variants is more code than a thin `rustls::Error` wrapper. The
trade is matchable context vs. minimal code; for a crate whose entire
purpose is shared TLS config across multiple assembly-layer call sites,
the context wins. The assembly layer is the consumer that benefits
from the distinction.
- The client-side `TlsError` coverage is slightly asymmetrical with the
server side: the server has `QuinnWrap` (quinn-specific) and
`SelfSigned` (rcgen); the client has `VerifierBuild` (webpki). This
reflects the actual asymmetry — the server wraps for quinn and
generates self-signed certs; the client builds a CA verifier. The
shared variants (`CertLoad`, `Rustls`, `AcmeConfig`) cover both. The
asymmetry is the real shape, not a missing piece.
- `rustls::Error`'s `std::error::Error` impl is empty (no `source()`),
so the source chain stops at the variant. The variant name carries the
category; the `rustls::Error`'s `Display` carries the detail. This is
the same as the current code's behavior (the detail is in the
`io::Error::other(e)` string); the enum adds the category on top.
- `NoInitialCipherSuite` has a private field — it cannot be constructed
externally (only received from quinn). This is fine for `#[from]`
(we receive it, we don't construct it), but it means tests cannot
synthesize a `QuinnWrap` variant without a real quinn rejection. The
`for_quinn` failure path is tested via integration, not unit.
## Door type
**One-way.** `TlsError` is the public API surface of `alknet-tls`. Every
assembly-layer call site (`TlsServerConfig::new`, `TlsClientConfig::new`,
`for_quinn`) returns it. Changing the variant set after consumers exist
is a breaking change to every call site's `match`. `#[non_exhaustive]`
makes *adding* variants non-breaking, but *removing* or *renaming*
variants is a breaking change. The one-way-ness is why this ADR exists
before implementation, not after.
The internal implementation (whether `CertLoad` uses `#[from] io::Error`
or an explicit `io::Error` field; whether `AcmeConfig` is a `String` or
a structured enum) is two-way — those are implementation details that
can change without breaking the variant names.
## References
- OQ-63 (resolved by this ADR) — `TlsError` shape
- [ADR-082](082-alknet-tls-extraction.md) — `TlsServerConfig` extraction
(introduces `TlsError` in signatures)
- [ADR-083](083-endpoint-as-accept-loop-runner.md) — endpoint takes no
TLS config; `EndpointError::TlsConfig` is removed, making `TlsError`
the sole TLS error surface
- [ADR-084](084-aws-lc-rs-crypto-provider.md) — `aws_lc_rs` provider
(infallible; no `ProviderInit` variant)
- [ADR-087](087-tlsclientconfig-not-blocked-on-dial.md) —
`TlsClientConfig` (extends `TlsError` to client-side variants)
- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §3 — verifier
selection rule (known peer → fingerprint pin; unknown X.509 → CA
verify; unknown raw-key → fail closed at handshake, not at `new`)
- `crates/alknet-core/src/endpoint.rs` — the current server-side
`io::Error`-wrapping pattern (`TlsConfig(io::Error::other(e))`)
- `crates/alknet-call/src/client/call_client.rs` — the current
client-side `String`-wrapping pattern (`e.to_string()`)
- Dependency-crate sources (read from the cargo cache):
rustls 0.23.41 (`src/error.rs`, `src/builder.rs`, `src/server/builder.rs`,
`src/client/builder.rs`, `src/webpki/server_verifier.rs`,
`src/webpki/anchors.rs`), rustls-pemfile 2.2.0 (`src/lib.rs`,
`src/pemfile.rs`), rcgen 0.13.2 (`src/error.rs`, `src/certificate.rs`,
`src/key.rs`), quinn-proto 0.11.15 (`src/crypto/rustls.rs` —
`NoInitialCipherSuite`, `TryFrom<rustls::ServerConfig>`), rustls-acme
0.12.1 (`src/state.rs`, `src/acme.rs` — `EventError`, `OrderError`,
infallible `AcmeConfig::new`/`state`), rustls-pki-types 1.14.1
(`src/lib.rs`, `src/pem.rs`)