phase 1: architecture spec — overview, server/client, ADR-001..006

- ADR-001: inherit the alknet TLS design as the baseline; deviations
  recorded as alktls ADRs
- ADR-002: TlsError ships the ADR-088 six-variant shape from day one
  (typed #[from] sources; NoqWrap; no string catch-all)
- ADR-003: the QUIC feature is noq (iroh's extracted fork), pre-
  consumer rename; default = [] per the lean-crate convention
  (corrects the extracted code's default = ["quinn"])
- ADR-004: complete accessors — for_tcp_tls() adopted, rustls_config()
  adopted; server accessors borrow (&self), client accessors consume
- ADR-005: identity + credentials + fingerprint types move into
  alktls; auth layer stays out
- ADR-006: eight-module layout; seed tests + integration invariant
  pins (exact nine-scheme list, client enable_early_data)
- specs: overview (transport picture, terminology), server.md (ACME
  lifecycle, invariants), client.md (verifier selection matrix, root-
  store fallback); open-questions.md promotes OQ-TLS-01..08 (all
  resolved at entry)
- Cargo.toml: quinn feature -> noq (per ADR-003); AGENTS.md aligned

Architecture review pass done: 0 critical, 2 major (ADR-002 AcmeConfig
doc comment contradiction; ADR-003 unrecorded default deviation) and
8 minors all addressed; cross-references verified against alknet ADRs,
rustls/noq/iroh sources.

Verified: cargo test, test --all-features, clippy -D warnings,
fmt --check, doc --no-deps
This commit is contained in:
2026-09-10 05:37:55 +00:00
parent e93238cb4a
commit d74a27f764
15 changed files with 1570 additions and 246 deletions
+152
View File
@@ -0,0 +1,152 @@
---
status: draft
last_updated: 2026-09-10
---
# alktls — Server side
`TlsServerConfig` and its resolvers: the server-side TLS setup,
extracted from alknet (`crates/alknet-tls/src/server.rs`) and ported
per the ADRs. Statuses of the decisions referenced here: see
[open-questions.md](open-questions.md) and the ADR index in
[overview.md](overview.md).
## `TlsServerConfig`
The central server-side type. Built once from a `TlsIdentity` + ALPN
list, shared across transports via `Arc` (not `Clone` — it holds the
ACME task's `JoinHandle`).
```rust
pub struct TlsServerConfig {
rustls_config: rustls::ServerConfig, // Clone-safe — Arc internally
acme_handle: Option<tokio::task::JoinHandle<()>>, // acme-gated
}
impl TlsServerConfig {
pub async fn new(identity: &TlsIdentity, alpns: &[Vec<u8>])
-> Result<Self, TlsError>;
#[cfg(feature = "noq")]
pub fn for_noq(&self) -> Result<noq::ServerConfig, TlsError>;
#[cfg(feature = "tcp")]
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor;
pub fn rustls_config(&self) -> &rustls::ServerConfig;
}
```
Construction dispatch (ADR-001's identity model):
- `X509 { cert, key }` — loads the chain + key from disk (`pem.rs`),
`with_single_cert`.
- `RawKey(Ed25519SecretKey)``RawKeyCertResolver` presents the
Ed25519 key as an RFC 7250 raw public key server cert.
- `SelfSigned``generate_self_signed_cert()` (rcgen), in-memory.
- `Acme { domains, cache_dir, directory, contact }` — the ACME path
(below). **Server-only**: on the client path it is a config error.
## Behavior-preservation invariants
These are load-bearing (ADR-001); an implementation that omits any of
them compiles but silently changes TLS behavior. Each is asserted by
test (ADR-006):
- **`max_early_data_size = u32::MAX`** on every server config path —
enables 0-RTT / early data. Omitting it silently breaks 0-RTT
clients.
- **`rustls::crypto::aws_lc_rs::default_provider()`** as the crypto
provider on all paths (alknet ADR-084). Never `ring`, never the
process-default provider, without a new ADR.
- **`AcceptAnyCertVerifier::supported_verify_schemes()`** returns
ED25519 + ECDSA P-256/P-384 + RSA PSS (SHA256/384/512) + RSA PKCS1
(SHA256/384/512) — nine schemes, verbatim, pinned by an exact-list
integration test.
- **`acme-tls/1` ALPN append** for the ACME path only, done by the
crate, not the caller (alknet ADR-027 §7).
- **Non-empty root store** — the client CA path merges `webpki-roots`
when the platform store is empty (see [client.md](client.md)).
## `AcceptAnyCertVerifier`
The server-side client-cert verifier: **request-but-don't-require**.
It asks for a client cert (X.509 or RFC 7250 raw key) so the caller
can extract the fingerprint via `peer_identity()`, but does not
require one and does not verify the presented cert against a CA. The
fingerprint is matched against peer records by the auth layer
(`IdentityProvider::resolve_from_fingerprint`) *outside* this crate —
the TLS crate hands over the fingerprint string; peer resolution is
not a TLS concern (ADR-005).
Server-side only: this must not be reused as a client-side
`ServerCertVerifier` — client-side verification is alknet ADR-034's
selection matrix (see [client.md](client.md)).
## `RawKeyCertResolver`
Presents an `Ed25519SecretKey` as an RFC 7250 raw public key server
certificate: the SPKI DER (Ed25519 OID + 32-byte key) is the "cert",
`only_raw_public_keys() == true`, and the signing key is the shared
`Ed25519SigningKey` helper (`signing.rs`).
## The ACME path
For `TlsIdentity::Acme`, `new` (feature `acme`):
1. Builds `rustls_acme::AcmeConfig` — the upstream builder, distinct
from `TlsError::AcmeConfig` (domains, `DirCache` cache dir,
directory URL from `AcmeDirectory`, contacts).
2. Wires `state.resolver()` as the cert resolver into the server
config.
3. Appends `acme-tls/1` to the ALPN list (TLS-ALPN-01 challenge).
4. Spawns the event-loop task (`tokio::spawn`) matching
`EventOk`/`EventError` variants to `tracing` logs, and returns
**immediately** — it does not await the first certificate.
Lifecycle semantics:
- The returned config is usable right away; handshakes fail
transiently until the first order completes (or a cached cert
deploys).
- Order errors log at `warn!` and retry inside `rustls-acme`; the
task exits only when the event stream ends.
- The task is detached: the stored `JoinHandle` is never aborted;
ACME runs for the process lifetime (OQ-TLS-06 resolved
detached-only; a `shutdown()` surface would be additive later if
the rewrite's graceful-shutdown design wants one).
- **One state machine per domain** — never spawn a second ACME task
for an already-served domain (duplicate orders = Let's Encrypt
rate-limit risk + cert-cache divergence). This is why
`TlsServerConfig` is not `Clone`.
- Runtime errors are stream events, not `TlsError` variants
(ADR-002's scope boundary). `TlsError::AcmeConfig` covers
config-mismatch mistakes ("ACME feature not enabled but `Acme`
configured" — and on the client path, `Acme` used for client
auth), not runtime failures.
## What the server side does NOT do
- No accept loop: `for_tcp_tls()` yields a `TlsAcceptor`; the
`TcpListener::accept``TlsAcceptor::accept` → dispatch loop
belongs to the caller (alknet ADR-083 — the endpoint takes no TLS
config; the assembly layer builds configs and transports).
- No ALPN policy: the ALPN list is a parameter (the assembly layer
filters per endpoint type — alknet ADR-086 §3); the crate appends
only `acme-tls/1` on the ACME path.
- No handshake: verifier selection and handshake outcomes on the
*server* side are `AcceptAnyCertVerifier` + the caller's
fingerprint extraction; a rejected handshake is the transport's
error, not `TlsError`.
- No peer resolution: the extracted fingerprint string goes to the
caller; `PeerEntry`/`AuthPolicy` live in the auth layer.
## References
- [overview.md](overview.md) — the index; [client.md](client.md) —
the client side
- alknet `crates/tls/README.md` §Architecture — the full server-side
spec this doc mirrors
- ADR-001 (invariants), ADR-002 (`TlsError`), ADR-003 (`for_noq`),
ADR-004 (accessors), ADR-005 (identity types), ADR-006 (modules,
tests)