- 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
129 lines
5.2 KiB
Markdown
129 lines
5.2 KiB
Markdown
---
|
|
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 |