repo scaffold + AGENTS.md + Phase 0 research

- Cargo scaffold: feature gates (quinn/tcp/acme), lean tokio subset,
  placeholder lib; Cargo.lock committed with time pinned to 0.3.36 so
  rust-version = 1.85 is actually satisfiable (rcgen's default time
  resolution requires 1.88 — alknet-tls fails the same check)
- AGENTS.md adapted from alktunnels: TLS-crate conventions (behavior-
  preservation invariants, fail-closed verifier selection, one ACME
  state machine, config-construction scope boundary, no wasm target)
- .opencode/agents: implementation-specialist conventions + coordinator
  prompt template + architect deferral examples updated for alktls
- docs/research/phase-0.md: extraction inventory with verified
  invariants (line-referenced), spec-vs-code gaps (TlsError shape,
  for_tcp_tls, config-type ownership), rewrite requirements,
  OQ-TLS-01..07, MSRV verification record

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