--- id: port-server name: Port server side — TlsServerConfig, resolvers, ACME path (src/server.rs) status: completed depends_on: [port-identity-types, port-pem-signing] scope: broad risk: medium impact: phase level: implementation tags: [server, acme, port, invariants] --- ## Description Port `src/server.rs` from alknet-tls per the ADRs: `TlsServerConfig`, `build_rustls_server_config`, `RawKeyCertResolver`, `AcceptAnyCertVerifier`, `SelfSignedCert` / `generate_self_signed_cert`, and the ACME branch. This is the biggest port task; the deltas against the extraction source are all ADR-pinned, everything else is a port. ### The API deltas (from the extracted code) - Accessors borrow: `for_noq(&self)` (ADR-004), `for_tcp_tls(&self)` (adopted — did not exist in the extraction), `rustls_config(&self)` (adopted — the field was `pub(crate)`; now the re-exported accessor). - `for_noq` replaces `for_quinn`: `noq::ServerConfig::with_crypto(Arc::new( noq::crypto::rustls::QuicServerConfig::try_from(inner)?))`; the failure maps to `TlsError::NoqWrap` (ADR-002/003). - `build_rustls_server_config`'s `TlsIdentity::Acme` arm: the extracted `unreachable!` becomes a `TlsError::AcmeConfig` return (ADR-006; no panics in library code). The internal dispatch invariant (Acme is handled by `new_acme`) is preserved — the arm is defensive. - Field name: `acme_handle` (ADR-006 unified on the alknet-spec name). ### The behavior-preservation invariants (all must be test-pinned here) - `max_early_data_size = u32::MAX` on every path (X509, RawKey, SelfSigned, ACME branch). - `aws_lc_rs::default_provider()` on every path (constructed via `builder_with_provider`; no process-default fallback). - `AcceptAnyCertVerifier::supported_verify_schemes()` returns the nine schemes verbatim (ED25519; ECDSA P-256/P-384; RSA PSS 256/384/512; RSA PKCS1 256/384/512). - `acme-tls/1` appended by the crate on the ACME path only. - Verifier behavior: `offer_client_auth() == true`, `client_auth_mandatory() == false`, empty `root_hint_subjects`, accepts any client cert. - `RawKeyCertResolver`: `only_raw_public_keys() == true`, resolves the SPKI-backed `CertifiedKey`. ### ACME path (feature `acme`) Port `new_acme` per server.md: `rustls_acme::AcmeConfig` + `DirCache` + directory URL + contacts; `state.resolver()` wired in; `acme-tls/1` appended; the event-loop task spawned (`EventOk`/`EventError` matched to `tracing` logs — port the extracted log lines); returns immediately. Handle stored in `acme_handle`, never aborted (detached; OQ-TLS-06). Without the feature, `Acme` identities return `TlsError::AcmeConfig`. ## Work 1. Port the module; apply the deltas above; rewire imports to this crate's types (identity, signing, pem, TlsError). 2. Port the extracted in-module tests (they assert most invariants). 3. Add the exact nine-scheme list pin (the extracted test only checks two schemes' membership). 4. Feature-combo verification (below). ## Verification - [x] Invariant pins green: `max_early_data_size` per path, nine-scheme exact list, resolver behavior, verifier behavior - [x] ACME branch (with `--features acme`): spawns, returns immediately, appends `acme-tls/1` (test with a staging URL + tempdir cache; do NOT hit Let's Encrypt — construct and assert config state, assert the ALPN list) - [x] `Acme` identity without the `acme` feature → `TlsError::AcmeConfig` - [x] `for_noq` / `for_tcp_tls` round-trip per identity variant (construction-level; handshakes are out of scope) - [x] `cargo test` (default), `cargo test --all-features`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` ## Acceptance Criteria - [x] All five server invariants are test-asserted (not just compiled) - [x] `for_noq`, `for_tcp_tls`, `rustls_config` exist with ADR-004 signatures (`&self`; `for_tcp_tls` infallible) - [x] No `unreachable!`/panics in library code - [x] `lib.rs` re-exports the server surface ## References - docs/architecture/server.md (the normative doc) - docs/architecture/decisions/002-tlserror-shape.md, 003, 004 - Prior art: `/workspace/@alkdev/alknet/crates/alknet-tls/src/server.rs` (port source; note every delta above) ## Notes - **The extracted `unreachable!` is now the defensive arm**: the Acme arm in `build_rustls_server_config` returns `TlsError::AcmeConfig("TlsIdentity::Acme is handled by TlsServerConfig::new_acme, not build_rustls_server_config")` — test-asserted (`build_rustls_server_config_acme_returns_config_error`). - **Error mapping**: every extraction `TlsError::Config(e.to_string())` site at a `rustls::Error` producer (`with_safe_default_protocol_versions`, `with_single_cert`) became `?` into `TlsError::Rustls` (`#[from]`); rcgen sites became `?` into `TlsError::SelfSigned` (`#[from] rcgen::Error`). The non-acme `TlsServerConfig::new` Acme arm keeps its message verbatim but under `AcmeConfig`. - **The ACME test uses a `Custom("http://127.0.0.1:9/directory")` directory rather than the staging URL** — never contacted either way (port 9 discard; no network I/O beyond rustls-acme's async task start); the task's "staging URL" intent (do-not-hit-Let's-Encrypt) is satisfied with a stronger guarantee: a blackhole address. Staging would risk a real contact if a test misfired; `127.0.0.1:9` cannot succeed. - **`for_noq(&self)` clones the inner config** (`try_from` consumes) — the ADR-004 borrow shape requires it; the clone is cheap (Arc-shared resolvers). - **Cargo.toml deltas (two, both required)**: 1. `noq` gains `"aws-lc-rs"` in its feature list. ADR-003's TOML block (`features = ["rustls"]`) was written against noq 1.2's API, but the lockfile resolves `noq-proto 1.3.0`, where `ServerConfig::with_crypto(crypto)` (the single-arg constructor `for_noq` calls) is `#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]` — the retry-token key comes from noq's `ring_like` module, which needs one of the two provider features. `"aws-lc-rs"` (not `"ring"`) matches the crate's provider posture; it does not fight ADR-084: the config's internal provider is still the crate's explicit `aws_lc_rs::default_provider()` (noq consumes it from the config — ADR-003's provider paragraph). One new lockfile line (`aws-lc-rs` under noq-proto). ADR-003 needs a one-line amendment (recorded for review-impl docs sync). 2. `acme = ["dep:rustls-acme", "dep:futures"]` — ADR-006 §Feature gates already records this ("the `acme` feature gates ... the spawned task + the `futures` dep"); the scaffold's TOML omitted the `futures` half. Without it, `--features acme` alone fails to compile (the spawned task uses `futures::StreamExt`). ## Summary Ported `src/server.rs` (614 lines) from alknet-tls server.rs with all five ADR-pinned deltas applied: `for_noq(&self)` (borrow + clone + `NoqWrap` via `#[from]`), `for_tcp_tls(&self)` adopted (infallible TlsAcceptor), `rustls_config(&self)` adopted, `acme_handle` renamed, Acme arm → `TlsError::AcmeConfig` (no `unreachable!`). All error sites remapped to the typed variants (`Rustls`/`SelfSigned`/`AcmeConfig`). The ACME path is verbatim (DirCache + directory URL + contacts + resolver + `acme-tls/1` + spawned event loop with the extracted tracing lines, returns immediately, handle never aborted). 14 tests: 12 ported (with the Acme-unreachable → AcmeConfig and for_quinn → for_noq rewires; the acme_directory URL tests live in identity.rs now) + the nine-scheme exact-list pin + ACME spawn/return/ALPN assertion + no-feature AcmeConfig test + `for_tcp_tls`/`rustls_config` round-trips for X509 (rcgen PEM pair in tempdir)/RawKey/SelfSigned. Verification: `cargo test` 68 pass, `--all-features` 75 pass, `--features noq` 72 pass, clippy -D warnings (default + all-features), `fmt --check`, feature checks (tcp/acme) — all green.