docs(arch): alknet-tls review fixes — dep claim, Clone clarity, dedup, ADR refs

C1: Correct dep-change claim — only rustls-pemfile, rcgen, rustls-acme
leave core; quinn, iroh, ed25519-dalek stay (endpoint struct, accept
loops, and Ed25519SecretKey remain in core).

W1: Trim duplicated 'Why' section in README to summary + ADR-082 link;
keep the three-use-cases table as reference.

W2: Clarify TlsServerConfig is not Clone (holds JoinHandle); share via
Arc, accessors clone the inner rustls::ServerConfig. Fix in README and
ADR-082.

W6: Resolve futures dep inconsistency — acme-gated in both README and ADR.

S1: Behavior-preservation invariants now reference ADR-027 as their
origin.

S2: Document for_quinn fallibility (QuicServerConfig::try_from can fail)
vs for_tcp_tls infallibility (TlsAcceptor::new cannot fail).
This commit is contained in:
glm-5.2 committed 2026-07-13 11:46:58 +00:00
1 parent 192bd0a5a2
commit 837f94f2aa
2 files changed
+59 -74

No files matched your search

+56 -73
View File
@@ -7,9 +7,10 @@ last_updated: 2026-07-13
Shared TLS configuration and certificate management. Builds a
`rustls::ServerConfig` (or an ACME state machine + cert resolver) once,
and hands clones to multiple transports — quinn, `tokio-rustls` (TCP+TLS),
and iroh — so one certificate identity serves QUIC and TCP endpoints
simultaneously. One ACME state machine, one cert, N transports.
and shares it across multiple transports — quinn, `tokio-rustls`
(TCP+TLS), and iroh — so one certificate identity serves QUIC and TCP
endpoints simultaneously. One ACME state machine, one cert, N
transports.
## What
@@ -26,56 +27,50 @@ Let's Encrypt rate-limiting).
```rust
pub struct TlsServerConfig {
config: rustls::ServerConfig, // Clone — Arc internally
acme_handle: Option<JoinHandle<()>>, // one ACME task, shared
config: rustls::ServerConfig, // Clone-safe — Arc internally
acme_handle: Option<JoinHandle<()>>, // one ACME task (see lifecycle below)
}
impl TlsServerConfig {
pub async fn new(identity: &TlsIdentity, alpns: &[Vec<u8>]) -> Result<Self, TlsError>;
/// Clone for quinn. Feature-gated on `quinn`.
/// Produce a quinn server config. Clones the inner rustls config
/// (cheap — Arc-shared cert resolver) and wraps it for quinn.
/// Feature-gated on `quinn`.
#[cfg(feature = "quinn")]
pub fn for_quinn(&self) -> Result<quinn::ServerConfig, TlsError>;
/// Clone for tokio-rustls (TCP+TLS). Feature-gated on `tcp`.
/// Produce a tokio-rustls acceptor for TCP+TLS. Clones the inner
/// rustls config. Feature-gated on `tcp`.
#[cfg(feature = "tcp")]
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor;
/// The underlying rustls config, for any other consumer.
/// Borrow the underlying rustls config, for any other consumer.
pub fn rustls_config(&self) -> &rustls::ServerConfig;
}
```
The key property: `rustls::ServerConfig` is `Clone` (it holds `Arc`s to
the cert resolver and verifier, not the raw key material). So one
`TlsServerConfig` can feed quinn and TCP+TLS simultaneously — one cert,
one ACME state machine, two transports.
`TlsServerConfig` is **not `Clone`** — it holds a `JoinHandle` for the
ACME task, which is not cloneable. Share it via `Arc<TlsServerConfig>`.
Each accessor (`for_quinn`, `for_tcp_tls`) clones the inner
`rustls::ServerConfig`, which is cheap (it holds `Arc`s to the cert
resolver and verifier, not the raw key material). The assembly layer
builds one `TlsServerConfig`, wraps it in `Arc`, and hands `Arc::clone()`
to each transport consumer. One cert, one ACME state machine, N
transports.
## Why
### The cert-reuse problem is concrete
A hub that serves HTTP (TCP+TLS on 443) and channels (QUIC on 4433) with
the same X.509 cert cannot do it with the current code. The
`rustls::ServerConfig` is moved into `quinn::ServerConfig` and consumed.
A TCP+TLS listener would have to build its own `rustls::ServerConfig`
from the same `TlsIdentity` — re-loading the cert file, or re-deriving the
raw key cert, or running a second ACME state machine for the same
domains.
### ACME is the worst case
Two ACME state machines for the same domain:
- Two cert-order attempts (race condition on Let's Encrypt's rate
limiter).
- Two cert caches (divergent cache dirs or a shared dir with no
coordination).
- Two `AcmeState` tasks spawning duplicate `resolver()` instances.
One `TlsServerConfig` with one `AcmeState` task solves this: the ACME
state machine runs once, the `resolver()` is `Arc`-shared across
transports, and both quinn and TCP+TLS get the same cert from the same
order.
`alknet-core` builds the `rustls::ServerConfig` once, then consumes it
into a `quinn::ServerConfig` — making the cert unreusable for a TCP+TLS
listener. For ACME the problem is worse: the `AcmeState` task is spawned
inside the quinn endpoint, so a TCP+TLS listener would need a second ACME
state machine for the same domain (duplicate orders, divergent cert
caches, Let's Encrypt rate-limit risk). The full rationale, including
the cert-reuse problem, the ACME worst case, and the three reasons a
separate crate is the right shape (dependency isolation, ACME weight,
quinn/iroh having their own TLS), is in
[ADR-082](../../decisions/082-alknet-tls-extraction.md).
### The three use cases
@@ -90,25 +85,6 @@ ALPN, the `HandlerRegistry` dispatches by ALPN, the transport is a
parameter. The TLS crate's job is to make the cert available to whichever
transports the deployment runs.
### Why a separate crate
1. **Dependency isolation.** `tokio-rustls` is a real dependency that
shouldn't be forced on every `alknet-core` consumer. A node that only
uses quinn doesn't need `tokio-rustls`. A node that uses TCP+TLS
needs it. Feature-gating in core mixes concerns — core is types and
traits, not transport-specific TLS setup.
2. **ACME is heavy.** `rustls-acme` spawns a long-running async task,
manages a cert cache, talks to Let's Encrypt. That's transport infra,
not core types. It belongs in a crate the assembly layer pulls in when
it needs real TLS setup, not in core.
3. **Quinn and iroh have their own TLS.** Quinn wraps
`rustls::ServerConfig` into `quinn::ServerConfig`. Iroh uses its own
raw-key TLS built into the `Endpoint`. `alknet-tls` is the "I have a
cert and I want to share it across transports" layer — it doesn't
replace quinn's or iroh's TLS, it provides the shared cert source.
## Architecture
### What moves from `alknet-core` to `alknet-tls`
@@ -176,9 +152,12 @@ challenge, ADR-027 §7).
### Behavior-preservation invariants
The extraction must preserve these load-bearing TLS behaviors from the
current code. An implementer who omits any of these produces a crate
that compiles and passes type-checks but silently changes TLS behavior:
The extraction must preserve these load-bearing TLS behaviors. They
originate from [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md),
which established the `TlsIdentity` model, the `Acme` variant, and the
`acme-tls/1` ALPN challenge handling. An implementer who omits any of
these produces a crate that compiles and passes type-checks but silently
changes TLS behavior:
- **`max_early_data_size = u32::MAX`** on all server config paths (X509,
RawKey, SelfSigned, ACME). Enables 0-RTT / early data. Omitting it
@@ -191,10 +170,10 @@ that compiles and passes type-checks but silently changes TLS behavior:
ED25519 + ECDSA P-256/P-384 + RSA PSS/PKCS1 (SHA256/384/512). This
list determines which client cert signature algorithms the server
accepts. Must be preserved verbatim.
- **`acme-tls/1` ALPN append** for the ACME path only. The TLS-ALPN-01
challenge requires the server to advertise `acme-tls/1` in its ALPN
list. Appended in `TlsServerConfig::new`'s ACME branch, not by the
caller.
- **`acme-tls/1` ALPN append** for the ACME path only (ADR-027 §7). The
TLS-ALPN-01 challenge requires the server to advertise `acme-tls/1` in
its ALPN list. Appended in `TlsServerConfig::new`'s ACME branch, not by
the caller.
Transport-specific accessors:
@@ -202,13 +181,17 @@ Transport-specific accessors:
impl TlsServerConfig {
/// Produce a `quinn::ServerConfig` for a QUIC listener. Clones the
/// rustls config (cheap — Arc-shared cert resolver), wraps it in
/// `QuicServerConfig`. Feature-gated on `quinn`.
/// `QuicServerConfig`. Returns `Result` because
/// `QuicServerConfig::try_from(rustls::ServerConfig)` can fail if
/// the rustls config contains quinn-incompatible settings.
/// Feature-gated on `quinn`.
#[cfg(feature = "quinn")]
pub fn for_quinn(&self) -> Result<quinn::ServerConfig, TlsError>;
/// Produce a `tokio_rustls::TlsAcceptor` for a TCP+TLS listener.
/// Clones the rustls config. Feature-gated on `tcp` (pulls
/// `tokio-rustls`).
/// Clones the rustls config. Infallible —
/// `TlsAcceptor::new(rustls::ServerConfig)` cannot fail.
/// Feature-gated on `tcp` (pulls `tokio-rustls`).
#[cfg(feature = "tcp")]
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor;
@@ -265,21 +248,21 @@ alknet-tls
├── ed25519-dalek (Ed25519 signing key — always present, via core)
├── sha2 (fingerprint computation — always present, via core)
├── tokio (spawn for ACME task — always present)
├── futures (StreamExt for ACME event loop — present; only used
│ in the ACME path, can be acme-gated or always present)
├── futures (StreamExt for ACME event loop — acme-gated)
├── tracing (logging)
├── quinn (optional — for_quinn())
├── tokio-rustls (optional — for_tcp_tls())
└── rustls-acme (optional — ACME state machine)
```
`alknet-core` loses the `quinn`, `iroh`, `rustls-pemfile`, `rcgen`,
`ed25519-dalek`, and `rustls-acme` deps from its `[features]`
section — they move to `alknet-tls`. Core keeps a narrow `rustls` /
`rustls-pki-types` dep only if `fingerprint.rs` stays (OQ-59): the
production fingerprint code uses `sha2` + manual DER only, but the test
helper `build_ed25519_spki_der` uses `rustls::sign::public_key_to_spki`.
If `fingerprint.rs` moves to `alknet-tls`, core becomes `rustls`-free.
`alknet-core` loses `rustls-pemfile`, `rcgen`, and `rustls-acme` from
its dependencies — the cert-loading, self-signed generation, and ACME
machinery move to `alknet-tls`. Core keeps `quinn` and `iroh` (the
endpoint struct and accept loops remain in core), `ed25519-dalek`
(`Ed25519SecretKey` stays in `config.rs`), and `rustls` /
`rustls-pki-types` (`fingerprint.rs` uses `rustls::pki_types` in
production and `rustls::sign` in the test helper `build_ed25519_spki_der`
— see OQ-59).
### What `AlknetEndpoint` does after the refactor
@@ -82,7 +82,9 @@ that case, the same `Ed25519SecretKey` feeds both `TlsServerConfig::new`
A new crate `alknet-tls` holds the TLS setup code extracted from
`alknet-core/endpoint.rs`. The central type is `TlsServerConfig`, built
once from a `TlsIdentity` + ALPN list, shared across transports via
cheap clones.
`Arc<TlsServerConfig>`. `TlsServerConfig` is not `Clone` (it holds a
`JoinHandle`); each transport accessor clones the inner
`rustls::ServerConfig`, which is cheap (Arc-shared cert resolver).
```rust
pub struct TlsServerConfig {