phase 1: architecture spec — overview, server/client, ADR-001..006
- ADR-001: inherit the alknet TLS design as the baseline; deviations recorded as alktls ADRs - ADR-002: TlsError ships the ADR-088 six-variant shape from day one (typed #[from] sources; NoqWrap; no string catch-all) - ADR-003: the QUIC feature is noq (iroh's extracted fork), pre- consumer rename; default = [] per the lean-crate convention (corrects the extracted code's default = ["quinn"]) - ADR-004: complete accessors — for_tcp_tls() adopted, rustls_config() adopted; server accessors borrow (&self), client accessors consume - ADR-005: identity + credentials + fingerprint types move into alktls; auth layer stays out - ADR-006: eight-module layout; seed tests + integration invariant pins (exact nine-scheme list, client enable_early_data) - specs: overview (transport picture, terminology), server.md (ACME lifecycle, invariants), client.md (verifier selection matrix, root- store fallback); open-questions.md promotes OQ-TLS-01..08 (all resolved at entry) - Cargo.toml: quinn feature -> noq (per ADR-003); AGENTS.md aligned Architecture review pass done: 0 critical, 2 major (ADR-002 AcmeConfig doc comment contradiction; ADR-003 unrecorded default deviation) and 8 minors all addressed; cross-references verified against alknet ADRs, rustls/noq/iroh sources. Verified: cargo test, test --all-features, clippy -D warnings, fmt --check, doc --no-deps
This commit is contained in:
11
AGENTS.md
11
AGENTS.md
@@ -49,7 +49,7 @@ not change `git config`, skip hooks, or use `git commit -i`.
|
||||
This is the TLS crate — shared TLS setup types for the alk* stack:
|
||||
server and client `rustls` configs, cert resolvers, verifiers, and
|
||||
ACME state-machine wiring, transport-agnostic and shareable across
|
||||
transports (quinn, `tokio-rustls` TCP+TLS, and anything else that
|
||||
transports (noq, `tokio-rustls` TCP+TLS, and anything else that
|
||||
consumes a `rustls` config). It is the extraction of the TLS handling
|
||||
from alknet (alknet ADR-082/087/088 and the `crates/tls` spec are the
|
||||
prior art). The conventions below apply to all work in `src/` and
|
||||
@@ -80,8 +80,8 @@ session, not just spawned implementation agents.
|
||||
in `[dependencies]` (dev-dependencies may use `full`).
|
||||
|
||||
4. **The default crate should stay lean** — TLS setup and config types
|
||||
only. Transport-specific wrapping is feature-gated: `quinn` (the
|
||||
`for_quinn()` accessors), `tcp` (`tokio-rustls`), `acme` (the ACME
|
||||
only. Transport-specific wrapping is feature-gated: `noq` (the
|
||||
`for_noq()` accessors), `tcp` (`tokio-rustls`), `acme` (the ACME
|
||||
state machine, a heavy dep). The `rustls` dep is always present —
|
||||
it is the core library. Unlike the sibling protocol crates
|
||||
(alktunnels, alktty), wasm is not a load-bearing target here: the
|
||||
@@ -139,8 +139,9 @@ session, not just spawned implementation agents.
|
||||
logged in the spawned task, not `TlsError` variants. Do not grow
|
||||
`TlsError` to cover handshake outcomes.
|
||||
|
||||
9. **Feature gates** — transport-specific deps are opt-in (`quinn`,
|
||||
`tcp`, `acme`). The base crate compiles lean. Verify `cargo test`
|
||||
9. **Feature gates** — transport-specific deps are opt-in (`noq`,
|
||||
`tcp`, `acme`); `default = []` — the default crate compiles lean
|
||||
(ADR-003). Verify `cargo test`
|
||||
(default) and `cargo test --all-features` both pass whenever features
|
||||
are touched.
|
||||
|
||||
|
||||
488
Cargo.lock
generated
488
Cargo.lock
generated
@@ -2,6 +2,41 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alktls"
|
||||
version = "0.1.0"
|
||||
@@ -9,7 +44,8 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"futures",
|
||||
"hex",
|
||||
"quinn",
|
||||
"noq",
|
||||
"noq-proto",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"rustls-acme",
|
||||
@@ -302,6 +338,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
@@ -311,16 +357,6 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -336,6 +372,15 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -386,6 +431,15 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -449,6 +503,29 @@ version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.119",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -500,6 +577,17 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-assoc"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0590c4a94da3372e83493b956755a6e2266830b6e4e3b101afe66e3f39477b91"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -530,18 +618,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastbloom"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"libm",
|
||||
"portable-atomic",
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
@@ -560,12 +636,6 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
@@ -701,10 +771,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -721,6 +789,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.3"
|
||||
@@ -773,61 +851,27 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identity-hash"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da"
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.20",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.35"
|
||||
@@ -861,12 +905,6 @@ version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -927,6 +965,67 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noq"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b78be567e796cfa74bb9bdc4117790af2d505eb887019cf8803244353eb09d89"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"derive_more",
|
||||
"noq-proto",
|
||||
"noq-udp",
|
||||
"pin-project-lite",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noq-proto"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c1e5b6fe668491eca022f745a0a9402585626c73a7b839b3424ace15d6a9c8f"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"enum-assoc",
|
||||
"getrandom 0.4.3",
|
||||
"identity-hash",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"sorted-index-buffer",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noq-udp"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dc50afea61aff926e150a36c31f06094608848e114a3fe667d76ec233163b1c"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.8"
|
||||
@@ -976,6 +1075,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
@@ -1069,10 +1174,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
@@ -1089,64 +1200,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fastbloom",
|
||||
"getrandom 0.4.3",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -1280,7 +1333,6 @@ dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
@@ -1344,33 +1396,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||
dependencies = [
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
@@ -1389,15 +1414,6 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
@@ -1521,28 +1537,6 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -1565,6 +1559,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sorted-index-buffer"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -1750,13 +1750,37 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@@ -1794,6 +1818,28 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -1806,16 +1852,6 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
@@ -1877,15 +1913,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -1904,15 +1931,6 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
|
||||
@@ -6,7 +6,7 @@ rust-version = "1.88"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Shared TLS setup types: server and client rustls configs, cert resolvers, verifiers, and ACME state-machine wiring, transport-agnostic and shareable across transports."
|
||||
repository = "https://git.alk.dev/alkdev/alktls"
|
||||
keywords = ["tls", "rustls", "acme", "quinn", "network"]
|
||||
keywords = ["tls", "rustls", "acme", "noq", "network"]
|
||||
categories = ["network-programming", "cryptography", "asynchronous"]
|
||||
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/research/", "docs/plans/", "docs/sdd_process.md"]
|
||||
|
||||
@@ -14,8 +14,8 @@ exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/research/", "docs/p
|
||||
name = "alktls"
|
||||
|
||||
[features]
|
||||
default = ["quinn"]
|
||||
quinn = ["dep:quinn"]
|
||||
default = []
|
||||
noq = ["dep:noq", "dep:noq-proto"]
|
||||
tcp = ["dep:tokio-rustls"]
|
||||
acme = ["dep:rustls-acme"]
|
||||
|
||||
@@ -33,7 +33,8 @@ tracing = "0.1"
|
||||
thiserror = "2"
|
||||
|
||||
futures = { version = "0.3", optional = true }
|
||||
quinn = { version = "0.11", optional = true }
|
||||
noq = { version = "1.2", optional = true, default-features = false, features = ["rustls"] }
|
||||
noq-proto = { version = "1.2", optional = true, default-features = false }
|
||||
tokio-rustls = { version = "0.26", optional = true }
|
||||
rustls-acme = { version = "0.12", optional = true, features = ["aws-lc-rs"] }
|
||||
|
||||
|
||||
53
docs/architecture/README.md
Normal file
53
docs/architecture/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Architecture
|
||||
|
||||
The authoritative architecture spec for the alktls crate (Phase 1 of
|
||||
the SDD process). All docs are **Draft** pending the architecture
|
||||
review pass; statuses update here as docs advance.
|
||||
|
||||
All docs follow the SDD process conventions: specs reference ADRs and
|
||||
OQs by number, ADRs explain WHY, `open-questions.md` tracks what is
|
||||
unresolved.
|
||||
|
||||
## Documents
|
||||
|
||||
| Doc | Status | Scope |
|
||||
|-----|--------|-------|
|
||||
| [overview.md](overview.md) | Draft | Purpose, transport picture, API surface, ADR/OQ index |
|
||||
| [server.md](server.md) | Draft | `TlsServerConfig`, resolvers, ACME path, server invariants |
|
||||
| [client.md](client.md) | Draft | `TlsClientConfig`, verifier selection, client auth, root-store fallback |
|
||||
| [open-questions.md](open-questions.md) | live | The authoritative OQ tracker (all Phase 0 OQs resolved at entry) |
|
||||
|
||||
## ADRs
|
||||
|
||||
| ADR | Status | Decision |
|
||||
|-----|--------|----------|
|
||||
| [001](decisions/001-inherit-alknet-tls-design.md) | Accepted | Inherit the alknet TLS design as the baseline; deviations recorded as alktls ADRs |
|
||||
| [002](decisions/002-tlserror-shape.md) | Accepted | `TlsError`: the ADR-088 six-variant shape from day one; config-construction scope boundary |
|
||||
| [003](decisions/003-noq-replaces-quinn.md) | Accepted | The QUIC feature is `noq` (iroh's extracted fork), not `quinn`; iroh stays key-not-config |
|
||||
| [004](decisions/004-accessor-surface.md) | Accepted | Complete accessors: `for_tcp_tls()` adopted; server borrows, client consumes |
|
||||
| [005](decisions/005-config-types-move-into-alktls.md) | Accepted | Identity + credentials + fingerprint types move into alktls; auth layer stays out |
|
||||
| [006](decisions/006-module-layout-and-tests.md) | Accepted | Eight-module layout; seed tests + integration invariant pins |
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Docs move `Draft` → `Reviewed` when their open questions are resolved
|
||||
and the architecture review reports zero critical issues; ADRs are
|
||||
Accepted at write time and never revert (supersede instead).
|
||||
`open-questions.md` is the authoritative tracker; the Phase 0 doc's
|
||||
OQ statuses are the historical record.
|
||||
|
||||
## Phase status
|
||||
|
||||
- **Phase 0** (complete, 2026-09-10): `docs/research/phase-0.md` —
|
||||
extraction inventory, verified invariants, gaps, noq investigation,
|
||||
OQ-TLS-01..08.
|
||||
- **Phase 1** (this directory): all Phase 0 OQs resolved at entry —
|
||||
six via ADR-001..006, two as documented behavior (OQ-TLS-02,
|
||||
OQ-TLS-06); review pass pending.
|
||||
- **Phase 2** (next): decomposition into `tasks/` — the port guided by
|
||||
ADR-006's module map and seed tests.
|
||||
135
docs/architecture/client.md
Normal file
135
docs/architecture/client.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Client side
|
||||
|
||||
`TlsClientConfig` and its verifiers: the client-side TLS setup,
|
||||
extracted from alknet (`crates/alknet-tls/src/client.rs`) and ported
|
||||
per the ADRs. Decisions referenced here: see
|
||||
[open-questions.md](open-questions.md) and the index in
|
||||
[overview.md](overview.md).
|
||||
|
||||
## `TlsClientConfig`
|
||||
|
||||
Built **per dial** from a `ConnectionCredentials` + ALPN (the
|
||||
dial-seam pattern, alknet ADR-089). Consumed by its accessors — a
|
||||
`TlsClientConfig` is not reused across dials (ADR-004).
|
||||
|
||||
```rust
|
||||
pub struct TlsClientConfig {
|
||||
rustls_config: rustls::ClientConfig,
|
||||
}
|
||||
|
||||
impl TlsClientConfig {
|
||||
pub fn new(credentials: &ConnectionCredentials, alpn: &[u8])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(self) -> Result<noq::ClientConfig, TlsError>;
|
||||
|
||||
pub fn into_rustls_config(self) -> rustls::ClientConfig;
|
||||
}
|
||||
```
|
||||
|
||||
`new` is sync and infallible-free aside from config construction
|
||||
(cert file loading for an X.509 local identity is the only I/O).
|
||||
Every config sets `enable_early_data = true` (the client half of the
|
||||
0-RTT invariant) and carries the aws-lc-rs provider (alknet ADR-084).
|
||||
|
||||
## The two credential dimensions
|
||||
|
||||
`ConnectionCredentials` (ADR-005, moved from alknet ADR-091) carries
|
||||
exactly the two inputs the client config consumes:
|
||||
|
||||
1. **`local_identity: Option<TlsIdentity>`** — the client-auth cert
|
||||
presentation:
|
||||
|
||||
| Local identity | Presented |
|
||||
|----------------|-----------|
|
||||
| `RawKey` | RFC 7250 raw public key (SPKI DER; `only_raw_public_keys()` auto-detected from the DER) |
|
||||
| `X509` | the cert chain + key, loaded from disk |
|
||||
| `SelfSigned` / `None` | nothing (`NoClientCertResolver`) — documented, resolved in OQ-TLS-02 |
|
||||
| `Acme` | **config error** (`TlsError::AcmeConfig`) — server-only identity |
|
||||
|
||||
2. **`remote_identity: Option<RemoteIdentity>`** — the verifier
|
||||
selection matrix (below). Both `Option`s are load-bearing, not
|
||||
cosmetic: `Some` means "pin this", `None` means "trust the CA or
|
||||
fail" — never a placeholder default.
|
||||
|
||||
## Verifier selection (alknet ADR-034 §3)
|
||||
|
||||
Exactly three outcomes, driven by `remote_identity`:
|
||||
|
||||
| `remote_identity` | Remote cert | Verifier | Outcome |
|
||||
|-------------------|-------------|----------|---------|
|
||||
| `Some(fingerprint)` | any | `FingerprintPinVerifier` | pin match required |
|
||||
| `None` | X.509 | `WebPkiServerVerifier` (CA) | CA verification against the platform root store |
|
||||
| `None` | Ed25519 raw key | (the CA verifier fails) | **fail closed at handshake** |
|
||||
|
||||
`None` is the public-X.509-endpoint state, **not** "skip
|
||||
verification". An unknown raw-key remote fails at handshake — it must
|
||||
never silently downgrade to CA verification. Verifier selection
|
||||
happens at config construction; the fail-closed *manifests* at
|
||||
handshake time (the config carries the CA verifier; the raw-key
|
||||
remote simply cannot satisfy it) — this is the ADR-002 scope
|
||||
boundary in action.
|
||||
|
||||
**Fail-closed is structural**: known peer + fingerprint → pin;
|
||||
unknown + X.509 → CA; unknown + raw key → fail. No fourth path.
|
||||
|
||||
## `FingerprintPinVerifier`
|
||||
|
||||
The known-peer path. The fingerprint IS the trust anchor: for
|
||||
`ed25519:<hex>` remotes the raw Ed25519 key is extracted from the
|
||||
presented cert (SPKI) and matched; for `SHA256:<hex>` remotes the
|
||||
full cert DER is hashed and matched. No CA verification, no name
|
||||
verification — only the pin.
|
||||
|
||||
Handshake signatures are still verified (TLS 1.2/1.3, aws-lc-rs
|
||||
algorithms; Ed25519 SPKI certs route through
|
||||
`verify_tls13_signature_with_raw_key`): a stolen fingerprint cannot
|
||||
be replayed with a forged signature — the presenter must prove
|
||||
possession of the corresponding private key.
|
||||
|
||||
## The root-store fallback (alknet ADR-088 §5)
|
||||
|
||||
The `None` + X.509 CA path loads the platform's native root certs
|
||||
(`rustls-native-certs`). If the platform store is empty — a
|
||||
containerized deployment with no system CA bundle — the built-in
|
||||
`webpki-roots` are merged in so the store is **never empty**.
|
||||
Native-certs *load* errors are logged, not returned; the fallback
|
||||
guarantees non-emptiness regardless. This makes `NoRootAnchors`
|
||||
unreachable in practice (the `VerifierBuild` variant exists for the
|
||||
builder API's completeness, not a reachable path — ADR-002).
|
||||
|
||||
The root store is built unconditionally for the CA path — not
|
||||
feature-gated on `tcp` or `noq` — because any client dialing a public
|
||||
X.509 endpoint needs it regardless of transport.
|
||||
|
||||
## What the client side does NOT do
|
||||
|
||||
- No dial: the dial seam (`AlknetClient` in alknet; the rewrite's
|
||||
dial crate) consumes the config via `for_noq()` /
|
||||
`into_rustls_config()` and owns `TcpStream::connect`,
|
||||
`Endpoint::connect`, SNI, and SOCKS5 proxying (alknet ADR-089;
|
||||
SOCKS5 proxy seam per alknet ADR-090).
|
||||
Handshake failures are the dial's errors (`ClientDialError`),
|
||||
not `TlsError`.
|
||||
- No iroh: the iroh dial takes the 32-byte Ed25519 key directly
|
||||
(key-not-config; ADR-003). iroh's built-in verifier (NodeId check)
|
||||
is iroh's own application of the same fail-closed rule.
|
||||
- No per-request auth: a call-protocol `auth_token` is a call-layer
|
||||
concept; it never reaches `TlsClientConfig`.
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](overview.md) — the index; [server.md](server.md) —
|
||||
the server side
|
||||
- alknet `crates/tls/README.md` §Client side — the full client-side
|
||||
spec this doc mirrors
|
||||
- alknet ADR-034 (verifier selection), ADR-088 §5 (root-store
|
||||
fallback), ADR-091 (`ConnectionCredentials`)
|
||||
- ADR-002 (`TlsError`), ADR-004 (accessors), ADR-005 (credential
|
||||
types), ADR-006 (tests — the verifier-selection matrix)
|
||||
108
docs/architecture/decisions/001-inherit-alknet-tls-design.md
Normal file
108
docs/architecture/decisions/001-inherit-alknet-tls-design.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-001: Inherit the alknet TLS design; alktls as the rewrite's TLS crate
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
alktls is the extraction of the TLS handling from alknet
|
||||
(`crates/alknet-tls` + the config types in `crates/alknet-core`), the
|
||||
last crate extracted before the alknet rewrite begins. alknet already
|
||||
recorded the full TLS design as ADRs (082/083/084/086/087/088/089/
|
||||
091, plus ADR-027's identity model and ADR-034's verifier selection)
|
||||
and a complete crate spec (`docs/architecture/crates/tls/README.md`).
|
||||
The extracted code runs in production today and every
|
||||
behavior-preservation invariant was verified present in Phase 0
|
||||
(`docs/research/phase-0.md` §Prior art).
|
||||
|
||||
Phase 1 must decide how this crate relates to that body of decisions:
|
||||
re-litigate, inherit wholesale, or inherit with recorded deviations.
|
||||
|
||||
## Decision
|
||||
|
||||
alktls **inherits** the alknet TLS design. The alknet ADRs and the
|
||||
`crates/tls` README are adopted as this crate's authoritative prior
|
||||
decisions; Phase 1 does not re-litigate them. This ADR records that
|
||||
inheritance as alktls' own baseline, so this repo's ADR numbering
|
||||
starts from a coherent baseline instead of dangling references into
|
||||
another repo's architecture.
|
||||
|
||||
Concretely, alktls adopts:
|
||||
|
||||
- `TlsServerConfig` / `TlsClientConfig` as the central types, built
|
||||
once, shared across transports via `Arc` (alknet ADR-082). The inner
|
||||
rustls config is `Clone` (Arc-shared resolvers); `TlsServerConfig`
|
||||
is not `Clone` (it holds the ACME task's `JoinHandle`).
|
||||
- The config-construction scope boundary: this crate builds configs;
|
||||
handshake outcomes flow through the transport's connector; ACME
|
||||
runtime errors are stream events logged in the spawned task
|
||||
(alknet ADR-088 §6).
|
||||
- The behavior-preservation invariants verbatim: `max_early_data_size
|
||||
= u32::MAX` on all server paths **plus `enable_early_data = true`
|
||||
on every client config** (the client half of the 0-RTT invariant),
|
||||
`aws_lc_rs::default_provider()` on
|
||||
all paths, `AcceptAnyCertVerifier`'s nine-scheme
|
||||
`supported_verify_schemes()`, crate-side `acme-tls/1` append for the
|
||||
ACME path only, non-empty root store via `webpki-roots` fallback
|
||||
(alknet ADR-082, ADR-084, ADR-088 §5).
|
||||
- The identity model: `TlsIdentity` four variants, `Acme` server-only,
|
||||
client-auth presentation follows the local identity, verifier
|
||||
selection fail-closed (alknet ADR-027, ADR-034, ADR-091).
|
||||
- One ACME state machine per domain, shared across transports; never a
|
||||
second order for a served domain (alknet ADR-082 §The cert-reuse
|
||||
problem).
|
||||
- iroh stays key-not-config: no `for_iroh()`; the identity type
|
||||
exposes 32-byte Ed25519 access for `iroh_base::SecretKey`
|
||||
(alknet ADR-082 §Iroh is different).
|
||||
|
||||
Where alktls deviates from or refines the inherited design, the
|
||||
deviation is recorded as an alktls ADR (001.. this series) rather than
|
||||
silently diverging. The known deviations/corrections at time of
|
||||
writing: the `TlsError` shape (ADR-002), the QUIC feature (ADR-003,
|
||||
which also corrects the extracted code's `default = ["quinn"]` back
|
||||
to the spec's `default = []`), the API surface completion (ADR-004),
|
||||
and config-type ownership (ADR-005).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- The crate implements proven code, not a new design; Phase 2 is a
|
||||
port with a verified behavior checklist, not an invention.
|
||||
- Design rationale is already written and battle-tested; this repo's
|
||||
ADR series stays small and points at the alknet reasoning where the
|
||||
full argument lives.
|
||||
- The alknet rewrite consumes one crate with one consistent ADR
|
||||
lineage instead of reverse-engineering decisions from scattered
|
||||
alknet ADRs.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- alktls ADRs are derivative — a reader must consult alknet's ADRs for
|
||||
the full rationale of the inherited posture (mitigated: each
|
||||
alktls ADR restates the decision it pins, and the Phase 0 doc
|
||||
carries the verified line references).
|
||||
- If alknet's rewrite later re-decides an inherited posture, both
|
||||
series need a superseding note.
|
||||
|
||||
## References
|
||||
|
||||
- `docs/research/phase-0.md` — the Phase 0 inventory (verified
|
||||
invariants, gaps, noq investigation)
|
||||
- alknet ADR-082 (extraction), ADR-083 (endpoint takes no TLS config),
|
||||
ADR-084 (aws-lc-rs), ADR-027 (identity model), ADR-034 (verifier
|
||||
selection), ADR-086 §3 (split ALPN lists), ADR-087
|
||||
(`TlsClientConfig`), ADR-088 (`TlsError`, root-store fallback), 089
|
||||
(dial seam), 091 (`ConnectionCredentials`)
|
||||
- alknet `docs/architecture/crates/tls/README.md` — the full crate
|
||||
spec this crate implements
|
||||
- ADR-002 — the `TlsError` shape (gap #1)
|
||||
- ADR-003 — the QUIC feature: noq replaces quinn
|
||||
- ADR-004 — the API surface (gap #2, accessor shapes)
|
||||
- ADR-005 — config-type ownership (gap #3)
|
||||
149
docs/architecture/decisions/002-tlserror-shape.md
Normal file
149
docs/architecture/decisions/002-tlserror-shape.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-002: `TlsError` — the ADR-088 six-variant shape from day one
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The extracted code (`crates/alknet-tls/src/lib.rs`) carries a
|
||||
simplified 3-variant `TlsError`: `Config(String)`, `Io(io::Error)`,
|
||||
`Cert(String)`. alknet ADR-088 recorded the target shape — a single
|
||||
`#[non_exhaustive]` enum, one variant per failure category, typed
|
||||
`#[from]` sources — but it was never implemented; the extracted code
|
||||
folds every typed failure into `Config(e.to_string())` strings, losing
|
||||
the `#[source]` chain and the category distinction.
|
||||
|
||||
Phase 0 identified this as gap #1, the highest-value gap closure: this
|
||||
crate is pre-consumer, so the enum shape is still a two-way door; once
|
||||
the alknet rewrite consumes it, it is one-way.
|
||||
|
||||
Two mechanics from ADR-088's "Gotchas" needed verification before
|
||||
pinning the shape:
|
||||
|
||||
- `CertLoad`'s `#[from] io::Error` relies on `rustls_pemfile`
|
||||
funnelling its own non-`std::error::Error` error type into
|
||||
`io::Error`. Confirmed at the pinned versions: `rustls_pemfile`
|
||||
2.x returns `Result<T, io::Error>` from its iterator and read APIs
|
||||
(`certs`, `private_key`), so PEM parse failures surface as
|
||||
`io::Error` — the `CertLoad(#[from] io::Error)` variant is sound.
|
||||
- `VerifierBuild` wraps `rustls::webpki::VerifierBuilderError`
|
||||
(confirmed present in rustls 0.23.44: `NoRootAnchors` +
|
||||
`InvalidCrl(CertRevocationListError)`, `#[non_exhaustive]`).
|
||||
With the ADR-088 §5 root-store fallback in place,
|
||||
`NoRootAnchors` is unreachable in practice — the variant exists for
|
||||
the builder API's completeness, not a reachable failure path.
|
||||
|
||||
## Decision
|
||||
|
||||
`TlsError` ships as the alknet ADR-088 shape, unmodified:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum TlsError {
|
||||
/// Cert or key file read / PEM parse. `rustls_pemfile` funnels its
|
||||
/// own error type into `io::Error`, so one `io::Error` source covers
|
||||
/// the whole loading path.
|
||||
#[error("loading cert/key material: {0}")]
|
||||
CertLoad(#[from] std::io::Error),
|
||||
|
||||
/// Self-signed cert generation (rcgen). Server `SelfSigned` path.
|
||||
#[error("generating self-signed cert: {0}")]
|
||||
SelfSigned(#[from] rcgen::Error),
|
||||
|
||||
/// rustls server or client config construction
|
||||
/// (`with_safe_default_protocol_versions`, `with_single_cert`,
|
||||
/// `CertifiedKey::from_der`, `RootCertStore::add`).
|
||||
#[error("building rustls config: {0}")]
|
||||
Rustls(#[from] rustls::Error),
|
||||
|
||||
/// `WebPkiServerVerifier::builder(_with_provider)..build()` — the
|
||||
/// unknown-X.509-remote client path.
|
||||
#[error("building webpki verifier: {0}")]
|
||||
VerifierBuild(#[from] rustls::webpki::VerifierBuilderError),
|
||||
|
||||
/// QUIC config wrapping — the one path where `for_noq()` fails
|
||||
/// (`NoInitialCipherSuite`, not a `rustls::Error`). noq-gated.
|
||||
#[cfg(feature = "noq")]
|
||||
#[error("wrapping rustls config for noq: {0}")]
|
||||
NoqWrap(#[from] noq_proto::crypto::rustls::NoInitialCipherSuite),
|
||||
|
||||
/// Config-mismatch errors that are not wrapped third-party
|
||||
/// errors: ACME feature not enabled but `Acme` configured
|
||||
/// (server), or `Acme` identity used for client auth. A config
|
||||
/// error, not a wrapped third-party error.
|
||||
#[error("TLS config error: {0}")]
|
||||
AcmeConfig(String),
|
||||
}
|
||||
```
|
||||
|
||||
Notes on the shape, per ADR-088's rationale:
|
||||
|
||||
- **Single enum, not a thin wrapper.** ADR-088's three findings stand:
|
||||
the QUIC-wrap failure is `NoInitialCipherSuite` (not a
|
||||
`rustls::Error`), `rustls_pemfile`'s error is not a
|
||||
`std::error::Error`, and `WebPkiServerVerifier::build()` returns its
|
||||
own `VerifierBuilderError`. One enum with `#[from]` sources models
|
||||
the actual call sites.
|
||||
- **The quinn-gated variant becomes `NoqWrap`** (noq-gated), renaming
|
||||
ADR-088's `QuinnWrap` per ADR-003. Same failure type name
|
||||
(`NoInitialCipherSuite`), different crate path.
|
||||
- **`AcmeConfig(String)` keeps string payloads.** The two Acme
|
||||
mismatches are genuinely configuration mistakes, not wrapped
|
||||
upstream errors; there is no third-party error type to preserve.
|
||||
The extracted code's residual `Config(String)` call sites
|
||||
(e.g. `CertifiedKey::from_der` string fallbacks in the client-auth
|
||||
builder) map to this variant — see the refinement below.
|
||||
- **Scope boundary holds (alknet ADR-088 §6).** `TlsError` is the
|
||||
config-construction error type. Handshake outcomes (a rejected
|
||||
cert, the unknown-raw-key fail-closed) flow through the transport's
|
||||
connector; ACME state-machine runtime errors are stream events
|
||||
logged in the spawned task. No handshake variants, ever.
|
||||
- **`#[non_exhaustive]` from day one.** The enum is crate-local
|
||||
today, but the rewrite compiles against it; new variants are
|
||||
additive for consumers that match with a wildcard arm.
|
||||
|
||||
**Refinement over the extracted code:** the extracted code uses
|
||||
`TlsError::Config(e.to_string())` at `rustls::Error` call sites that
|
||||
ADR-088 assigns to `Rustls` (e.g. `with_single_cert`,
|
||||
`CertifiedKey::from_der` — both return `rustls::Error`). This crate
|
||||
maps those call sites to the typed variants. The only genuinely
|
||||
stringy sites are the Acme mismatches, which land in `AcmeConfig`
|
||||
(matching the sketch above — `AcmeConfig` holds exactly the two
|
||||
config-mismatch cases, nothing else). No `Config(String)` catch-all
|
||||
survives — a variant-per-category enum with a string catch-all would
|
||||
reintroduce the fold this ADR removes.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- `#[source]` chains survive for programmatic inspection (the
|
||||
rewrite's config plumbing can distinguish a missing file from a bad
|
||||
PEM from a bad config without string parsing).
|
||||
- The crate ships the recorded target shape — no later migration.
|
||||
- The noq rename rides along; `TlsError` never has a `QuinnWrap`
|
||||
variant to deprecate.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- Six variants is more surface than three; consumers must match
|
||||
more arms (mitigated by `#[non_exhaustive]` + wildcard arms).
|
||||
- `rcgen::Error` in the public API couples the error type to rcgen's
|
||||
error churn (accepted — rcgen 0.13 is stable and the coupling is
|
||||
what ADR-088 chose).
|
||||
|
||||
## References
|
||||
|
||||
- alknet ADR-088 — the recorded target shape and its rationale
|
||||
- alknet ADR-088 §5 — the root-store fallback that keeps
|
||||
`NoRootAnchors` unreachable
|
||||
- `docs/research/phase-0.md` §Gaps #1 — the gap analysis
|
||||
- ADR-001 — the inheritance baseline
|
||||
- ADR-003 — the noq feature (the `NoqWrap` rename)
|
||||
119
docs/architecture/decisions/003-noq-replaces-quinn.md
Normal file
119
docs/architecture/decisions/003-noq-replaces-quinn.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-003: The QUIC feature is `noq`, not `quinn`
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The extracted crate's `quinn` feature (`for_quinn()` accessors,
|
||||
quinn-gated wrap) predates the noq split: iroh has extracted its
|
||||
internal quinn fork into the standalone, published `noq` project
|
||||
(`noq` / `noq-proto` / `noq-udp`; noq 1.2.0 on crates.io), and iroh
|
||||
1.1.0 is built on noq 1.2.0, re-exporting `noq::*` as public API.
|
||||
The alknet rewrite will standardize on noq for QUIC — both the bare
|
||||
QUIC transport and iroh's internal one — eliminating the duplicate
|
||||
QUIC implementations.
|
||||
|
||||
Phase 0 verified the seam is API-compatible with quinn 0.11
|
||||
(`docs/research/phase-0.md` §Prior art: noq):
|
||||
|
||||
- `noq_proto::crypto::rustls::QuicServerConfig::try_from(
|
||||
rustls::ServerConfig)` / `QuicClientConfig::try_from(
|
||||
rustls::ClientConfig)` — same constructor shape, same
|
||||
`NoInitialCipherSuite` failure type, different crate path.
|
||||
- noq 1.2 pins `rustls ^0.23.33`; iroh 1.1 pins `rustls 0.23.33` —
|
||||
the same 0.23 line this crate pins. One rustls tree across all
|
||||
paths.
|
||||
- noq's `rustls-aws-lc-rs` feature selects the aws-lc-rs provider;
|
||||
iroh's `tls-aws-lc-rs` does the same on the iroh side. The ADR-084
|
||||
posture composes unchanged.
|
||||
- noq's MSRV is 1.88 — the ecosystem floor (adopted 2026-09-10),
|
||||
so no MSRV tension remains.
|
||||
|
||||
The `quinn` feature has no published consumer: alktls is
|
||||
pre-consumer, and the alknet rewrite (its only planned consumer)
|
||||
targets noq. Carrying both features would double the accessor surface
|
||||
with nothing to transition.
|
||||
|
||||
## Decision
|
||||
|
||||
**The QUIC feature is `noq`.** The `quinn` feature never existed in a
|
||||
published alktls, so the rename is pre-consumer and free:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
default = []
|
||||
noq = ["dep:noq", "dep:noq-proto"]
|
||||
tcp = ["dep:tokio-rustls"]
|
||||
acme = ["dep:rustls-acme"]
|
||||
|
||||
[dependencies]
|
||||
noq = { version = "1.2", optional = true, default-features = false, features = ["rustls"] }
|
||||
noq-proto = { version = "1.2", optional = true, default-features = false }
|
||||
```
|
||||
|
||||
`default = []` — all transport features opt-in, matching the
|
||||
inherited alknet spec's feature-gate shape and AGENTS.md convention 9
|
||||
(the default crate compiles lean; a consumer that never runs QUIC
|
||||
should not pull `noq` + `noq-proto`). Note the extracted code's
|
||||
`Cargo.toml` carries `default = ["quinn"]` — a silent deviation from
|
||||
the alknet spec that this port corrects rather than carries forward.
|
||||
The `noq` dep is enabled with `default-features = false,
|
||||
features = ["rustls"]` so noq's provider-selection features do not
|
||||
fight the crate's explicit provider posture; `noq-proto` rides along
|
||||
for the `NoqWrap` error source.
|
||||
|
||||
- `TlsServerConfig::for_noq(&self) -> Result<noq::ServerConfig,
|
||||
TlsError>` (feature-gated on `noq`) —
|
||||
`noq::ServerConfig::with_crypto(Arc::new(
|
||||
noq::crypto::rustls::QuicServerConfig::try_from(inner)?))`.
|
||||
- `TlsClientConfig::for_noq(self) -> Result<noq::ClientConfig,
|
||||
TlsError>` (feature-gated on `noq`) — the client-side mirror.
|
||||
- `TlsError::NoqWrap(#[from]
|
||||
noq_proto::crypto::rustls::NoInitialCipherSuite)` (ADR-002).
|
||||
- There is no `for_quinn()`, no `quinn` dependency, no quinn feature.
|
||||
- iroh remains key-not-config (ADR-001): iroh's endpoint is fed the
|
||||
Ed25519 secret key; the `noq` feature does not attempt to serve
|
||||
iroh's TLS, and iroh does not appear in this crate's dependency
|
||||
tree.
|
||||
- The `tcp` feature (`tokio-rustls`) and `acme` feature are
|
||||
unaffected.
|
||||
|
||||
Provider wiring on the QUIC path: the rustls configs this crate builds
|
||||
already carry the aws-lc-rs provider (every path constructs its own
|
||||
provider via `builder_with_provider`); noq's `rustls` feature
|
||||
consumes that provider from the config.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- One QUIC implementation across the bare-QUIC and iroh transports;
|
||||
no duplicate quinn/noq stacks in the rewrite's binaries.
|
||||
- `TlsError` never has a dead `QuinnWrap` variant.
|
||||
- The crate's MSRV floor (1.88) is honest for the whole tree rather
|
||||
than feature-dependent.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- If a future consumer needs quinn (none is known), that is a new
|
||||
feature alongside `noq` — additive, cheap, and only paid if real.
|
||||
- noq is young (1.2.0, first release Feb 2026); its API may churn
|
||||
faster than quinn 0.11's frozen line (accepted — n0-computer owns
|
||||
both noq and iroh, and the rewrite rides their stack; pin `noq =
|
||||
"1.2"` and bump deliberately).
|
||||
|
||||
## References
|
||||
|
||||
- `docs/research/phase-0.md` §Prior art: noq — the verified seam facts
|
||||
- `/workspace/noq`, `/workspace/iroh` — the upstream clones inspected
|
||||
- ADR-001 — the inheritance baseline (quinn-era accessors inherited
|
||||
with deviations recorded here)
|
||||
- ADR-002 — `TlsError` (`NoqWrap`)
|
||||
- ADR-004 — accessor shapes (`&self` vs `self`)
|
||||
129
docs/architecture/decisions/004-accessor-surface.md
Normal file
129
docs/architecture/decisions/004-accessor-surface.md
Normal file
@@ -0,0 +1,129 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-004: Complete the accessors — `for_tcp_tls()`, `rustls_config()`, and borrow-vs-consume
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The alknet spec (ADR-082's API table and the `crates/tls` README)
|
||||
pins four server-side accessors and three client-side accessors. The
|
||||
extracted code implements a subset and diverges in shape:
|
||||
|
||||
| Accessor | Spec | Extracted code |
|
||||
|----------|------|----------------|
|
||||
| `TlsServerConfig::new` | `async`, `(&TlsIdentity, &[Vec<u8>])` | same |
|
||||
| `TlsServerConfig::for_quinn` | `&self` | `self` (consumes) |
|
||||
| `TlsServerConfig::for_tcp_tls` | `-> TlsAcceptor`, infallible | **missing** — callers wrap `tokio_rustls::TlsAcceptor::from(Arc::new(cfg.rustls_config.clone()))` themselves |
|
||||
| `TlsServerConfig::rustls_config` | `&self -> &ServerConfig` | **missing** — the field is `pub(crate)` |
|
||||
| `TlsClientConfig::new` | sync, `(&ConnectionCredentials, &[u8])` | same |
|
||||
| `TlsClientConfig::for_quinn` | `self` (consumes) | `self` |
|
||||
| `TlsClientConfig::into_rustls_config` | `self` (consumes) | same |
|
||||
|
||||
The missing server accessors force the assembly layer to reach into
|
||||
crate internals (`pub(crate)` field access is impossible for external
|
||||
consumers) or re-derive the acceptor wrap. The `self`-consuming
|
||||
server accessor prevents the literal ADR-082 story — one
|
||||
`TlsServerConfig` feeding both a QUIC endpoint and a TCP+TLS acceptor —
|
||||
without contortions.
|
||||
|
||||
## Decision
|
||||
|
||||
The public API surface is the spec's surface, with borrow-vs-consume
|
||||
decided per accessor:
|
||||
|
||||
```rust
|
||||
impl TlsServerConfig {
|
||||
pub async fn new(identity: &TlsIdentity, alpns: &[Vec<u8>])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
/// `&self` — the inner rustls config is Clone (Arc-shared
|
||||
/// resolvers); one TlsServerConfig can feed a noq endpoint AND a
|
||||
/// TCP+TLS acceptor without contortions.
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(&self) -> Result<noq::ServerConfig, TlsError>;
|
||||
|
||||
/// Infallible — `TlsAcceptor::from(Arc<ServerConfig>)` cannot
|
||||
/// fail. Feature-gated on `tcp`.
|
||||
#[cfg(feature = "tcp")]
|
||||
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor;
|
||||
|
||||
/// Borrow the inner config for transport wrappers the crate does
|
||||
/// not cover.
|
||||
pub fn rustls_config(&self) -> &rustls::ServerConfig;
|
||||
}
|
||||
|
||||
impl TlsClientConfig {
|
||||
pub fn new(credentials: &ConnectionCredentials, alpn: &[u8])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
/// Consumes — one dial = one config build; a `TlsClientConfig` is
|
||||
/// not reused across dials.
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(self) -> Result<noq::ClientConfig, TlsError>;
|
||||
|
||||
/// Consumes — the TCP+TLS dial wraps the returned config in a
|
||||
/// `TlsConnector` itself.
|
||||
pub fn into_rustls_config(self) -> rustls::ClientConfig;
|
||||
}
|
||||
```
|
||||
|
||||
Rationale per accessor:
|
||||
|
||||
- **Server accessors take `&self`.** The ADR-082 story is "one
|
||||
identity, N transports": the assembly layer builds one
|
||||
`TlsServerConfig` per endpoint type and hands it to every transport
|
||||
that endpoint serves. `&self` plus the Clone inner config makes the
|
||||
multi-transport sharing direct; the extracted `self`-consuming shape
|
||||
is a vestige of the quinn-only extraction era (the TCP path was
|
||||
re-derivable only because the field happened to be `pub(crate)` in
|
||||
the same workspace).
|
||||
- **Client accessors consume `self`.** A `TlsClientConfig` is built
|
||||
per dial (`dial_quic`, `dial_tcp_tls` build fresh configs per
|
||||
ADR-089's pattern); nothing reuses it. Consuming makes
|
||||
`into_rustls_config` zero-cost (no clone behind the scenes) and
|
||||
keeps the API honest about reuse.
|
||||
- **`for_tcp_tls()` is adopted** (Phase 0 gap #2). It is one line,
|
||||
infallible, and the spec is unambiguous; leaving it out would keep
|
||||
the de facto "callers wrap the acceptor" shape and force the rewrite
|
||||
to duplicate it.
|
||||
- **`rustls_config()` is adopted** for any transport wrapper beyond
|
||||
`for_noq` / `for_tcp_tls` (iroh does not need it — key-not-config —
|
||||
but the escape hatch costs nothing and stays true to the spec).
|
||||
- **`new` stays `async fn`** for API uniformity with the ACME path
|
||||
(which spawns the state-machine task); the non-ACME paths have no
|
||||
await point (sync file I/O) — this is the spec's recorded posture,
|
||||
and the uniform signature is worth more than the await-free purity.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- The assembly layer builds configs and transports without reaching
|
||||
into crate internals or hand-rolling acceptor wraps.
|
||||
- The API freeze matches the spec the rewrite was specced against —
|
||||
zero translation for the rewrite's consumers.
|
||||
- The multi-transport story (one config → noq + TCP+TLS) is directly
|
||||
expressible.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- `&self` server accessors require the inner rustls config to stay
|
||||
`Clone`-able — already true and load-bearing (Arc-shared
|
||||
resolvers); a future config shape that is not Clone would break the
|
||||
accessor contract (acceptable: Clone is structural to the
|
||||
sharing story).
|
||||
|
||||
## References
|
||||
|
||||
- alknet ADR-082 API table and the `crates/tls` README §Architecture —
|
||||
the spec surface this ADR adopts
|
||||
- `docs/research/phase-0.md` §Gaps #2, OQ-TLS-03/OQ-TLS-04 — the gap
|
||||
and the accessor-shape question
|
||||
- ADR-002 — `TlsError` (the `for_noq` failure variant)
|
||||
- ADR-003 — the `noq` feature
|
||||
109
docs/architecture/decisions/005-config-types-move-into-alktls.md
Normal file
109
docs/architecture/decisions/005-config-types-move-into-alktls.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-005: Config types move into alktls; alktls owns the identity, credentials, and fingerprint
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The extracted code imports four type groups from `alknet-core`:
|
||||
|
||||
| Group | Types | Used by |
|
||||
|-------|-------|---------|
|
||||
| Identity | `TlsIdentity`, `Ed25519SecretKey`, `AcmeDirectory` | server + client config construction |
|
||||
| Credentials | `ConnectionCredentials`, `RemoteIdentity` | `TlsClientConfig::new` |
|
||||
| Fingerprint | `fingerprint_from_cert_der`, `extract_ed25519_raw_key_from_spki` | `FingerprintPinVerifier`, server-side extraction (endpoint) |
|
||||
| Auth (not TLS) | `PeerEntry`, `AuthPolicy`, `Identity`, `IdentityProvider` | fingerprint → peer-id resolution (endpoint/auth layer) |
|
||||
|
||||
AGENTS.md convention 10 says this crate owns the config types after
|
||||
the rewrite; but they are *config* types that `StaticConfig`, the
|
||||
vault, and the auth layer also consume. Phase 0's OQ-TLS-01 framed
|
||||
three options: move everything (A), move identity + fingerprint and
|
||||
leave credentials at the dial seam (B), or keep in core with
|
||||
re-exports (C).
|
||||
|
||||
The structural constraint: **alktls must compile without alknet**. It
|
||||
is the rewrite's TLS crate; a dependency on the rewrite's core would
|
||||
invert the layering (core always depends on TLS types, never the
|
||||
reverse). Re-exports (C) would leave alktls importing from a crate
|
||||
that is itself being rewritten — the coupling this extraction exists
|
||||
to break.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option A with one carve-out: alktls owns the identity, credentials,
|
||||
and fingerprint types; the auth layer stays out.**
|
||||
|
||||
Moving into alktls (`src/`):
|
||||
|
||||
- `identity.rs` — `TlsIdentity`, `Ed25519SecretKey`, `AcmeDirectory`
|
||||
(from alknet-core `config.rs`). `Ed25519SecretKey` keeps its exact
|
||||
surface — `generate()`, `from_bytes(&[u8; 32])`, `as_bytes() ->
|
||||
[u8; 32]`, `public()`, `sign()` — because iroh's
|
||||
`iroh_base::SecretKey` consumes the 32 raw bytes (OQ-TLS-07's
|
||||
requirement, verified against iroh 1.1 in Phase 0) and the
|
||||
`Ed25519SigningKey` helper signs through it.
|
||||
- `credentials.rs` — `ConnectionCredentials`, `RemoteIdentity` (from
|
||||
alknet-core `credentials.rs`), including the load-bearing `Option`
|
||||
semantics in their doc comments: `Some(fingerprint)` = pin,
|
||||
`None` = CA-or-fail-closed, never a placeholder default.
|
||||
- `fingerprint.rs` — `fingerprint_from_cert_der`,
|
||||
`extract_ed25519_raw_key_from_spki`, and the manual DER parser
|
||||
(from alknet-core). Production code stays `sha2` + manual DER —
|
||||
no rustls dep needed by the module itself (the crate has rustls
|
||||
anyway, but the module's purity keeps it testable standalone).
|
||||
|
||||
Staying out of alktls:
|
||||
|
||||
- `PeerEntry`, `AuthPolicy`, `Identity`, `IdentityProvider`,
|
||||
fingerprint → peer-id resolution — these are the auth/identity
|
||||
layer (peer records, tokens, scopes), not TLS. The rewrite's core
|
||||
(or its auth crate) owns them. alktls' server side hands the
|
||||
extracted fingerprint string to the caller; the caller resolves it.
|
||||
This keeps the TLS-crate scope boundary clean: cert-level identity
|
||||
in, peer-level identity out.
|
||||
|
||||
`StaticConfig`'s `tls_identity` field follows the type: the rewrite's
|
||||
config struct depends on alktls for `TlsIdentity` (one-way: config →
|
||||
alktls, never alktls → config). The vault derives `Ed25519SecretKey`
|
||||
from key material and hands it in — alktls has no vault dep.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- alktls compiles standalone; the rewrite's core depends on alktls
|
||||
for TLS types in one direction only. No cycle risk when the
|
||||
rewrite's module map settles.
|
||||
- The credential bundle (`ConnectionCredentials`) and the verifier
|
||||
selection it drives (ADR-034) live in the same crate — the
|
||||
semantics are coupled (the `Option` dance is meaningless outside
|
||||
the verifier contract), so co-locating them prevents semantic
|
||||
drift between the bundle and the verifier.
|
||||
- The fingerprint helpers move with their consumers
|
||||
(`FingerprintPinVerifier`); the normalized `ed25519:<hex>` /
|
||||
`SHA256:<hex>` formats stay with the code that computes them.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- alktls' public API grows by the moved types (identity +
|
||||
credentials + fingerprint). This is the point — the types are TLS
|
||||
types — but it widens the API-freeze review surface.
|
||||
- `alknet-core` (in the rewrite's form) must take the alktls dep for
|
||||
`TlsIdentity` in its config struct. If the rewrite's core wants to
|
||||
stay dependency-light, the type could be re-exported rather than
|
||||
consumed deeply (implementation detail of the rewrite, not this
|
||||
crate's concern).
|
||||
|
||||
## References
|
||||
|
||||
- AGENTS.md convention 10 — the recorded ownership expectation
|
||||
- `docs/research/phase-0.md` §Gaps #3, OQ-TLS-01 — the framing
|
||||
- alknet ADR-091 — `ConnectionCredentials` semantics
|
||||
- alknet ADR-030 §6 — fingerprint normalization
|
||||
- ADR-001 — the inheritance baseline
|
||||
103
docs/architecture/decisions/006-module-layout-and-tests.md
Normal file
103
docs/architecture/decisions/006-module-layout-and-tests.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-006: Module layout and test surface
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The extracted crate is organized as four modules plus the error
|
||||
(`lib.rs`, `server.rs`, `client.rs`, `pem.rs`, `signing.rs`) with all
|
||||
tests as in-module `#[cfg(test)]` blocks. alktls adds three modules of
|
||||
moved types (ADR-005) and, per the repo's conventions, needs a
|
||||
decided module map and test layout before the port starts.
|
||||
|
||||
Two shape questions need answers: where the moved types live
|
||||
relative to the config constructors, and how the seed tests port
|
||||
(no external workspace exists here, so in-module tests are the only
|
||||
cross-module surface a full-crate crate has).
|
||||
|
||||
## Decision
|
||||
|
||||
**Module layout** (one module per file, re-exported from `lib.rs`):
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib.rs — crate docs, TlsError (ADR-002), re-exports
|
||||
├── identity.rs — TlsIdentity, Ed25519SecretKey, AcmeDirectory (ADR-005)
|
||||
├── credentials.rs — ConnectionCredentials, RemoteIdentity (ADR-005)
|
||||
├── fingerprint.rs — fingerprint_from_cert_der, extract_ed25519_raw_key_from_spki,
|
||||
│ DER parser (ADR-005)
|
||||
├── server.rs — TlsServerConfig, build_rustls_server_config,
|
||||
│ RawKeyCertResolver, AcceptAnyCertVerifier,
|
||||
│ SelfSignedCert / generate_self_signed_cert
|
||||
├── client.rs — TlsClientConfig, select_server_verifier,
|
||||
│ build_client_auth, RawKeyClientCertResolver,
|
||||
│ NoClientCertResolver, FingerprintPinVerifier,
|
||||
│ load_platform_root_cert_store
|
||||
├── pem.rs — load_cert_chain, load_private_key
|
||||
└── signing.rs — Ed25519SigningKey (rustls SigningKey + Signer)
|
||||
```
|
||||
|
||||
Public API surface is `lib.rs` re-exports only (AGENTS.md convention
|
||||
12); modules are `pub` for discoverability but the re-export block is
|
||||
the documented surface.
|
||||
|
||||
**Feature gates stay on types and accessors, not modules.** The
|
||||
`noq` feature gates `for_noq()` + `NoqWrap`; the `tcp` feature gates
|
||||
`for_tcp_tls()`; the `acme` feature gates the ACME branch of
|
||||
`TlsServerConfig::new` + the spawned task + the `futures` dep. Unlike
|
||||
the alktunnels/alktty backends, there are no substrate modules to
|
||||
gate — the transport-specific code is accessor-shaped.
|
||||
|
||||
**Test surface:**
|
||||
|
||||
- Port the extracted crate's in-module tests as the seed (they assert
|
||||
the behavior-preservation invariants: `max_early_data_size` on the
|
||||
server paths, `enable_early_data` on the client config, the
|
||||
nine-scheme list, resolver behavior, PEM error paths, signing).
|
||||
- Add `tests/` integration tests for the cross-module surfaces a
|
||||
single crate has: `TlsServerConfig::new` → `for_noq()` /
|
||||
`for_tcp_tls()` round-trips per identity variant;
|
||||
`TlsClientConfig` verifier selection matrix (`Some`/`None` ×
|
||||
raw-key/X.509); the root-store fallback against an empty platform
|
||||
store; `acme-tls/1` append on the ACME path only.
|
||||
- Pin the exact nine-scheme list in an integration test
|
||||
(Phase 0 OQ-TLS-05's regression-proof shape — the extracted
|
||||
in-module test only checks membership of two schemes), and pin
|
||||
`enable_early_data = true` on the client config (the client half
|
||||
of the 0-RTT invariant).
|
||||
- `TlsIdentity::Acme`'s `unreachable!` in
|
||||
`build_rustls_server_config` becomes a config error (the extracted
|
||||
code relies on an internal dispatch invariant; the port replaces
|
||||
the `unreachable!` with the `AcmeConfig` variant, keeping
|
||||
no-panics-in-library-code).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- The port is mechanically guided: module map + seed tests make the
|
||||
Phase 2 port a diff against the extracted source.
|
||||
- The invariant assertions move from "documented" to "tested" at both
|
||||
unit and integration level.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- Integration tests that wrap real QUIC endpoints are out of scope
|
||||
(they belong to transport crates); the seam tests assert config
|
||||
construction, not handshakes — the scope boundary holds.
|
||||
|
||||
## References
|
||||
|
||||
- AGENTS.md conventions 1, 9, 12 — comments, feature gates, module
|
||||
structure
|
||||
- `docs/research/phase-0.md` §Gaps #5, OQ-TLS-05 — the test-surface
|
||||
question
|
||||
- ADR-002 — `TlsError` (no `Config(String)` catch-all)
|
||||
- ADR-005 — the moved modules
|
||||
137
docs/architecture/open-questions.md
Normal file
137
docs/architecture/open-questions.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# Open Questions
|
||||
|
||||
Centralized tracker for alktls. Promoted from Phase 0
|
||||
(`docs/research/phase-0.md` OQ-TLS-01..08) on 2026-09-10. Statuses here
|
||||
are authoritative; the Phase 0 doc's statuses are the historical record.
|
||||
|
||||
## Statuses at a glance
|
||||
|
||||
| OQ | Topic | Status | Priority |
|
||||
|----|-------|--------|----------|
|
||||
| OQ-TLS-01 | Config-type ownership | **resolved** (ADR-005) | high |
|
||||
| OQ-TLS-02 | `SelfSigned` on the client path | resolved (documented behavior kept) | low |
|
||||
| OQ-TLS-03 | `for_tcp_tls()` adoption | **resolved** (ADR-004) | medium |
|
||||
| OQ-TLS-04 | Accessor borrow-vs-consume | **resolved** (ADR-004) | medium |
|
||||
| OQ-TLS-05 | Test surface | **resolved** (ADR-006) | low |
|
||||
| OQ-TLS-06 | ACME task shutdown surface | resolved (detached-only for v1) | low |
|
||||
| OQ-TLS-07 | iroh key surface | **resolved** (ADR-005, byte access pinned) | low |
|
||||
| OQ-TLS-08 | `quinn` → `noq` feature rename | **resolved** (ADR-003) | high |
|
||||
|
||||
## Identity & types
|
||||
|
||||
### OQ-TLS-01: Where do the config types live, and what moves?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Gaps #3
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: high
|
||||
- **Resolution**: alktls owns the identity types (`TlsIdentity`,
|
||||
`Ed25519SecretKey`, `AcmeDirectory`), the credential bundle
|
||||
(`ConnectionCredentials`, `RemoteIdentity`), and the fingerprint
|
||||
helpers. The auth layer (`PeerEntry`, `AuthPolicy`,
|
||||
`IdentityProvider`) stays out — peer-level identity is not TLS.
|
||||
Decision recorded in [ADR-005](decisions/005-config-types-move-into-alktls.md).
|
||||
- **Cross-references**: ADR-001, ADR-005
|
||||
|
||||
### OQ-TLS-02: `SelfSigned` on the client path — encode or document?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Gaps #4
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: low
|
||||
- **Resolution**: keep the current behavior (present nothing via
|
||||
`NoClientCertResolver`) and document it on the identity type and in
|
||||
the [client spec](client.md). Type-level enforcement (config error,
|
||||
like `Acme`) was rejected: `SelfSigned` as a *local* identity
|
||||
meaning "present nothing" is coherent — the dev cert exists for the
|
||||
server side, and presenting a self-signed client cert would add
|
||||
nothing the fingerprint path uses.
|
||||
- **Consequences**: a future dev client-auth use case would need a
|
||||
type-level change (additive, not a one-way door).
|
||||
- **Cross-references**: ADR-001 (identity model), client spec
|
||||
|
||||
### OQ-TLS-07: iroh relationship in the rewrite
|
||||
|
||||
- **Origin**: docs/research/phase-0.md OQ-TLS-07
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: low
|
||||
- **Resolution**: the requirement is pinned and verified —
|
||||
`Ed25519SecretKey` keeps 32-byte raw access (`from_bytes` /
|
||||
`as_bytes`) with the same byte-level surface iroh's
|
||||
`iroh_base::SecretKey` consumes (`from_bytes` / `to_bytes`; 32 raw
|
||||
bytes in/out — verified against iroh 1.1 in Phase 0). iroh stays
|
||||
key-not-config; no `for_iroh()`. The rewrite's iroh dial consumes
|
||||
the type alktls owns (ADR-005); no alktls-side work remains.
|
||||
- **Cross-references**: ADR-003, ADR-005
|
||||
|
||||
## API surface
|
||||
|
||||
### OQ-TLS-03: `for_tcp_tls()` — adopt the spec accessor?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Gaps #2
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: medium
|
||||
- **Resolution**: adopted — `for_tcp_tls(&self) ->
|
||||
tokio_rustls::TlsAcceptor`, feature-gated on `tcp`, infallible.
|
||||
Decision and rationale in [ADR-004](decisions/004-accessor-surface.md).
|
||||
- **Cross-references**: ADR-004
|
||||
|
||||
### OQ-TLS-04: `for_quinn()` — `self` or `&self`?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md OQ-TLS-04
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: medium
|
||||
- **Resolution**: server accessors take `&self` (one config feeds N
|
||||
transports — the ADR-082 story, directly expressible); client
|
||||
accessors consume `self` (per-dial build, zero-cost
|
||||
`into_rustls_config`). Full rationale in
|
||||
[ADR-004](decisions/004-accessor-surface.md).
|
||||
- **Cross-references**: ADR-003, ADR-004
|
||||
|
||||
### OQ-TLS-08: `quinn` feature vs `noq` feature — and the MSRV floor
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Prior art: noq
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: high
|
||||
- **Resolution**: the feature is `noq` (no `quinn` feature ever
|
||||
published; rename is pre-consumer and free). noq 1.2 pinned
|
||||
(`"1.2"`, default-features off, `rustls` feature), bump
|
||||
deliberately. The MSRV half of the OQ was separately resolved the
|
||||
same day (ecosystem floor 1.88; `time` pin dropped). Decision in
|
||||
[ADR-003](decisions/003-noq-replaces-quinn.md).
|
||||
- **Cross-references**: ADR-002, ADR-003, ADR-004
|
||||
|
||||
## Quality / process
|
||||
|
||||
### OQ-TLS-05: Test surface for the invariants
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Gaps #5
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: low
|
||||
- **Resolution**: port the in-module seed tests AND add `tests/`
|
||||
integration tests for the cross-module surfaces; pin the exact
|
||||
nine-scheme list (regression-proof). Decision in
|
||||
[ADR-006](decisions/006-module-layout-and-tests.md).
|
||||
- **Cross-references**: ADR-006
|
||||
|
||||
### OQ-TLS-06: Does the ACME state machine need a shutdown surface?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md OQ-TLS-06
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: low
|
||||
- **Resolution**: detached-only — the handle is stored (keeping
|
||||
`TlsServerConfig` non-Clone) and never aborted; the ACME task runs
|
||||
for the process lifetime. Documented on the type.
|
||||
- **Consequences**: a `shutdown()` (abort + await) surface would be
|
||||
additive and cheap if the rewrite's graceful-shutdown design wants
|
||||
one later — not a one-way door.
|
||||
- **Cross-references**: ADR-001 (one-ACME-machine rule),
|
||||
[server.md](server.md)
|
||||
|
||||
## Deferred / Blocked
|
||||
|
||||
(none — all promoted OQs are resolved; new OQs added during review
|
||||
land here with their deferral half per `docs/sdd_process.md`)
|
||||
108
docs/architecture/overview.md
Normal file
108
docs/architecture/overview.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Overview
|
||||
|
||||
## Purpose
|
||||
|
||||
alktls 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. It is the extraction of the TLS handling from alknet
|
||||
(`crates/alknet-tls` + the identity/credential/fingerprint types in
|
||||
`crates/alknet-core`), and it is the TLS crate the alknet rewrite
|
||||
consumes.
|
||||
|
||||
The crate owns **config construction**: given an identity and an ALPN
|
||||
list, produce a `rustls::ServerConfig` or `rustls::ClientConfig`, and
|
||||
hand it to whichever transport wrapper the deployment runs. It does
|
||||
not dial, accept, dispatch, or resolve peer identities — those are
|
||||
the dial seam's, the accept loop's, and the auth layer's jobs
|
||||
(alknet ADR-083/089; the scope boundary in ADR-001).
|
||||
|
||||
**Terminology.** The **assembly layer** is the deployment binary that
|
||||
builds the `TlsServerConfig`s / `TlsClientConfig`s, builds the
|
||||
transports from them, and wires the results together (alknet
|
||||
ADR-014's term — in practice, the hub/worker/endpoint binary). The
|
||||
dial seam is the outbound connection point the assembly layer (or a
|
||||
client crate) consumes; the accept loop is the inbound counterpart.
|
||||
alktls is the cert provider for both, never the loops themselves.
|
||||
|
||||
## The transport picture
|
||||
|
||||
A deployment (endpoint / hub) assembles a subset of three transports;
|
||||
alktls serves the first two directly and feeds the third a key:
|
||||
|
||||
| Transport | Stack | What alktls provides |
|
||||
|-----------|-------|----------------------|
|
||||
| TCP+TLS | `tokio-rustls` | `TlsServerConfig::for_tcp_tls()` → `TlsAcceptor` (feature `tcp`); `TlsClientConfig::into_rustls_config()` → `TlsConnector` (ungated) |
|
||||
| QUIC | `noq` (iroh's extracted quinn fork) | `for_noq()` on both configs (feature `noq`) |
|
||||
| iroh | iroh's own TLS | nothing — `Ed25519SecretKey` 32-byte access feeds `iroh_base::SecretKey` (key-not-config) |
|
||||
|
||||
One identity, N transports: the inner rustls config is Clone
|
||||
(Arc-shared resolvers); one `TlsServerConfig` feeds every transport an
|
||||
endpoint runs. One ACME state machine per domain, shared across
|
||||
transports — duplicate orders risk Let's Encrypt rate limits and
|
||||
cert-cache divergence (ADR-001).
|
||||
|
||||
## What the crate is
|
||||
|
||||
The public API surface (ADR-004, ADR-005):
|
||||
|
||||
- `TlsServerConfig` — built once from a `TlsIdentity` + ALPN list;
|
||||
accessors `for_noq()`, `for_tcp_tls()`, `rustls_config()`. Not
|
||||
`Clone` (holds the ACME task's `JoinHandle`); share via `Arc`.
|
||||
- `TlsClientConfig` — built per dial from `ConnectionCredentials` +
|
||||
ALPN; accessors `for_noq()`, `into_rustls_config()`.
|
||||
- `TlsError` — the ADR-088 six-variant `#[non_exhaustive]`
|
||||
config-construction error type (ADR-002).
|
||||
- Identity types — `TlsIdentity` (`X509` / `RawKey` / `SelfSigned` /
|
||||
`Acme`), `Ed25519SecretKey`, `AcmeDirectory` (ADR-005).
|
||||
- Credential bundle — `ConnectionCredentials`, `RemoteIdentity`
|
||||
(ADR-005); drives the verifier selection matrix (alknet ADR-034).
|
||||
- Fingerprint helpers — `fingerprint_from_cert_der`,
|
||||
`extract_ed25519_raw_key_from_spki` (ADR-005); normalized
|
||||
`ed25519:<hex>` / `SHA256:<hex>` formats.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [001](decisions/001-inherit-alknet-tls-design.md) | Inherit the alknet TLS design | The alknet ADRs + crate spec are the baseline; deviations recorded as alktls ADRs |
|
||||
| [002](decisions/002-tlserror-shape.md) | `TlsError` — ADR-088 shape from day one | Six typed variants, `#[non_exhaustive]`, no string catch-all; config-construction scope boundary |
|
||||
| [003](decisions/003-noq-replaces-quinn.md) | The QUIC feature is `noq` | noq 1.2 (iroh's extracted fork) replaces quinn pre-consumer; iroh stays key-not-config |
|
||||
| [004](decisions/004-accessor-surface.md) | Complete the accessors | `for_tcp_tls()` adopted; server accessors borrow (`&self`), client accessors consume |
|
||||
| [005](decisions/005-config-types-move-into-alktls.md) | Config types move into alktls | Identity + credentials + fingerprint move in; auth layer stays out |
|
||||
| [006](decisions/006-module-layout-and-tests.md) | Module layout and test surface | Eight modules; in-module seed tests + integration invariant pins |
|
||||
|
||||
## Open Questions
|
||||
|
||||
Open questions are tracked in [open-questions.md](open-questions.md).
|
||||
All Phase 0 questions (OQ-TLS-01..08) were resolved at Phase 1 entry
|
||||
(2026-09-10) — six via ADR-001..006, two resolved as documented
|
||||
behavior (OQ-TLS-02, OQ-TLS-06); the tracker records the resolutions
|
||||
with pointers.
|
||||
|
||||
## Components
|
||||
|
||||
- [server.md](server.md) — `TlsServerConfig`, resolvers, the ACME
|
||||
path, and the server-side invariants
|
||||
- [client.md](client.md) — `TlsClientConfig`, verifier selection,
|
||||
client-auth presentation, the root-store fallback
|
||||
|
||||
## Relationship to the alknet docs
|
||||
|
||||
The full design rationale lives in alknet's architecture docs
|
||||
(`/workspace/@alkdev/alknet/docs/architecture/`): ADR-082 (why a
|
||||
standalone TLS crate — the cert-reuse problem), ADR-083 (why the
|
||||
endpoint takes no TLS config), ADR-084 (why aws-lc-rs), ADR-027 (the
|
||||
identity model), ADR-034 (verifier selection), ADR-086 §3 (split ALPN
|
||||
lists), ADR-087 (`TlsClientConfig`), ADR-088 (`TlsError`, root-store
|
||||
fallback), ADR-089 (the dial seam), ADR-091 (`ConnectionCredentials`),
|
||||
and `crates/tls/README.md` (the full crate spec). alktls' ADR series
|
||||
records its own posture and points at the alknet reasoning rather
|
||||
than duplicating it.
|
||||
152
docs/architecture/server.md
Normal file
152
docs/architecture/server.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Server side
|
||||
|
||||
`TlsServerConfig` and its resolvers: the server-side TLS setup,
|
||||
extracted from alknet (`crates/alknet-tls/src/server.rs`) and ported
|
||||
per the ADRs. Statuses of the decisions referenced here: see
|
||||
[open-questions.md](open-questions.md) and the ADR index in
|
||||
[overview.md](overview.md).
|
||||
|
||||
## `TlsServerConfig`
|
||||
|
||||
The central server-side type. Built once from a `TlsIdentity` + ALPN
|
||||
list, shared across transports via `Arc` (not `Clone` — it holds the
|
||||
ACME task's `JoinHandle`).
|
||||
|
||||
```rust
|
||||
pub struct TlsServerConfig {
|
||||
rustls_config: rustls::ServerConfig, // Clone-safe — Arc internally
|
||||
acme_handle: Option<tokio::task::JoinHandle<()>>, // acme-gated
|
||||
}
|
||||
|
||||
impl TlsServerConfig {
|
||||
pub async fn new(identity: &TlsIdentity, alpns: &[Vec<u8>])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(&self) -> Result<noq::ServerConfig, TlsError>;
|
||||
|
||||
#[cfg(feature = "tcp")]
|
||||
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor;
|
||||
|
||||
pub fn rustls_config(&self) -> &rustls::ServerConfig;
|
||||
}
|
||||
```
|
||||
|
||||
Construction dispatch (ADR-001's identity model):
|
||||
|
||||
- `X509 { cert, key }` — loads the chain + key from disk (`pem.rs`),
|
||||
`with_single_cert`.
|
||||
- `RawKey(Ed25519SecretKey)` — `RawKeyCertResolver` presents the
|
||||
Ed25519 key as an RFC 7250 raw public key server cert.
|
||||
- `SelfSigned` — `generate_self_signed_cert()` (rcgen), in-memory.
|
||||
- `Acme { domains, cache_dir, directory, contact }` — the ACME path
|
||||
(below). **Server-only**: on the client path it is a config error.
|
||||
|
||||
## Behavior-preservation invariants
|
||||
|
||||
These are load-bearing (ADR-001); an implementation that omits any of
|
||||
them compiles but silently changes TLS behavior. Each is asserted by
|
||||
test (ADR-006):
|
||||
|
||||
- **`max_early_data_size = u32::MAX`** on every server config path —
|
||||
enables 0-RTT / early data. Omitting it silently breaks 0-RTT
|
||||
clients.
|
||||
- **`rustls::crypto::aws_lc_rs::default_provider()`** as the crypto
|
||||
provider on all paths (alknet ADR-084). Never `ring`, never the
|
||||
process-default provider, without a new ADR.
|
||||
- **`AcceptAnyCertVerifier::supported_verify_schemes()`** returns
|
||||
ED25519 + ECDSA P-256/P-384 + RSA PSS (SHA256/384/512) + RSA PKCS1
|
||||
(SHA256/384/512) — nine schemes, verbatim, pinned by an exact-list
|
||||
integration test.
|
||||
- **`acme-tls/1` ALPN append** for the ACME path only, done by the
|
||||
crate, not the caller (alknet ADR-027 §7).
|
||||
- **Non-empty root store** — the client CA path merges `webpki-roots`
|
||||
when the platform store is empty (see [client.md](client.md)).
|
||||
|
||||
## `AcceptAnyCertVerifier`
|
||||
|
||||
The server-side client-cert verifier: **request-but-don't-require**.
|
||||
It asks for a client cert (X.509 or RFC 7250 raw key) so the caller
|
||||
can extract the fingerprint via `peer_identity()`, but does not
|
||||
require one and does not verify the presented cert against a CA. The
|
||||
fingerprint is matched against peer records by the auth layer
|
||||
(`IdentityProvider::resolve_from_fingerprint`) *outside* this crate —
|
||||
the TLS crate hands over the fingerprint string; peer resolution is
|
||||
not a TLS concern (ADR-005).
|
||||
|
||||
Server-side only: this must not be reused as a client-side
|
||||
`ServerCertVerifier` — client-side verification is alknet ADR-034's
|
||||
selection matrix (see [client.md](client.md)).
|
||||
|
||||
## `RawKeyCertResolver`
|
||||
|
||||
Presents an `Ed25519SecretKey` as an RFC 7250 raw public key server
|
||||
certificate: the SPKI DER (Ed25519 OID + 32-byte key) is the "cert",
|
||||
`only_raw_public_keys() == true`, and the signing key is the shared
|
||||
`Ed25519SigningKey` helper (`signing.rs`).
|
||||
|
||||
## The ACME path
|
||||
|
||||
For `TlsIdentity::Acme`, `new` (feature `acme`):
|
||||
|
||||
1. Builds `rustls_acme::AcmeConfig` — the upstream builder, distinct
|
||||
from `TlsError::AcmeConfig` (domains, `DirCache` cache dir,
|
||||
directory URL from `AcmeDirectory`, contacts).
|
||||
2. Wires `state.resolver()` as the cert resolver into the server
|
||||
config.
|
||||
3. Appends `acme-tls/1` to the ALPN list (TLS-ALPN-01 challenge).
|
||||
4. Spawns the event-loop task (`tokio::spawn`) matching
|
||||
`EventOk`/`EventError` variants to `tracing` logs, and returns
|
||||
**immediately** — it does not await the first certificate.
|
||||
|
||||
Lifecycle semantics:
|
||||
|
||||
- The returned config is usable right away; handshakes fail
|
||||
transiently until the first order completes (or a cached cert
|
||||
deploys).
|
||||
- Order errors log at `warn!` and retry inside `rustls-acme`; the
|
||||
task exits only when the event stream ends.
|
||||
- The task is detached: the stored `JoinHandle` is never aborted;
|
||||
ACME runs for the process lifetime (OQ-TLS-06 resolved
|
||||
detached-only; a `shutdown()` surface would be additive later if
|
||||
the rewrite's graceful-shutdown design wants one).
|
||||
- **One state machine per domain** — never spawn a second ACME task
|
||||
for an already-served domain (duplicate orders = Let's Encrypt
|
||||
rate-limit risk + cert-cache divergence). This is why
|
||||
`TlsServerConfig` is not `Clone`.
|
||||
- Runtime errors are stream events, not `TlsError` variants
|
||||
(ADR-002's scope boundary). `TlsError::AcmeConfig` covers
|
||||
config-mismatch mistakes ("ACME feature not enabled but `Acme`
|
||||
configured" — and on the client path, `Acme` used for client
|
||||
auth), not runtime failures.
|
||||
|
||||
## What the server side does NOT do
|
||||
|
||||
- No accept loop: `for_tcp_tls()` yields a `TlsAcceptor`; the
|
||||
`TcpListener::accept` → `TlsAcceptor::accept` → dispatch loop
|
||||
belongs to the caller (alknet ADR-083 — the endpoint takes no TLS
|
||||
config; the assembly layer builds configs and transports).
|
||||
- No ALPN policy: the ALPN list is a parameter (the assembly layer
|
||||
filters per endpoint type — alknet ADR-086 §3); the crate appends
|
||||
only `acme-tls/1` on the ACME path.
|
||||
- No handshake: verifier selection and handshake outcomes on the
|
||||
*server* side are `AcceptAnyCertVerifier` + the caller's
|
||||
fingerprint extraction; a rejected handshake is the transport's
|
||||
error, not `TlsError`.
|
||||
- No peer resolution: the extracted fingerprint string goes to the
|
||||
caller; `PeerEntry`/`AuthPolicy` live in the auth layer.
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](overview.md) — the index; [client.md](client.md) —
|
||||
the client side
|
||||
- alknet `crates/tls/README.md` §Architecture — the full server-side
|
||||
spec this doc mirrors
|
||||
- ADR-001 (invariants), ADR-002 (`TlsError`), ADR-003 (`for_noq`),
|
||||
ADR-004 (accessors), ADR-005 (identity types), ADR-006 (modules,
|
||||
tests)
|
||||
@@ -614,8 +614,10 @@ All internal; this crate has no external research debt:
|
||||
- [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)
|
||||
- [ ] Open questions promoted to Phase 1
|
||||
`docs/architecture/open-questions.md` (Phase 1 work)
|
||||
- [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
|
||||
Reference in New Issue
Block a user