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:
@@ -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
|
||||
Reference in New Issue
Block a user