- 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
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::ServerConfigbuilt 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 viaArc, 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;
TlsServerConfigis notClone(it holds the ACME task'sJoinHandle) precisely so callers must share viaArc. - 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'stls-aws-lc-rsfeature; FIPS- capable. Do not switch toringor the process-default provider without a new ADR. - The identity model (alknet ADR-027).
TlsIdentityis a four- variant enum:X509 { cert, key }(paths, loaded from disk),RawKey(Ed25519SecretKey)(RFC 7250 raw public key),SelfSigned(dev), andAcme { domains, cache_dir, directory, contact }(server-only).Acmeon the client path is a config error. TlsClientConfigis 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).Noneis the public-X.509-endpoint state, not "skip verification." - The
TlsErrorshape (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-rootsis 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/1is 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) consumesTlsClientConfigviafor_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 aniroh::SecretKey; there is nofor_iroh()here. Updated 2026-09-09: the noq investigation (§Prior art: noq and the quinn→noq shift) adds a same-shapedfor_noq()accessor next tofor_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 aPeerEntry.fingerprintsentry 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). BothOptions 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::MAXon 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 setsconfig.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/1ALPN appended insidenew_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()mergeswebpki-rootswhen 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 overstate.next()matchingEventOk/EventErrorvariants totracinglog lines.newspawns and returns immediately (does not await the first cert) — handshakes fail transiently until the first order completes. Spec-consistent. new_acmeswallows the resolver's cert absence by design. The spawned task logsOrdererrors atwarn!("will retry") — ACME retry semantics live insiderustls-acme; the task exits only when the stream ends.for_quinn()consumesself, not&selfin the extracted code (both server and client). The spec sketches&selffor the server accessor. Minor API question for Phase 1 (see OQ-TLS-04) — consumingselfis safe for the one-config-per-endpoint-type assembly pattern but prevents building two transports from oneTlsServerConfigwithout going throughrustls_config()manually.FingerprintPinVerifierverifies 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 throughverify_tls13_signature_with_raw_keyand 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.
RawKeyClientCertResolverauto-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/Nonepresent nothing (NoClientCertResolver), andAcmeis rejected at config time. - The 3-variant
TlsErrorfolds typed sources into strings. Everymap_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'stests/, 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.
TlsErroris 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::Errorrelies onrustls_pemfilefunnelling its non-std::error::Errorerror type intoio::Error(ADR-088 §Gotchas #2) — verify that holds at the pinned rustls/pemfile versions.for_tcp_tls()does not exist. Both ADR-082's API table and thecrates/tlsREADME specfor_tcp_tls() -> tokio_rustls::TlsAcceptor(feature-gated ontcp); 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.- The config types still live in
alknet-core.TlsIdentity,Ed25519SecretKey(config.rs),ConnectionCredentials/RemoteIdentity(credentials.rs), andfingerprint.rsare all in core. This crate is expected to own them after the rewrite (AGENTS.md convention 10) — but they are config types thatStaticConfigand 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/AuthPolicyare not TLS concerns and stay wherever identity resolution lands. SelfSignedis server-only in practice. The extracted client path mapsSome(TlsIdentity::SelfSigned)toNoClientCertResolver(present nothing) — reasonable, but the identity enum doesn't encode the constraint the wayAcmeis encoded (client-path config error). Minor semantic wobble to resolve when the types move (OQ-TLS-02).tokiofeatures. The extracted crate usesfeatures = ["full"](workspace style); this crate pins the lean subset (rt,sync,macros) per AGENTS.md convention 3. Verifyrustls-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_configon the server;new,for_quinn,into_rustls_configon the client;TlsErrorin 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 fromalknet-tls, which importsalknet-corethroughout. - 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.1are strings passed in by the assembly layer (ADR-086 §3 split lists); this crate owns onlyacme-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-udpsiblings). Workspace layout mirrors quinn's (noqasync facade + sans-ionoq-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 withnoq_proto::crypto::rustls::NoInitialCipherSuite— the same constructor shape and the same failure type ADR-088'sQuinnWrapvariant wraps. Portingfor_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 onrustls ^0.23.33— the same rustls 0.23 line this crate pins. Thetls-aws-lc-rs/tls-ringfeature split maps onto noq'saws-lc-rs/ringfeatures; our aws-lc-rs-only posture (ADR-084) composes cleanly (noqfeaturerustls-aws-lc-rs). - The iroh key surface is unchanged for us.
iroh_base::SecretKeyis still 32 raw Ed25519 bytes (from_bytes/to_bytes), the key-not-config exception still holds (nofor_iroh(), no rustls config handed to iroh), and the fingerprint normalization story (ed25519:<hex>) is unaffected.
Implications for this crate:
- The
quinnfeature becomes (or is joined by) anoqfeature. The transport-gate story stays the same — feature-gated wrapping — but the accessor name and dep change. Phase 1 decides: renamefor_quinn→for_noqoutright (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 wrapsNoInitialCipherSuite; under noq it wrapsnoq_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
tcpfeature) 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 alktls —
StaticConfigand 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 —
ConnectionCredentialsis 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-tlsshape 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
quinnbefore any consumer exists — featurenoq = ["dep:noq"], accessorsfor_noq(),TlsErrorvariant wrappingnoq_proto::crypto::rustls::NoInitialCipherSuite. Clean; the pre-consumer window (this crate, pre-rewrite) is exactly when renames are free. No published alk* consumer usesfor_quinn. - B: both features during a transition.
quinnandnoqside 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 (TlsClientConfigper 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.rsfor theQuicServerConfig/QuicClientConfigseam andNoInitialCipherSuite(§Prior art: noq)./workspace/iroh— the iroh clone (upstream, 1.1.0):Cargo.tomlnoq/rustls pinning and thetls-aws-lc-rsfeature;iroh-base/src/ key.rsfor 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 (TlsErrorshape, 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) —
TlsErrorshape,for_tcp_tls(), config-type ownership,SelfSignedclient 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,
timepin 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