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:
2026-09-10 05:37:55 +00:00
parent e93238cb4a
commit d74a27f764
15 changed files with 1570 additions and 246 deletions
@@ -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