port-server (src/server.rs, 614 lines): - TlsServerConfig with ADR-004 accessors: for_noq(&self) (borrow + inner-clone + NoqWrap via #[from]), for_tcp_tls(&self) adopted (infallible TlsAcceptor), rustls_config(&self) adopted - acme_handle renamed per ADR-006; the defensive Acme arm returns TlsError::AcmeConfig (unreachable! eliminated) - error sites remapped to typed variants (Rustls/SelfSigned/AcmeConfig) - ACME path verbatim: DirCache + directory + contacts + resolver + acme-tls/1 + spawned event loop (tracing lines ported), returns immediately, detached handle - 14 tests incl. the nine-scheme exact-list pin, ACME spawn/return/ALPN assertion (blackhole URL — no network I/O), no-feature AcmeConfig, for_tcp_tls/rustls_config round-trips port-client (src/credentials.rs + src/client.rs): - ConnectionCredentials/RemoteIdentity wholesale with load-bearing Option-semantics docs (None = public-X.509 state, Some = pin) - verifier selection matrix + client-auth presentation matrix test-pinned at unit level; enable_early_data=true pinned; root-store fallback asserted non-empty - FingerprintPinVerifier: pin match/mismatch + raw-key signature routing (verify_tls13_signature_with_raw_key) asserted - resolvers/verifier made pub for the re-export block Cargo.toml (two required deltas): - noq feature gains aws-lc-rs: lockfile resolves noq-proto 1.3.0 where ServerConfig::with_crypto is #[cfg(any(aws-lc-rs, ring))]; ADR-003's TOML block was written against the 1.2 API. Amendment note recorded in tasks/port-server.md for the review-impl sync - acme = [dep:rustls-acme, dep:futures] — ADR-006 already gates the futures dep on acme; the scaffold omitted it lib.rs: re-export block complete (all eight modules) Verification: cargo test 68, --all-features 75, --features noq 72, clippy -D warnings, fmt --check, doc --no-deps (0 warnings) — green
162 lines
7.8 KiB
Markdown
162 lines
7.8 KiB
Markdown
---
|
|
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. |