Files
alktls/docs/research/phase-0.md
T
glm-5.3-flash e93238cb4a adopt ecosystem MSRV floor 1.88; drop the time pin
- rust-version 1.85 -> 1.88 (ecosystem-wide resolution from the audit
  sessions; matches noq 1.2's floor, sits below iroh 1.91)
- Cargo.lock: time 0.3.36 pin removed (existed only to keep the 1.85
  claim satisfiable); tree re-floated to current
- phase-0: MSRV thread marked RESOLVED; OQ-TLS-08 narrowed to the
  quinn->noq feature-rename half; lesson recorded (rust-version is
  passive metadata — claims must be compile-checked, not assumed)

Verified: cargo test, test --all-features, clippy -D warnings,
fmt --check, rustup run 1.88 cargo check
2026-09-10 03:51:31 +00:00

34 KiB

status, last_updated
status last_updated
draft 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 TlsServerConfigs — 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:<hex> for RFC 7250 raw keys, SHA256:<hex> 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<TlsIdentity> (client-auth presentation) + remote_identity: Option<RemoteIdentity> (verifier selection). Both Options 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<rustls::ServerConfig>) 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::spawned 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 (quinnnoq).

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:<hex>) 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_quinnfor_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 TlsServerConfigs 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 alktlsStaticConfig 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 coreConnectionCredentials 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.rsTlsIdentity, Ed25519SecretKey, AcmeDirectory (OQ-TLS-01 scope).
  • crates/alknet-core/src/credentials.rsConnectionCredentials, 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)

  • Inventory of the extracted code with verified behavior-preservation invariants (§Prior art) — all five present, with line references
  • Gap list: spec-pinned but unimplemented (§Gaps) — TlsError shape, for_tcp_tls(), config-type ownership, SelfSigned client semantics, tokio feature subset
  • Rewrite-derived requirements (§What the alknet rewrite needs) — API surface freeze before consumers, no alknet imports, caller-owned ALPNs, iroh key-not-config
  • 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)
  • 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)
  • 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)
  • Open questions promoted to Phase 1 docs/architecture/open-questions.md (Phase 1 work)
  • 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