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:
1 parent
192bd0a5a2
commit
837f94f2aa
2 files changed
+59
-74
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user