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,135 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Client side
|
||||
|
||||
`TlsClientConfig` and its verifiers: the client-side TLS setup,
|
||||
extracted from alknet (`crates/alknet-tls/src/client.rs`) and ported
|
||||
per the ADRs. Decisions referenced here: see
|
||||
[open-questions.md](open-questions.md) and the index in
|
||||
[overview.md](overview.md).
|
||||
|
||||
## `TlsClientConfig`
|
||||
|
||||
Built **per dial** from a `ConnectionCredentials` + ALPN (the
|
||||
dial-seam pattern, alknet ADR-089). Consumed by its accessors — a
|
||||
`TlsClientConfig` is not reused across dials (ADR-004).
|
||||
|
||||
```rust
|
||||
pub struct TlsClientConfig {
|
||||
rustls_config: rustls::ClientConfig,
|
||||
}
|
||||
|
||||
impl TlsClientConfig {
|
||||
pub fn new(credentials: &ConnectionCredentials, alpn: &[u8])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(self) -> Result<noq::ClientConfig, TlsError>;
|
||||
|
||||
pub fn into_rustls_config(self) -> rustls::ClientConfig;
|
||||
}
|
||||
```
|
||||
|
||||
`new` is sync and infallible-free aside from config construction
|
||||
(cert file loading for an X.509 local identity is the only I/O).
|
||||
Every config sets `enable_early_data = true` (the client half of the
|
||||
0-RTT invariant) and carries the aws-lc-rs provider (alknet ADR-084).
|
||||
|
||||
## The two credential dimensions
|
||||
|
||||
`ConnectionCredentials` (ADR-005, moved from alknet ADR-091) carries
|
||||
exactly the two inputs the client config consumes:
|
||||
|
||||
1. **`local_identity: Option<TlsIdentity>`** — the client-auth cert
|
||||
presentation:
|
||||
|
||||
| Local identity | Presented |
|
||||
|----------------|-----------|
|
||||
| `RawKey` | RFC 7250 raw public key (SPKI DER; `only_raw_public_keys()` auto-detected from the DER) |
|
||||
| `X509` | the cert chain + key, loaded from disk |
|
||||
| `SelfSigned` / `None` | nothing (`NoClientCertResolver`) — documented, resolved in OQ-TLS-02 |
|
||||
| `Acme` | **config error** (`TlsError::AcmeConfig`) — server-only identity |
|
||||
|
||||
2. **`remote_identity: Option<RemoteIdentity>`** — the verifier
|
||||
selection matrix (below). Both `Option`s are load-bearing, not
|
||||
cosmetic: `Some` means "pin this", `None` means "trust the CA or
|
||||
fail" — never a placeholder default.
|
||||
|
||||
## Verifier selection (alknet ADR-034 §3)
|
||||
|
||||
Exactly three outcomes, driven by `remote_identity`:
|
||||
|
||||
| `remote_identity` | Remote cert | Verifier | Outcome |
|
||||
|-------------------|-------------|----------|---------|
|
||||
| `Some(fingerprint)` | any | `FingerprintPinVerifier` | pin match required |
|
||||
| `None` | X.509 | `WebPkiServerVerifier` (CA) | CA verification against the platform root store |
|
||||
| `None` | Ed25519 raw key | (the CA verifier fails) | **fail closed at handshake** |
|
||||
|
||||
`None` is the public-X.509-endpoint state, **not** "skip
|
||||
verification". An unknown raw-key remote fails at handshake — it must
|
||||
never silently downgrade to CA verification. Verifier selection
|
||||
happens at config construction; the fail-closed *manifests* at
|
||||
handshake time (the config carries the CA verifier; the raw-key
|
||||
remote simply cannot satisfy it) — this is the ADR-002 scope
|
||||
boundary in action.
|
||||
|
||||
**Fail-closed is structural**: known peer + fingerprint → pin;
|
||||
unknown + X.509 → CA; unknown + raw key → fail. No fourth path.
|
||||
|
||||
## `FingerprintPinVerifier`
|
||||
|
||||
The known-peer path. The fingerprint IS the trust anchor: for
|
||||
`ed25519:<hex>` remotes the raw Ed25519 key is extracted from the
|
||||
presented cert (SPKI) and matched; for `SHA256:<hex>` remotes the
|
||||
full cert DER is hashed and matched. No CA verification, no name
|
||||
verification — only the pin.
|
||||
|
||||
Handshake signatures are still verified (TLS 1.2/1.3, aws-lc-rs
|
||||
algorithms; Ed25519 SPKI certs route through
|
||||
`verify_tls13_signature_with_raw_key`): a stolen fingerprint cannot
|
||||
be replayed with a forged signature — the presenter must prove
|
||||
possession of the corresponding private key.
|
||||
|
||||
## The root-store fallback (alknet ADR-088 §5)
|
||||
|
||||
The `None` + X.509 CA path loads the platform's native root certs
|
||||
(`rustls-native-certs`). If the platform store is empty — a
|
||||
containerized deployment with no system CA bundle — the built-in
|
||||
`webpki-roots` are merged in so the store is **never empty**.
|
||||
Native-certs *load* errors are logged, not returned; the fallback
|
||||
guarantees non-emptiness regardless. This makes `NoRootAnchors`
|
||||
unreachable in practice (the `VerifierBuild` variant exists for the
|
||||
builder API's completeness, not a reachable path — ADR-002).
|
||||
|
||||
The root store is built unconditionally for the CA path — not
|
||||
feature-gated on `tcp` or `noq` — because any client dialing a public
|
||||
X.509 endpoint needs it regardless of transport.
|
||||
|
||||
## What the client side does NOT do
|
||||
|
||||
- No dial: the dial seam (`AlknetClient` in alknet; the rewrite's
|
||||
dial crate) consumes the config via `for_noq()` /
|
||||
`into_rustls_config()` and owns `TcpStream::connect`,
|
||||
`Endpoint::connect`, SNI, and SOCKS5 proxying (alknet ADR-089;
|
||||
SOCKS5 proxy seam per alknet ADR-090).
|
||||
Handshake failures are the dial's errors (`ClientDialError`),
|
||||
not `TlsError`.
|
||||
- No iroh: the iroh dial takes the 32-byte Ed25519 key directly
|
||||
(key-not-config; ADR-003). iroh's built-in verifier (NodeId check)
|
||||
is iroh's own application of the same fail-closed rule.
|
||||
- No per-request auth: a call-protocol `auth_token` is a call-layer
|
||||
concept; it never reaches `TlsClientConfig`.
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](overview.md) — the index; [server.md](server.md) —
|
||||
the server side
|
||||
- alknet `crates/tls/README.md` §Client side — the full client-side
|
||||
spec this doc mirrors
|
||||
- alknet ADR-034 (verifier selection), ADR-088 §5 (root-store
|
||||
fallback), ADR-091 (`ConnectionCredentials`)
|
||||
- ADR-002 (`TlsError`), ADR-004 (accessors), ADR-005 (credential
|
||||
types), ADR-006 (tests — the verifier-selection matrix)
|
||||
Reference in New Issue
Block a user