phase-0: noq/iroh investigation — quinn→noq shift recorded
- noq 1.2.0 published (iroh's extracted quinn fork); iroh 1.1.0 is built on noq 1.2.0 and re-exports noq as its public API - TLS seam verified API-compatible with quinn 0.11: QuicServerConfig/ QuicClientConfig try_from + NoInitialCipherSuite — port is mechanical - one rustls 0.23 tree across all QUIC paths (both pin 0.23.33); ADR-084 posture strengthens (no ring/aws-lc provider split possible) - iroh key surface unchanged (32-byte Ed25519 SecretKey) — key-not-config exception intact - OQ-TLS-08 opened: quinn feature → noq feature rename + honest MSRV floor (noq 1.88, iroh 1.91); OQ-TLS-04/07 updated; survey + checklist extended Verified: cargo fmt --check, cargo test, cargo doc --no-deps
This commit is contained in:
+156
-13
@@ -15,8 +15,17 @@ input to Phase 1 (Architecture), where the Architect will produce
|
|||||||
Drafted 2026-09-09. The crate is the TLS layer of the alk* stack: shared TLS
|
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,
|
setup types — server and client `rustls` configs, cert resolvers, verifiers,
|
||||||
and ACME state-machine wiring — transport-agnostic and shareable across
|
and ACME state-machine wiring — transport-agnostic and shareable across
|
||||||
transports (quinn, `tokio-rustls` TCP+TLS, and anything else that consumes a
|
transports (noq QUIC below, `tokio-rustls` TCP+TLS, and anything else
|
||||||
`rustls` config).
|
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
|
Unlike alktunnels (which was entirely new protocol code), this crate is an
|
||||||
**extraction** of working, battle-tested TLS code from alknet. The prior art
|
**extraction** of working, battle-tested TLS code from alknet. The prior art
|
||||||
@@ -102,7 +111,10 @@ pointers; read the referenced ADRs for the full rationale.
|
|||||||
`for_quinn()` / `into_rustls_config()`; this crate does not dial. The
|
`for_quinn()` / `into_rustls_config()`; this crate does not dial. The
|
||||||
iroh transport is the key-not-config exception on both sides — iroh
|
iroh transport is the key-not-config exception on both sides — iroh
|
||||||
has its own TLS and takes an `iroh::SecretKey`; there is no
|
has its own TLS and takes an `iroh::SecretKey`; there is no
|
||||||
`for_iroh()` here.
|
`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
|
- **Fingerprint normalization (alknet ADR-030 §6).** `ed25519:<hex>` for
|
||||||
RFC 7250 raw keys, `SHA256:<hex>` for cert DER — identical across
|
RFC 7250 raw keys, `SHA256:<hex>` for cert DER — identical across
|
||||||
quinn, iroh, and TCP+TLS paths, so a `PeerEntry.fingerprints` entry
|
quinn, iroh, and TCP+TLS paths, so a `PeerEntry.fingerprints` entry
|
||||||
@@ -291,10 +303,12 @@ 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),
|
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`,
|
`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
|
`ed25519-dalek 2`, `thiserror 2`, `tokio 1`. As of 2026-09 the rustls
|
||||||
0.23 line is still current (0.23.43, July 2026) — no forced major bump.
|
0.23 line is still current (0.23.44 stable; iroh and noq pin
|
||||||
Phase 1 should re-verify at packaging time and record any bump as a
|
`rustls 0.23.33` — same 0.23 major, so one unified 0.23 tree across
|
||||||
line in the dependency ADR. Dependency shape per the spec: `rustls`
|
quinn/noq/iroh paths is achievable). Phase 1 should re-verify at
|
||||||
always present (the core library); `quinn`/`tcp`/`acme` feature-gated;
|
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
|
`rustls-native-certs` + `webpki-roots` always present (the CA path is
|
||||||
needed by any client dialing public X.509 endpoints regardless of
|
needed by any client dialing public X.509 endpoints regardless of
|
||||||
transport); `futures` acme-gated (the event loop's `StreamExt`).
|
transport); `futures` acme-gated (the event loop's `StreamExt`).
|
||||||
@@ -315,6 +329,86 @@ with the pin — if Phase 1 later decides `rust-version` should track
|
|||||||
reality (e.g. 1.88+), that is a one-line change plus unpinning. Either
|
reality (e.g. 1.88+), that is a one-line change plus unpinning. Either
|
||||||
way, the decision is recorded here rather than inherited silently.
|
way, the decision is recorded here rather than inherited silently.
|
||||||
|
|
||||||
|
**MSRV pressure from the noq/iroh direction.** noq 1.2 declares
|
||||||
|
rust-version 1.88 and iroh 1.1 declares 1.91 (both edition 2024). The
|
||||||
|
quinn 0.11 path today keeps the tree at 1.85; the noq path raises the
|
||||||
|
crate's de facto floor to 1.88+ for consumers that enable the quic
|
||||||
|
feature. Since quic/iroh consumers (the rewrite) sit on iroh 1.91
|
||||||
|
anyway, this argues for Phase 1 setting `rust-version = 1.88`
|
||||||
|
crate-wide (matching noq's floor, below iroh's) and unpinning `time` —
|
||||||
|
see OQ-TLS-08. The alknet-tls extraction source (quinn-only, no iroh
|
||||||
|
dep) can stay at its own floor; the floors only unify at the
|
||||||
|
assembly layer.
|
||||||
|
|
||||||
|
## 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_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
|
## Open Questions
|
||||||
|
|
||||||
Numbered OQ-TLS-01.. so they can be promoted into
|
Numbered OQ-TLS-01.. so they can be promoted into
|
||||||
@@ -386,6 +480,7 @@ endpoint and a TCP+TLS acceptor directly — the ADR-082 story's most
|
|||||||
literal form. `self` forces callers through `rustls_config()` for the
|
literal form. `self` forces callers through `rustls_config()` for the
|
||||||
second transport. Note `TlsClientConfig::for_quinn(self)` is natural
|
second transport. Note `TlsClientConfig::for_quinn(self)` is natural
|
||||||
(one dial = one config build), so the two accessors need not agree.
|
(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
|
**Status:** open, small — lean `&self` on the server accessor to match
|
||||||
the spec sketch, `self` on the client (consumed by `into_rustls_config`
|
the spec sketch, `self` on the client (consumed by `into_rustls_config`
|
||||||
@@ -423,14 +518,50 @@ revisit with the rewrite's graceful-shutdown design.
|
|||||||
The extracted client's iroh dial only reuses `TlsError` for error
|
The extracted client's iroh dial only reuses `TlsError` for error
|
||||||
shaping; the server-side iroh path reads `Ed25519SecretKey` directly.
|
shaping; the server-side iroh path reads `Ed25519SecretKey` directly.
|
||||||
The rewrite may re-shape how iroh consumes the identity (especially if
|
The rewrite may re-shape how iroh consumes the identity (especially if
|
||||||
OQ-TLS-01 moves the types). No code needed here beyond keeping
|
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
|
`Ed25519SecretKey` (or its successor) constructible from raw bytes and
|
||||||
`Clone` — but the rewrite's iroh dial design should be checked against
|
`Clone`. No code needed here beyond that; the rewrite's iroh dial
|
||||||
whatever OQ-TLS-01 decides.
|
design should be checked against whatever OQ-TLS-01 decides.
|
||||||
|
|
||||||
**Status:** open, deferred-shaped — blocked on the rewrite's dial/iroh
|
**Status:** open, deferred-shaped — blocked on the rewrite's dial/iroh
|
||||||
design landing; track so the identity type keeps iroh's needs
|
design landing; track so the identity type keeps iroh's needs
|
||||||
(`as_bytes()`-style access) in view.
|
(`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. Sub-decision: with the noq feature as the QUIC path, the
|
||||||
|
crate's de facto MSRV floor rises to noq's 1.88 (iroh consumers sit at
|
||||||
|
1.91 anyway). Phase 1 should set `rust-version = 1.88` crate-wide and
|
||||||
|
unpin `time` (the 1.85 pin exists only to keep the aspirational 1.85
|
||||||
|
claim — with an honest 1.88 floor, default resolution is fine and one
|
||||||
|
less pin to maintain). The `tcp` feature (tokio-rustls) has no 1.88
|
||||||
|
pressure of its own; a unified 1.88 crate floor is the simple shape.
|
||||||
|
|
||||||
|
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 — needs a Phase 1 ADR (feature-gate + accessor naming
|
||||||
|
is one-way-door API surface). Lean A with the 1.88 floor; final call
|
||||||
|
belongs to the Phase 1 ADR alongside OQ-TLS-04's accessor-shape
|
||||||
|
decision.
|
||||||
|
|
||||||
## Survey / prior-art list
|
## Survey / prior-art list
|
||||||
|
|
||||||
@@ -449,6 +580,12 @@ All internal; this crate has no external research debt:
|
|||||||
key-not-config exception).
|
key-not-config exception).
|
||||||
- `crates/alknet-endpoint/src/accept/tcp_tls.rs` — the server-side
|
- `crates/alknet-endpoint/src/accept/tcp_tls.rs` — the server-side
|
||||||
TCP+TLS accept loop (this crate provides the acceptor, not the loop).
|
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),
|
- alknet ADRs: 082 (extraction), 083 (endpoint takes no TLS config),
|
||||||
084 (aws-lc-rs), 027 (identity model, `acme-tls/1`), 030 §6
|
084 (aws-lc-rs), 027 (identity model, `acme-tls/1`), 030 §6
|
||||||
(fingerprint normalization), 086 §3 (split ALPN lists), 087
|
(fingerprint normalization), 086 §3 (split ALPN lists), 087
|
||||||
@@ -469,8 +606,14 @@ All internal; this crate has no external research debt:
|
|||||||
API surface freeze before consumers, no alknet imports,
|
API surface freeze before consumers, no alknet imports,
|
||||||
caller-owned ALPNs, iroh key-not-config
|
caller-owned ALPNs, iroh key-not-config
|
||||||
- [x] Version/dependency posture recorded (§Version and dependency
|
- [x] Version/dependency posture recorded (§Version and dependency
|
||||||
posture) — rustls 0.23 line current; feature-gate shape per spec
|
posture) — rustls 0.23 line current; feature-gate shape per spec;
|
||||||
- [x] Open questions enumerated (OQ-TLS-01..07) — all small; two are
|
MSRV verified under the real 1.85 toolchain (`time` pin) with the
|
||||||
|
honest-floor decision deferred to OQ-TLS-08
|
||||||
|
- [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
|
deferred-shaped (OQ-TLS-01 finalize, OQ-TLS-07); none require a
|
||||||
POC (the code exists and runs; there is nothing to de-risk)
|
POC (the code exists and runs; there is nothing to de-risk)
|
||||||
- [ ] Open questions promoted to Phase 1
|
- [ ] Open questions promoted to Phase 1
|
||||||
|
|||||||
Reference in New Issue
Block a user