--- status: draft last_updated: 2026-09-09 --- # alktls — Phase 0 Research Findings This document captures Phase 0 (Exploration) findings and open design questions for the `alktls` crate. The objective of Phase 0 per `docs/sdd_process.md` is: *"Capture vision and guiding principles; research options; validate approaches; converge on a recommended approach."* It is the input to Phase 1 (Architecture), where the Architect will produce `docs/architecture/` specs, ADRs, and open questions. Drafted 2026-09-09. The crate is the TLS layer of the alk* stack: shared TLS setup types — server and client `rustls` configs, cert resolvers, verifiers, and ACME state-machine wiring — transport-agnostic and shareable across transports (noq QUIC below, `tokio-rustls` TCP+TLS, and anything else that consumes a `rustls` config). The 2026-09-09 same-day revision adds the noq investigation (§Prior art: noq and the quinn→noq shift): the alknet rewrite will move QUIC from quinn to noq (iroh's extracted internal quinn fork, published as `noq 1.2.0`, with iroh 1.1.0 built on it), which adds a `for_noq()` accessor question (OQ-TLS-08) and an MSRV-floor decision. The transport picture for the rewrite is three paths — TCP+TLS, QUIC+TLS, and iroh (relay-assisted p2p QUIC) — sharing one rustls 0.23 + aws-lc-rs stack. Unlike alktunnels (which was entirely new protocol code), this crate is an **extraction** of working, battle-tested TLS code from alknet. The prior art is unusually strong: `crates/alknet-tls` exists and runs in production today, and the alknet architecture docs (ADR-082/083/084/086/087/088/089 plus the `crates/tls` README spec) already record most of the design rationale. Phase 0's job is therefore different in character from alktunnels': mostly **inventory and gap analysis** — what already works, what the spec pinned but the code never implemented, what must change when the config types move from `alknet-core` into this crate, and what the alknet rewrite needs this crate to own. Expect this document to be shorter on open questions than alktunnels' Phase 0 was. ## What is already settled The design is ADR-pinned in alknet; this crate inherits it. It does not re-litigate these decisions — it implements them. The summaries below are pointers; read the referenced ADRs for the full rationale. - **The extraction shape (alknet ADR-082).** TLS setup is a standalone crate because a `rustls::ServerConfig` built for one transport gets *consumed* into that transport's wrapper (e.g. `quinn::ServerConfig`), making the cert unreusable for a TCP+TLS listener. The fix: build the config once (`TlsServerConfig`), share it via `Arc`, clone the cheap inner rustls config per transport. One cert, one ACME state machine, N transports. - **The cert-reuse problem and the ACME worst case (ADR-082 §The cert-reuse problem).** Two ACME state machines for the same domain = duplicate Let's Encrypt orders (rate-limit risk), divergent cert caches, duplicate resolvers. One ACME state machine shared across transports is the only correct design; `TlsServerConfig` is not `Clone` (it holds the ACME task's `JoinHandle`) precisely so callers must share via `Arc`. - **The endpoint takes no TLS config (alknet ADR-083).** A hub serving native clients (raw key) and browsers (X.509/ACME) holds *two* `TlsServerConfig`s — there is no single "the TLS config" for an endpoint to take. The assembly layer builds the configs and the transports; the endpoint just dispatches. This crate is the cert provider, not the accept loop. - **aws-lc-rs as the crypto provider (alknet ADR-084).** `rustls::crypto::aws_lc_rs::default_provider()` on all server and client config paths. Matches iroh's `tls-aws-lc-rs` feature; FIPS- capable. Do not switch to `ring` or the process-default provider without a new ADR. - **The identity model (alknet ADR-027).** `TlsIdentity` is a four- variant enum: `X509 { cert, key }` (paths, loaded from disk), `RawKey(Ed25519SecretKey)` (RFC 7250 raw public key), `SelfSigned` (dev), and `Acme { domains, cache_dir, directory, contact }` (server-only). `Acme` on the client path is a config error. - **`TlsClientConfig` is a first-class deliverable (alknet ADR-087).** Not deferred behind the dial-seam extraction: the TLS config is a *prerequisite* for the dial, not a consequence of it. Centralizes ADR-034 verifier selection and ADR-084 provider wiring for all outbound dials. - **Verifier selection (alknet ADR-034 §3).** Exactly three outcomes, driven by `ConnectionCredentials.remote_identity`: `Some(fingerprint)` → known peer → `FingerprintPinVerifier` (the fingerprint IS the trust anchor; handshake signatures still verified so a stolen fingerprint can't be replayed with a forged signature); `None` + X.509 → CA verification (`WebPkiServerVerifier`); `None` + raw key → fail closed at handshake (raw-key remotes are always known peers — there is no CA to fall back to). `None` is the public-X.509-endpoint state, **not** "skip verification." - **The `TlsError` shape (alknet ADR-088).** Single `#[non_exhaustive]` enum, one variant per failure category: `CertLoad`, `SelfSigned`, `Rustls`, `VerifierBuild`, `QuinnWrap` (quinn-gated), `AcmeConfig`. Scope boundary (§6): config-construction errors only — handshake-time outcomes flow through the transport's connector, ACME runtime errors are stream events logged in the spawned task. - **Root-store fallback (alknet ADR-088 §5).** The unknown-X.509-remote CA path loads the platform's native root certs; if that store is empty (containerized deployment, no system CA bundle), `webpki-roots` is merged in so the store is never empty. Native-certs *load* errors are logged, not returned. - **Split ALPN lists per endpoint type (alknet ADR-086 §3).** The native config advertises native ALPNs (`alknet/channels`, `alknet/call`); the web config advertises entry-point ALPNs (`h2`, `http/1.1`) + `alknet/channels`; `acme-tls/1` is appended automatically for the ACME path. The assembly layer filters per config — this crate just takes the list. - **The dial seam is elsewhere (alknet ADR-089).** `AlknetClient` (transport-polymorphic dial) consumes `TlsClientConfig` via `for_quinn()` / `into_rustls_config()`; this crate does not dial. The iroh transport is the key-not-config exception on both sides — iroh has its own TLS and takes an `iroh::SecretKey`; there is no `for_iroh()` here. **Updated 2026-09-09:** the noq investigation (§Prior art: noq and the quinn→noq shift) adds a same-shaped `for_noq()` accessor next to `for_quinn()` and confirms the iroh exception still holds on iroh 1.1/noq 1.2. - **Fingerprint normalization (alknet ADR-030 §6).** `ed25519:` for RFC 7250 raw keys, `SHA256:` for cert DER — identical across quinn, iroh, and TCP+TLS paths, so a `PeerEntry.fingerprints` entry matches regardless of transport. - **`ConnectionCredentials` (alknet ADR-091).** The dial's credential bundle: `local_identity: Option` (client-auth presentation) + `remote_identity: Option` (verifier selection). Both `Option`s are load-bearing, not cosmetic — see the verifier-selection bullet above. ## Prior art: the extracted code (`crates/alknet-tls`) The working implementation lives at `/workspace/@alkdev/alknet/crates/alknet-tls/` (~1.2k lines across four modules + tests). Module inventory, current state, and known deltas against the ADR-088 spec: | Module | Contents | State | |--------|----------|-------| | `lib.rs` | `TlsError` — simplified 3-variant enum (`Config(String)`, `Io`, `Cert(String)`) | **Diverges from ADR-088** — see the gaps section | | `server.rs` | `TlsServerConfig` (`new`, `new_acme`, `for_quinn`), `build_rustls_server_config`, `RawKeyCertResolver`, `AcceptAnyCertVerifier`, `SelfSignedCert`/`generate_self_signed_cert` | Working; all behavior-preservation invariants present (verified below) | | `client.rs` | `TlsClientConfig` (`new`, `for_quinn`, `into_rustls_config`), `select_server_verifier`, `build_client_auth`, `RawKeyClientCertResolver`, `NoClientCertResolver`, `FingerprintPinVerifier`, `load_platform_root_cert_store` | Working; verifier selection and root-store fallback match the spec | | `pem.rs` | `load_cert_chain`, `load_private_key` | Working; shared by server + client | | `signing.rs` | `Ed25519SigningKey` (rustls `SigningKey` + `Signer` over `ed25519_dalek`) | Working; one copy shared by both sides | Consumers in alknet today: `alknet-client` (the QUIC and TCP+TLS dials build a `TlsClientConfig` per dial), `alknet-endpoint` (TCP+TLS accept loop takes a `TlsAcceptor`), and the iroh dial (error-type reuse only). Server-side, the assembly wiring consumes `TlsServerConfig` and its `for_quinn()`; the spec's `for_tcp_tls()` accessor is **not implemented** in the extracted code — the TCP+TLS accept path currently receives a `TlsAcceptor` built elsewhere (the caller wraps `tokio_rustls::TlsAcceptor::from(Arc)` itself). That accessor belongs in this crate (spec §Architecture, ADR-082's API table) — see the gaps section. ### Behavior-preservation invariants — verified present in the code All five load-bearing invariants (ADR-082 §Behavior-preservation invariants, ADR-088 §5) are confirmed in the extracted source: - `config.max_early_data_size = u32::MAX` on every server config path — X509 (`server.rs:168`), RawKey (`server.rs:179`), SelfSigned (`server.rs:191`), and the ACME branch (`server.rs:83`). The client sets `config.enable_early_data = true` (`client.rs:36`). - `rustls::crypto::aws_lc_rs::default_provider()` on all paths — server (`server.rs:77`, `server.rs:155`) and client (`client.rs:26`). - `AcceptAnyCertVerifier::supported_verify_schemes()` returns ED25519 + ECDSA P-256/P-384 + RSA PSS (SHA256/384/512) + RSA PKCS1 (SHA256/384/512) — nine schemes verbatim (`server.rs:284`). - `acme-tls/1` ALPN appended inside `new_acme` (`server.rs:86`) — the crate appends it, not the caller; only on the ACME path. - Non-empty root store — `load_platform_root_cert_store()` merges `webpki-roots` when the platform store is empty (`client.rs:128-149`); native-certs load errors are logged, not returned. Phase 1's port must carry these over unchanged, and the port's test suite should assert them directly (the extracted code's tests already do: `max_early_data_size` assertions, the nine-scheme list, the empty-store fallback). ### Implementation notes worth carrying forward Details discovered in the code that the spec glosses over but any implementation will trip on: - **The ACME path is `rustls-acme`'s event stream**, not a bespoke state machine: `AcmeConfig::new(domains).cache(DirCache).directory(url).contact(...)` → `state.resolver()` wired into the server config → `tokio::spawn`ed loop over `state.next()` matching `EventOk`/`EventError` variants to `tracing` log lines. `new` spawns and returns immediately (does not await the first cert) — handshakes fail transiently until the first order completes. Spec-consistent. - **`new_acme` swallows the resolver's cert absence by design.** The spawned task logs `Order` errors at `warn!` ("will retry") — ACME retry semantics live inside `rustls-acme`; the task exits only when the stream ends. - **`for_quinn()` consumes `self`, not `&self`** in the extracted code (both server and client). The spec sketches `&self` for the server accessor. Minor API question for Phase 1 (see OQ-TLS-04) — consuming `self` is safe for the one-config-per-endpoint-type assembly pattern but prevents building two transports from one `TlsServerConfig` without going through `rustls_config()` manually. - **`FingerprintPinVerifier` verifies handshake signatures.** It is not a "trust any cert with the right fingerprint" bypass: TLS 1.2/1.3 signature verification still runs (`verify_tls12_signature` / `verify_tls13_signature`), routing Ed25519 SPKI certs through `verify_tls13_signature_with_raw_key` and everything else through the standard aws-lc-rs-backed verification. A pinned fingerprint without the private key fails the handshake. - **Client-auth resolver selection is identity-driven.** `RawKeyClientCertResolver` auto-detects the raw-key case from the cert DER (an Ed25519 SPKI ⇒ `only_raw_public_keys() == true`), the same resolver type serves X.509 chains (raw-keys flag false), `SelfSigned`/`None` present nothing (`NoClientCertResolver`), and `Acme` is rejected at config time. - **The 3-variant `TlsError` folds typed sources into strings.** Every `map_err(|e| TlsError::Config(e.to_string()))` in the extracted code is a place where ADR-088's typed variants (`Rustls`, `SelfSigned`, `VerifierBuild`, `QuinnWrap`) would preserve the `#[source]` chain. This is the single largest code delta against the spec. - **Tests are in-module `#[cfg(test)]` blocks** covering resolvers, verifier behavior, PEM error paths, signing, and the invariants — worth porting as the seed of this crate's `tests/`, plus integration tests here (a full-crate crate has no workspace to lean on for cross-crate coverage). ## Gaps: spec-pinned but never implemented These are recorded decisions without code, or code diverging from the recorded decision. Phase 1 must close or explicitly re-decide each. 1. **`TlsError` is the 3-variant simplified enum, not the ADR-088 shape.** The six-variant `#[non_exhaustive]` shape (`CertLoad`, `SelfSigned`, `Rustls`, `VerifierBuild`, `QuinnWrap`, `AcmeConfig`) with `#[from]` sources was specced in alknet ADR-088 and never implemented. The ADR-088 README notes the rewrite is a two-way-door change (no external match arms yet). This crate should ship the ADR-088 shape from day one — it is the single highest-value gap closure. Caveat: `CertLoad`'s `#[from] io::Error` relies on `rustls_pemfile` funnelling its non-`std::error::Error` error type into `io::Error` (ADR-088 §Gotchas #2) — verify that holds at the pinned rustls/pemfile versions. 2. **`for_tcp_tls()` does not exist.** Both ADR-082's API table and the `crates/tls` README spec `for_tcp_tls() -> tokio_rustls::TlsAcceptor` (feature-gated on `tcp`); the extracted code never added it — the assembly layer wraps the acceptor itself. Decide in Phase 1 whether alktls owns the accessor (spec-conforming) or leaves acceptor wrapping to callers (the current de facto shape). Leaning: own it — it is infallible, tiny, and the spec is unambiguous. 3. **The config types still live in `alknet-core`.** `TlsIdentity`, `Ed25519SecretKey` (`config.rs`), `ConnectionCredentials`/ `RemoteIdentity` (`credentials.rs`), and `fingerprint.rs` are all in core. This crate is expected to own them after the rewrite (AGENTS.md convention 10) — but they are *config* types that `StaticConfig` and the vault integration also consume, so the ownership boundary needs a deliberate decision, not a mechanical move (OQ-TLS-01). The fingerprint helpers (SHA-256 + manual DER parsing, no rustls dep in production) are a candidate to move wholesale; `PeerEntry`/`AuthPolicy` are not TLS concerns and stay wherever identity resolution lands. 4. **`SelfSigned` is server-only in practice.** The extracted client path maps `Some(TlsIdentity::SelfSigned)` to `NoClientCertResolver` (present nothing) — reasonable, but the identity enum doesn't encode the constraint the way `Acme` is encoded (client-path config error). Minor semantic wobble to resolve when the types move (OQ-TLS-02). 5. **`tokio` features.** The extracted crate uses `features = ["full"]` (workspace style); this crate pins the lean subset (`rt`, `sync`, `macros`) per AGENTS.md convention 3. Verify `rustls-acme`'s transitive tokio needs don't force a wider set (it brings its own features regardless — the constraint is only on *this* crate's direct dep declaration). ## What the alknet rewrite needs from this crate Context for scoping: alktls is one of the last extractions before the alknet rewrite begins. The rewrite's consumers (new endpoint, new client, assembly layer) will consume this crate instead of `alknet-tls`. Derived requirements: - **The public API surface should be the spec's surface**: `new`, `for_quinn`, `for_tcp_tls` (gap #2), `rustls_config` on the server; `new`, `for_quinn`, `into_rustls_config` on the client; `TlsError` in the ADR-088 shape (gap #1). The rewrite is the consumer cutoff — get the surface right *before* it exists. - **No alknet imports.** The crate must compile without `alknet-core`; everything it needs (identity, credentials, fingerprint) either moves in or is re-decided (OQ-TLS-01). This is the structural difference from `alknet-tls`, which imports `alknet-core` throughout. - **Noalknet-net assumptions.** The extracted code is transport-agnostic already (the whole point of ADR-082), so no de-welding work is expected — but the rewrite should confirm nothing in the ACME path or PEM loading reaches for alknet types. - **ALPN naming is the caller's problem.** `alknet/channels`, `alknet/call`, `h2`, `http/1.1` are strings passed in by the assembly layer (ADR-086 §3 split lists); this crate owns only `acme-tls/1` (append-for-ACME). The rewrite's ALPN renames (`alknet/` → `alk/` or otherwise) do not touch this crate. - **Iroh stays key-not-config.** The rewrite's iroh transport reads the Ed25519 secret key directly; `for_iroh()` must not appear here (ADR-082 §Iroh is different). ## Version and dependency posture The extracted crate pins: `rustls 0.23` (aws_lc_rs feature), `quinn 0.11`, `tokio-rustls 0.26`, `rustls-acme 0.12` (aws-lc-rs feature), `rustls-native-certs 0.8`, `webpki-roots 0.26`, `rcgen 0.13`, `ed25519-dalek 2`, `thiserror 2`, `tokio 1`. As of 2026-09 the rustls 0.23 line is still current (0.23.44 stable; iroh and noq pin `rustls 0.23.33` — same 0.23 major, so one unified 0.23 tree across quinn/noq/iroh paths is achievable). Phase 1 should re-verify at packaging time and record any bump as a line in the dependency ADR. Dependency shape per the spec: `rustls` always present (the core library); `quinn`/`tcp`/`acme` feature-gated; `rustls-native-certs` + `webpki-roots` always present (the CA path is needed by any client dialing public X.509 endpoints regardless of transport); `futures` acme-gated (the event loop's `StreamExt`). **MSRV verification (2026-09-09).** The sibling crates declared `rust-version = "1.85"`, but that claim was never compile-checked against the TLS dependency tree: with default resolution, `rcgen → yasna/time` pulls `time 0.3.5x` which requires rustc 1.88, and both this crate's fresh lock *and alknet's own `alknet-tls`* fail `rustup run 1.85 cargo check` for exactly that reason. Interim resolution: pin `time` to `0.3.36` and verify under the real 1.85 toolchain. **RESOLVED 2026-09-10 — ecosystem floor raised to 1.88.** The finding was audited across the whole ecosystem (3 published + 1 unpublished crate, dedicated audit sessions): the 1.85 claim failed in all of them; every crate has now adopted `rust-version = "1.88"` with the lock re-floated. alktls follows: `rust-version = "1.88"`, `time` pin dropped (it existed only to keep the 1.85 claim satisfiable), `rustup run 1.88 cargo check` verified. The lesson recorded for the ecosystem: `rust-version` claims are passive metadata — nothing fails at build time on modern toolchains, so the claim must be *compile-checked* against the resolved tree, not assumed; a committed `Cargo.lock` is what makes an MSRV claim reproducible either way. **MSRV pressure from the noq/iroh direction — subsumed.** noq 1.2 declares rust-version 1.88 and iroh 1.1 declares 1.91 (both edition 2024). The 2026-09-10 ecosystem floor of 1.88 (above) matches noq's exactly and sits below iroh's, so the noq/iroh MSRV pressure that motivated the Phase 1 lean is already absorbed — what remains of OQ-TLS-08 is only the feature-rename half (`quinn` → `noq`). ## Prior art: noq and the quinn→noq shift (2026-09-09) The alknet rewrite will move QUIC from `quinn` to `noq` — iroh's extracted internal quinn fork, now a standalone published project. Facts verified against `/workspace/noq` and `/workspace/iroh` (both clean clones of upstream): - **noq is published and current**: crates.io `noq 1.2.0` (Feb 2026 created, Aug 2026 updated, ~1.6M downloads; `noq-proto`/`noq-udp` siblings). Workspace layout mirrors quinn's (`noq` async facade + sans-io `noq-proto`). rust-version **1.88**, edition 2024. - **iroh 1.1.0 (crates.io, Aug 2026) is built on noq 1.2.0** and re-exports `noq::*`, `noq_proto::*`, `noq_udp::*` as part of its public API. The QUIC + iroh transports in the rewrite will share one QUIC implementation by construction. - **The TLS seam is API-compatible with quinn 0.11** — this is the load-bearing fact for this crate: `noq_proto::crypto::rustls::QuicServerConfig::try_from( rustls::ServerConfig)` / `QuicClientConfig::try_from( rustls::ClientConfig)`, failing with `noq_proto::crypto::rustls::NoInitialCipherSuite` — the same constructor shape and the same failure type ADR-088's `QuinnWrap` variant wraps. Porting `for_quinn()` → `for_noq()` is mechanical (type renames in one module; no config-construction change). - **One rustls, one provider, both QUIC paths.** iroh 1.1 depends on `rustls 0.23.33` (default-features off) and noq 1.2 on `rustls ^0.23.33` — the same rustls 0.23 line this crate pins. The `tls-aws-lc-rs` / `tls-ring` feature split maps onto noq's `aws-lc-rs` / `ring` features; our aws-lc-rs-only posture (ADR-084) composes cleanly (`noq` feature `rustls-aws-lc-rs`). - **The iroh key surface is unchanged for us.** `iroh_base::SecretKey` is still 32 raw Ed25519 bytes (`from_bytes`/`to_bytes`), the key-not-config exception still holds (no `for_iroh()`, no rustls config handed to iroh), and the fingerprint normalization story (`ed25519:`) is unaffected. Implications for this crate: - **The `quinn` feature becomes (or is joined by) a `noq` feature.** The transport-gate story stays the same — feature-gated wrapping — but the accessor name and dep change. Phase 1 decides: rename `for_quinn` → `for_noq` outright (clean, pre-consumer window) or carry both features during a transition (alknet's rewrite is the only consumer; there is nothing to transition with — leaning one accessor, see OQ-TLS-08). - **`QuinnWrap`'s error source is the same shape under a new name.** ADR-088's variant wraps `NoInitialCipherSuite`; under noq it wraps `noq_proto::crypto::rustls::NoInitialCipherSuite` — same type name, different crate path. The ADR-088 shape survives the rename. - **Consolidation win for ADR-084.** With quinn and iroh on one QUIC stack (noq) and one rustls version (0.23), the "one provider, all paths" invariant gets *stronger* — there is no longer a risk of quinn's ring-default and iroh's aws-lc-rs-default diverging inside one binary. - **tokio-rustls (the `tcp` feature) is unaffected** — noq does not touch the TCP+TLS path. ## Prior art: the three-transport picture The transport story the rewrite assembles: **TCP+TLS** (tokio-rustls, this crate's `tcp` feature), **QUIC+TLS** (noq, this crate's quic accessor), and **iroh** (relay-assisted p2p QUIC — iroh's own TLS inside, fed the Ed25519 key). A hub/endpoint may have public ips/ports or not; iroh is the "or not" half. For this crate the consequences are: two `TlsServerConfig`s per endpoint-type split (ADR-086 §3) unchanged; the quic accessor feeds either a bare noq endpoint or (via iroh's own builder) nothing at all; and the identity type must keep `iroh_base::SecretKey`-shaped byte access (OQ-TLS-07). ## Open Questions Numbered OQ-TLS-01.. so they can be promoted into `docs/architecture/open-questions.md` in Phase 1. Most are small — this is the gap list, not a research agenda. ### OQ-TLS-01: Where do the config types live, and what moves? `TlsIdentity`, `Ed25519SecretKey`, `ConnectionCredentials`, `RemoteIdentity`, and the fingerprint helpers live in `alknet-core` today. AGENTS.md convention 10 says this crate owns them after the rewrite — but `StaticConfig` (core's config struct) holds a `TlsIdentity`, `PeerEntry` fingerprint resolution is auth-layer (not TLS), and the vault derives the local identity at startup. Options: - **A: move all five into alktls** — `StaticConfig` and the vault depend on alktls for the types. alknet-core grows a dep on alktls (or the rewrite's equivalent of core does). Cleanest ownership; couples core config to the TLS crate. - **B: move identity + fingerprint, keep credentials in core** — `ConnectionCredentials` is a dial-layer bundle (ADR-091); it may belong to the rewrite's dial seam instead. - **C: keep types in core-equivalent, alktls re-exports** — the `alknet-tls` shape exactly; minimal change but leaves the ownership question open forever. Considerations: the rewrite is the window to decide this — after consumers exist, moving types is a breaking change. The fingerprint helpers are pure (sha2 + manual DER, no rustls dep) so they can live anywhere; `Ed25519SigningKey` (signing.rs) needs whichever type `Ed25519SecretKey` lands in. **Status:** open — lean A (full move) for the identity types + fingerprint, with `ConnectionCredentials` decided alongside the rewrite's dial-seam design (B). Needs the rewrite's core-shape decision to finalize; do not let it block the rest of Phase 1. ### OQ-TLS-02: `SelfSigned` on the client path — encode or document? The extracted code maps `Some(SelfSigned)` client-auth to "present nothing" (`NoClientCertResolver`). `Acme` is a client-path config *error* (encoded), but `SelfSigned`'s client-path meaning is implicit. Options: encode it (`SelfSigned` → config error on client, like Acme), keep present-nothing (documented), or make the dev path present the self-signed cert. Present-nothing matches current behavior and needs no code; encoding the constraint is a type-level nicety. **Status:** open, small — lean "keep behavior, document it" unless Phase 1 wants the type-level enforcement. ### OQ-TLS-03: `for_tcp_tls()` — adopt the spec accessor? Gap #2 above. The spec (ADR-082 API table, `crates/tls` README) pins `for_tcp_tls() -> tokio_rustls::TlsAcceptor` feature-gated on `tcp`; the extracted code never implemented it (callers wrap the acceptor themselves from `rustls_config()`). **Status:** open, lean adopt (infallible, spec-conforming, one-line). Decide in Phase 1 before the API freezes. ### OQ-TLS-04: `for_quinn()` — `self` or `&self`? The extracted code consumes `self` (server and client); the spec sketches `&self` for the server accessor. `&self` (plus the inner rustls config being `Clone`) allows one `TlsServerConfig` to feed both a quinn endpoint and a TCP+TLS acceptor directly — the ADR-082 story's most literal form. `self` forces callers through `rustls_config()` for the second transport. Note `TlsClientConfig::for_quinn(self)` is natural (one dial = one config build), so the two accessors need not agree. This question carries over unchanged to `for_noq()` (OQ-TLS-08). **Status:** open, small — lean `&self` on the server accessor to match the spec sketch, `self` on the client (consumed by `into_rustls_config` pattern anyway). ### OQ-TLS-05: Test surface for the invariants The behavior-preservation invariants deserve direct tests (the extracted code has them in-module). Questions: port the in-module tests as-is, split into `tests/` integration tests (no workspace consumers to cover here, so integration tests are the only cross-module surface), or both? Also: does the invariant test for `supported_verify_schemes` pin the exact nine-scheme list (regression-proof) or just non-empty + ED25519 present (the current extracted test)? **Status:** open, small — the alktunnels precedent (coverage weak-spot test commits late in the cycle) suggests porting in-module tests early and adding exact-list pins; not a blocker. ### OQ-TLS-06: Does the ACME state machine need a shutdown surface? `TlsServerConfig` holds the ACME task's `JoinHandle` (that is why it is not `Clone`), but nothing in the extracted code ever aborts the task — the handle is stored and dropped (dropping a `JoinHandle` detaches). For a long-lived hub that is fine (the state machine should run for the process lifetime); for tests and graceful-shutdown stories, a `shutdown()` (abort + await) might be wanted. Spec is silent. **Status:** open, small — decide whether v1 ships detached-only (document) or a shutdown surface (code). Lean detached-only for v1; revisit with the rewrite's graceful-shutdown design. ### OQ-TLS-07: iroh relationship in the rewrite The extracted client's iroh dial only reuses `TlsError` for error shaping; the server-side iroh path reads `Ed25519SecretKey` directly. The rewrite may re-shape how iroh consumes the identity (especially if OQ-TLS-01 moves the types). Verified 2026-09-09 against iroh 1.1: `iroh_base::SecretKey` is still 32 raw Ed25519 bytes (`from_bytes`/`to_bytes`), so the requirement is unchanged — keep `Ed25519SecretKey` (or its successor) constructible from raw bytes and `Clone`. No code needed here beyond that; the rewrite's iroh dial design should be checked against whatever OQ-TLS-01 decides. **Status:** open, deferred-shaped — blocked on the rewrite's dial/iroh design landing; track so the identity type keeps iroh's needs (`to_bytes()`-style access) in view. ### OQ-TLS-08: `quinn` feature vs `noq` feature — and the MSRV floor The rewrite moves QUIC to noq 1.2 (iroh's extracted fork; §Prior art). The extracted `quinn` feature + `for_quinn()` accessors face a decision: - **A: rename outright.** Drop `quinn` before any consumer exists — feature `noq = ["dep:noq"]`, accessors `for_noq()`, `TlsError` variant wrapping `noq_proto::crypto::rustls::NoInitialCipherSuite`. Clean; the pre-consumer window (this crate, pre-rewrite) is exactly when renames are free. No published alk* consumer uses `for_quinn`. - **B: both features during a transition.** `quinn` and `noq` side by side. There is no third consumer to transition with — alknet's rewrite is the only one — so this doubles the accessor surface for nothing. Leaning A. MSRV sub-decision RESOLVED 2026-09-10: the ecosystem floor is 1.88 crate-wide (matching noq's; `time` unpinned — see §Version and dependency posture), so the only open half of this OQ is the feature rename itself. Also record in the Phase 1 ADR: noq 1.2's `rustls-aws-lc-rs` feature is the wiring for ADR-084's provider posture on the QUIC path, and iroh 1.1's `tls-aws-lc-rs` feature is the matching iroh-side wiring — both feature-select the *same* rustls 0.23 + aws-lc-rs stack this crate already pins, so no new provider decision is needed. **Status:** open (feature-rename half only) — needs a Phase 1 ADR (feature-gate + accessor naming is one-way-door API surface). Lean A; final call belongs to the Phase 1 ADR alongside OQ-TLS-04's accessor-shape decision. ## Survey / prior-art list All internal; this crate has no external research debt: - `crates/alknet-tls/` — the extraction source (module inventory §Prior art). Authoritative for behavior; its tests are the seed test suite. - `crates/alknet-core/src/config.rs` — `TlsIdentity`, `Ed25519SecretKey`, `AcmeDirectory` (OQ-TLS-01 scope). - `crates/alknet-core/src/credentials.rs` — `ConnectionCredentials`, `RemoteIdentity` (ADR-091). - `crates/alknet-core/src/fingerprint.rs` — fingerprint computation + manual DER parsing (pure; moves cleanly). - `crates/alknet-client/src/dial/{quinn,tcp_tls,iroh}.rs` — the client consumption patterns (`TlsClientConfig` per dial; the iroh key-not-config exception). - `crates/alknet-endpoint/src/accept/tcp_tls.rs` — the server-side TCP+TLS accept loop (this crate provides the acceptor, not the loop). - `/workspace/noq` — the noq clone (upstream n0-computer/noq, 1.2.0): `noq-proto/src/crypto/rustls.rs` for the `QuicServerConfig`/ `QuicClientConfig` seam and `NoInitialCipherSuite` (§Prior art: noq). - `/workspace/iroh` — the iroh clone (upstream, 1.1.0): `Cargo.toml` noq/rustls pinning and the `tls-aws-lc-rs` feature; `iroh-base/src/ key.rs` for the SecretKey byte surface (§Prior art). - alknet ADRs: 082 (extraction), 083 (endpoint takes no TLS config), 084 (aws-lc-rs), 027 (identity model, `acme-tls/1`), 030 §6 (fingerprint normalization), 086 §3 (split ALPN lists), 087 (`TlsClientConfig`), 088 (`TlsError` shape, root-store fallback), 089 (dial seam), 091 (`ConnectionCredentials`), 034 (verifier selection). - `docs/architecture/crates/tls/README.md` — the full crate spec; the closest thing to this crate's Phase 1 target document. ## Convergence checklist (what Phase 0 must produce) - [x] Inventory of the extracted code with verified behavior-preservation invariants (§Prior art) — all five present, with line references - [x] Gap list: spec-pinned but unimplemented (§Gaps) — `TlsError` shape, `for_tcp_tls()`, config-type ownership, `SelfSigned` client semantics, tokio feature subset - [x] Rewrite-derived requirements (§What the alknet rewrite needs) — API surface freeze before consumers, no alknet imports, caller-owned ALPNs, iroh key-not-config - [x] Version/dependency posture recorded (§Version and dependency posture) — rustls 0.23 line current; feature-gate shape per spec; MSRV verified and resolved 2026-09-10 (ecosystem floor 1.88, `time` pin dropped, verified under the real 1.88 toolchain) - [x] noq/iroh investigation (2026-09-09) — noq 1.2 published, iroh 1.1 built on it; TLS seam API-compatible with quinn 0.11 (mechanical port); one rustls 0.23 tree across all QUIC paths; key-not-config exception intact; OQ-TLS-08 opened (feature rename + MSRV floor) - [x] Open questions enumerated (OQ-TLS-01..08) — all small; two are deferred-shaped (OQ-TLS-01 finalize, OQ-TLS-07); none require a POC (the code exists and runs; there is nothing to de-risk) - [x] Open questions promoted to Phase 1 `docs/architecture/open-questions.md` with statuses — DONE 2026-09-10 (OQ-TLS-01..08 promoted; all resolved at Phase 1 entry via ADR-001..006 or documented behavior) - [ ] POC decision: none planned — extraction of working code does not need a POC; the OQ-TN-10-style validation step from alktunnels does not apply here