diff --git a/docs/architecture/crates/tls/README.md b/docs/architecture/crates/tls/README.md index 63d34bf..c16407f 100644 --- a/docs/architecture/crates/tls/README.md +++ b/docs/architecture/crates/tls/README.md @@ -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>, // one ACME task, shared + config: rustls::ServerConfig, // Clone-safe — Arc internally + acme_handle: Option>, // one ACME task (see lifecycle below) } impl TlsServerConfig { pub async fn new(identity: &TlsIdentity, alpns: &[Vec]) -> Result; - /// 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; - /// 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`. +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; /// 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 diff --git a/docs/architecture/decisions/082-alknet-tls-extraction.md b/docs/architecture/decisions/082-alknet-tls-extraction.md index cea5dd2..ba309b6 100644 --- a/docs/architecture/decisions/082-alknet-tls-extraction.md +++ b/docs/architecture/decisions/082-alknet-tls-extraction.md @@ -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` 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 {