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,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)
|
||||
Reference in New Issue
Block a user