docs(arch): tighten tls/endpoint/client specs — remove connect/connect_quic, move CallCredentials to core, shed alknet-call TLS deps

Review of the three new crates (alknet-tls, alknet-endpoint, alknet-client)
+ revised core found compile-blocking inconsistencies, stale claims, and
dep-graph contradictions. All resolved:

Critical:
- C1: CallClient::connect / ChannelClient::connect_quic REMOVED (not
  delegated) — keeping them as thin wrappers over AlknetClient::dial_quic
  would make protocol crates depend on alknet-client, contradicting the
  dep graph. Callers compose dial + take-over (2 lines).
- C2: alknet-client feature gates now pull alknet-core/quinn +
  alknet-core/iroh (for Connection::from_quinn_with_alpn / from_iroh).
- C3: rustls-native-certs + webpki-roots added to alknet-tls deps
  (always-present, not feature-gated — CA-verify path is transport-agnostic).

Warning:
- W1: CallCredentials/RemoteIdentity moved to alknet-core (from
  alknet-call) — the dial must not depend on the call protocol; not a
  two-way-door, it determines the dep graph.
- W2: webpki-roots fallback implemented in spec (ADR-088 §5 added) —
  the code claimed a fallback that never existed; now the store is never
  empty, NoRootAnchors unreachable, containerized deployments work.
- W3: EndpointError removed entirely (BindFailed + HandlerNotFound both
  vestigial after ADR-083); shutdown() is now infallible.
- W4: FingerprintPinVerifier moved to alknet-tls (from alknet-call) —
  alknet-call sheds quinn/rustls/rustls-pemfile/rustls-native-certs
  entirely; CallClient becomes a pure protocol crate.

Plus: ClientError removed (only produced by removed connect); S1
(CallCredentials → ClientVerifierContext mapping + auth_token stripped
at TLS boundary documented); amendment notes on ADR-017, ADR-069,
ADR-080, ADR-082, ADR-087, ADR-090; overview crate graph + README index
updated.

29 files, consistency-reviewed.
This commit is contained in:
glm-5.2 committed 2026-07-16 11:33:18 +00:00
1 parent 34e3be2801
commit bf6ce957c0
29 files changed
+524 -267

No files matched your search

+65 -26
View File
@@ -121,7 +121,7 @@ transports the deployment runs.
| `AcceptAnyCertVerifier` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` |
| `SelfSignedCert` / `generate_self_signed_cert()` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` |
| `load_cert_chain()` / `load_private_key()` | `alknet-core/endpoint.rs` | `alknet-tls` (consolidates with the `alknet-call` duplicate — see client table) |
| `fingerprint.rs` | `alknet-core/fingerprint.rs` | **stays in core** (shared by server + client; client is in `alknet-call`; production code uses `sha2` + manual DER only — `rustls` is test-only. See OQ-59.) |
| `fingerprint.rs` | `alknet-core/fingerprint.rs` | **stays in core** (shared by server + client; the client-side `FingerprintPinVerifier` is now in `alknet-tls` per ADR-089 §5, so both consumers are co-located; production code uses `sha2` + manual DER only — `rustls` is test-only. See OQ-59 — the original dep-edge concern that motivated keeping `fingerprint.rs` in core is dissolved by ADR-089 §5.) |
### What moves from `alknet-call` to `alknet-tls` (client side)
@@ -137,12 +137,12 @@ extraction is the client-side analogue of the server-side
| `build_client_auth()` | `alknet-call/client/call_client.rs` | `alknet-tls` (client-auth cert resolver construction inside `TlsClientConfig::new`) |
| `select_server_verifier()` | `alknet-call/client/call_client.rs` | `alknet-tls` (ADR-034 verifier selection inside `TlsClientConfig::new`) |
| `load_platform_root_cert_store()` | `alknet-call/client/call_client.rs` | `alknet-tls` (the unknown-X.509-remote CA path inside `TlsClientConfig::new`) |
| `FingerprintPinVerifier` | `alknet-call/client/call_client.rs` | `alknet-tls` or stays in `alknet-call` (implementation detail — ADR-087 §5; `TlsClientConfig::new` constructs it either way) |
| `FingerprintPinVerifier` | `alknet-call/client/call_client.rs` | `alknet-tls` (moved — it is a TLS concern; `TlsClientConfig::new` constructs it; moving it lets `alknet-call` shed its direct `rustls` dep entirely per ADR-089 §5) |
| `Ed25519SigningKey` (client-side copy) | `alknet-call/client/call_client.rs` | `alknet-tls` (consolidates with the `endpoint.rs` duplicate — one copy in `alknet-tls`) |
| `RawKeyClientCertResolver` | `alknet-call/client/call_client.rs` | `alknet-tls` |
| `NoClientCertResolver` | `alknet-call/client/call_client.rs` | `alknet-tls` |
| `load_cert_chain()` / `load_private_key()` (client-side copies) | `alknet-call/client/call_client.rs` | `alknet-tls` (consolidates with the `endpoint.rs` duplicate — one copy in `alknet-tls`) |
| `CallClient::connect` | `alknet-call/client/call_client.rs` | **stays in `alknet-call`** (the dial; calls `TlsClientConfig::new` + `for_quinn()` instead of `build_quinn_client_config`) |
| `CallClient::connect` | `alknet-call/client/call_client.rs` | **removed** (ADR-089 §5 — the dial is extracted to `AlknetClient`; `CallClient` keeps only `spawn_dispatch`, shedding its TLS/transport deps) |
**Consolidation note.** `Ed25519SigningKey` and
`load_cert_chain`/`load_private_key` are currently **duplicated** across
@@ -156,11 +156,14 @@ updated to import from `alknet-tls`.
types — `StaticConfig` holds a `TlsIdentity`, and config types belong in
core. `alknet-tls` re-exports them for convenience. `fingerprint.rs` stays
in core because it's shared by both the server path (endpoint extracts
fingerprint from the client cert) and the client path (`FingerprintPinVerifier`
in `alknet-call` matches the server's cert against a pinned fingerprint).
fingerprint from the client cert) and the client path
(`FingerprintPinVerifier` — now in `alknet-tls` per ADR-089 §5 —
matches the server's cert against a pinned fingerprint).
The production code in `fingerprint.rs` uses only `sha2` and manual DER
parsing — the `rustls::sign` usage is in the test helper only. See OQ-59
for the full trade-off.
(the original dep-edge concern that motivated keeping `fingerprint.rs`
in core is dissolved by ADR-089 §5 — `FingerprintPinVerifier` moved to
`alknet-tls`, so its consumers are co-located).
### `TlsServerConfig`
@@ -319,10 +322,16 @@ present (it's the core TLS library).
```
alknet-tls
├── alknet-core (TlsIdentity, Ed25519SecretKey, fingerprint — re-exported)
├── rustls (ServerConfig, cert types — always present)
├── rustls (ServerConfig, ClientConfig, cert types — always present)
├── rustls-pki-types (CertificateDer, PrivateKeyDer, etc. — via rustls re-export
│ or direct dep; core lists it directly)
├── rustls-pemfile (cert/key file loading — always present)
├── rustls-native-certs (platform root cert store — always present; the
│ unknown-X.509-remote CA path in `TlsClientConfig::new`)
├── webpki-roots (built-in CA roots fallback — always present; merged
│ into the root store when the platform store is empty,
│ so a containerized deployment with no system CA bundle
│ can still verify public X.509 remotes — see ADR-088 §5)
├── rcgen (self-signed cert generation — always present)
├── ed25519-dalek (Ed25519 signing key — always present, via core)
├── sha2 (fingerprint computation — always present, via core)
@@ -334,6 +343,14 @@ alknet-tls
└── rustls-acme (optional — ACME state machine)
```
`rustls-native-certs` and `webpki-roots` are always-present deps (not
feature-gated) because the unknown-X.509-remote CA-verification path in
`TlsClientConfig::new` is needed by any client dialing a public X.509
endpoint, regardless of transport (QUIC or TCP+TLS). In the
pre-extraction code these lived in `alknet-call` behind the `quinn`
feature; the extraction (ADR-089 §5) moves them to `alknet-tls` ungated,
and `alknet-call` sheds the deps entirely.
`alknet-core` loses `rustls-pemfile`, `rcgen`, and `rustls-acme` from
its dependencies — the cert-loading, self-signed generation, and ACME
machinery move to `alknet-tls`. Core's `acme` feature
@@ -431,7 +448,7 @@ impl AlknetEndpoint {
);
pub async fn run(self: Arc<Self>);
pub async fn shutdown(&self) -> Result<(), EndpointError>;
pub async fn shutdown(&self);
}
```
@@ -537,11 +554,29 @@ impl TlsClientConfig {
The `ClientVerifierContext` carries the inputs to ADR-034's verifier
selection (whether a `PeerEntry` exists for the remote, the expected
fingerprint). The exact struct shape is an implementation detail; the
decisions are in ADR-034. The `TlsError` variant granularity (covering
both server and client errors) is decided — see
decisions are in ADR-034. `ClientVerifierContext` is derived from
`CallCredentials` (in `alknet-core`) at the dial site — `AlknetClient`
extracts the TLS-relevant fields (`tls_identity` → `local_identity`,
`remote_identity` → fingerprint-pin input) and builds a
`ClientVerifierContext` from those. The call-protocol `auth_token` is
**stripped at the TLS boundary** — it never reaches `TlsClientConfig` or
`ClientVerifierContext`; it travels with the `Connection` into the
protocol take-over. The `TlsError` variant granularity (covering both
server and client errors) is decided — see
[ADR-088](../../decisions/088-tlserror-shape.md) and the
[`TlsError`](#tlserror) section below.
**Root store fallback (ADR-088 §5).** The unknown-X.509-remote
CA-verification path loads the platform's native root certs
(`rustls-native-certs`). If the platform store is empty (e.g. a
containerized deployment with no system CA bundle), the built-in
`webpki-roots` are merged in so the store is never empty. This makes
the `NoRootAnchors` failure mode unreachable in practice — a
containerized worker dialing a public X.509 hub succeeds without
requiring the operator to mount a CA bundle. Native-certs *load* errors
are logged, not returned (preserved behavior); the fallback guarantees
the store is non-empty regardless. See ADR-088 §5.
`TlsClientConfig` produces a `rustls::ClientConfig`; the caller (the
transport-specific dial helper — `AlknetClient::dial_quic` /
`dial_tcp_tls`, ADR-089) passes it to the transport's connector. The
@@ -642,10 +677,11 @@ arrive asynchronously and are logged (ADR-082 §"Behavior-preservation
invariants").
**Ownership.** `TlsError` lives in `alknet-tls`, owned by the crate
that produces it. It is not re-exported from `alknet-core`; core's
`EndpointError` has no `TlsConfig` variant after ADR-083 and does not
need to know about `TlsError`. The assembly layer (hub/worker) depends
on `alknet-tls` directly and gets `TlsError` from that dependency.
that produces it. It is not re-exported from `alknet-core`; `EndpointError`
is removed entirely after ADR-083 (both variants were vestigial), so
core has no endpoint error type and does not need to know about
`TlsError`. The assembly layer (hub/worker) depends on `alknet-tls`
directly and gets `TlsError` from that dependency.
## Crate dependencies (in the dep graph)
@@ -653,11 +689,12 @@ on `alknet-tls` directly and gets `TlsError` from that dependency.
alknet-tls
├── alknet-core (TlsIdentity, Ed25519SecretKey, fingerprint)
alknet-core (loses TLS setup code)
alknet-core (loses TLS setup code + endpoint)
├── (rustls — only for fingerprint.rs types, if kept)
alknet-call (client-side verifier — unchanged)
├── alknet-core (fingerprint.rs)
alknet-call (pure protocol crate — no TLS/transport deps per ADR-089 §5)
└── alknet-core (ProtocolHandler, Connection, types; CallCredentials/RemoteIdentity
moved to core per ADR-089 §5)
alknet-hub (multi-transport endpoint)
├── alknet-tls (TlsServerConfig — shared across quinn + TCP)
@@ -696,12 +733,13 @@ All design decisions are documented as ADRs in
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-59** (resolved): `fingerprint.rs` stays in `alknet-core`. The
client-side `FingerprintPinVerifier` (in `alknet-call`) uses fingerprint
functions and must not depend on `alknet-tls` (which would pull TLS
setup infra into client-only deployments). The `rustls` dep in core is
narrow — production fingerprint code uses only `sha2` + manual DER; the
`rustls::sign` usage is a test helper. `alknet-tls` re-exports the
fingerprint functions for convenience.
client-side `FingerprintPinVerifier` (now in `alknet-tls` per
ADR-089 §5 — the original `alknet-call` → `alknet-tls` dep-edge
concern that motivated keeping `fingerprint.rs` in core is
dissolved). `fingerprint.rs` stays in core because `alknet-core`'s
own `Identity`/fingerprint code uses it; `alknet-tls` re-exports.
The `rustls` dep in core is narrow — production fingerprint code uses
only `sha2` + manual DER; the `rustls::sign` usage is a test helper.
- **OQ-60** (resolved): Where does transport construction live? The
TCP+TLS accept loop lives in `alknet-endpoint` behind a `tcp` feature
as an owned endpoint transport (`with_tcp_tls`). Builder functions are
@@ -758,9 +796,10 @@ accessors (`for_quinn`, `for_tcp_tls`, `rustls_config`); iroh is the
exception (shares the key, not the config). `AlknetClient` is the dial
that feeds them — it produces a `Connection` and the protocol
take-overs (`spawn_dispatch`, `from_connection`) consume it. The
existing `CallClient::connect` / `ChannelClient::connect_quic`
convenience constructors become thin wrappers over
`AlknetClient::dial_quic`. The `alknet/register` ALPN (native
per-protocol QUIC convenience constructors (`CallClient::connect` /
`ChannelClient::connect_quic`) are **removed** per ADR-089 §5 — the
dial is centralized in `AlknetClient`, and the protocol crates shed
their TLS/transport deps. The `alknet/register` ALPN (native
registration entry point, parallel to HTTP registration in OQ-58) is
named by ADR-089; its wire protocol is deferred (OQ-66).