docs(arch): break the AlknetClient circular hedge — TlsClientConfig not blocked on dial (ADR-087, resolves OQ-64)
OQ-64 and OQ-55 were linked in a circular dependency: the client-side TLS config was deferred behind the dial seam (OQ-55), but the dial needs the TLS config. No second transport can dial until it has a TLS config; the TLS config was deferred until a second transport dials. Schrödinger's code — required and not required until observed. ADR-087 breaks the circle by separating two concerns that were conflated as 'the same seam': 1. TlsClientConfig — rustls::ClientConfig + ADR-034 verifier selection + ADR-084 crypto provider. Transport-agnostic. All decisions made. Buildable today. A PREREQUISITE for any dial, not a consequence of it. 2. The dial (AlknetClient::dial()) — transport-specific connection establishment. Extracting a transport-polymorphic dial from one shape (QUIC) would bake QUIC in. Legitimate deferral (OQ-55, unchanged). The hub makes this non-optional: a hub dials out to workers it supervises and to other hubs (hub-as-client). The first hub deployment (web + native) dials workers over QUIC with the worker's fingerprint pinned. There is no 'later' for the TLS config — it is on the critical path for the first hub and for alknet-worker. Changes: - ADR-087: TlsClientConfig in alknet-tls, not blocked on OQ-55 - OQ-64: resolved (yes, alknet-tls provides TlsClientConfig) - OQ-55: amended — only the dial seam is deferred; TLS client config is explicitly NOT part of the deferral - TLS README: 'Server-only (for now)' section replaced with TlsClientConfig section; crate is no longer server-only - Hub README: dial/supervision section references TlsClientConfig for outbound connections
This commit is contained in:
1 parent
7d9b1ebad9
commit
77321a7e84
8 files changed
+502
-140
No files matched your search
@@ -282,8 +282,12 @@ all.
|
||||
#### Dial (outbound workers) — transport-agnostic
|
||||
|
||||
The hub dials outbound workers via `ChannelClient`, not `CallClient`.
|
||||
The dial path mirrors the `from_connection` / `connect_quic` split
|
||||
(ADR-080):
|
||||
The hub is a client when it dials out — a hub (B) that connects to
|
||||
another hub (A) is a client from A's perspective. The dial needs a
|
||||
client-side TLS config (`TlsClientConfig`, ADR-087) for the outbound
|
||||
connection's `rustls::ClientConfig` (verifier selection per ADR-034:
|
||||
fingerprint pin for the worker's known key). The dial path mirrors the
|
||||
`from_connection` / `connect_quic` split (ADR-080):
|
||||
|
||||
```rust
|
||||
impl Hub {
|
||||
@@ -299,8 +303,9 @@ impl Hub {
|
||||
) -> Result<(PeerId, ChannelClient), HubError>;
|
||||
|
||||
/// QUIC convenience: dial a worker over QUIC, then
|
||||
/// `dial_worker_connection`. Two-way door — additive over
|
||||
/// `dial_worker_connection`.
|
||||
/// `dial_worker_connection`. Builds a `TlsClientConfig` (ADR-087)
|
||||
/// with the worker's fingerprint pinned (ADR-034), dials QUIC,
|
||||
/// wraps as a channels `Connection`, calls `dial_worker_connection`.
|
||||
pub async fn connect_quic_worker(
|
||||
&self,
|
||||
addr: SocketAddr,
|
||||
@@ -315,11 +320,12 @@ taking over the `Connection`, it runs `from_call` on channel 0 to
|
||||
discover the worker's operations, registers the discovered bundles in
|
||||
the connection's Layer 2 overlay, and attaches the peer to the
|
||||
aggregated env. `connect_quic_worker` is the "I just want QUIC"
|
||||
convenience — it calls `ChannelClient::connect_quic`, then
|
||||
`dial_worker_connection`. A future `connect_tcp_tls_worker` dials
|
||||
TCP+TLS and calls `dial_worker_connection` the same way. The
|
||||
one-way-door surface is `dial_worker_connection`; the dial helpers
|
||||
are two-way-door conveniences.
|
||||
convenience — it builds a `TlsClientConfig` (ADR-087) with the
|
||||
worker's fingerprint pinned, calls `ChannelClient::connect_quic`, then
|
||||
`dial_worker_connection`. A future `connect_tcp_tls_worker` builds a
|
||||
`TlsClientConfig` and dials TCP+TLS the same way. The one-way-door
|
||||
surface is `dial_worker_connection`; the dial helpers are two-way-door
|
||||
conveniences.
|
||||
|
||||
#### Accept (inbound workers and browsers) — transport-agnostic
|
||||
|
||||
@@ -780,6 +786,7 @@ into `CallAdapter::with_aggregated_env`.
|
||||
| Channel 0 pre-negotiated | [ADR-072](../../decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 = `alknet/call`; the `CallAdapter` runs here |
|
||||
| Channel lifecycle operations | [ADR-073](../../decisions/073-channel-lifecycle-operations.md) | `channel/open`/`close`/`control`/`resources/subscribe` — what the hub translates |
|
||||
| Endpoint types and entry points | [ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type |
|
||||
| `TlsClientConfig` for outbound dials | [ADR-087](../../decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `alknet-tls` provides client-side TLS config; hub-as-client is a first-class use case; not blocked on the dial-seam extraction (OQ-55) |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -843,6 +850,7 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
- ADR-082: alknet-tls extraction (`TlsServerConfig` — shared across quinn + TCP+TLS)
|
||||
- ADR-083: Endpoint as multi-transport accept-loop runner (`with_tcp_tls` — TCP+TLS owned by the endpoint; the hub composes transports and handlers)
|
||||
- ADR-086: Endpoint types and entry points (web/native/iroh; entry-point vs. endpoint; split ALPN lists per endpoint type)
|
||||
- ADR-087: `TlsClientConfig` not blocked on dial seam (client-side TLS config; hub-as-client requirement)
|
||||
- alkapi [hub.md](/workspace/@alkdev/alkapi/docs/architecture/hub.md) —
|
||||
the first hub consumer, the concrete use case that informed this
|
||||
crate
|
||||
|
||||
@@ -5,12 +5,15 @@ last_updated: 2026-07-15
|
||||
|
||||
# alknet-tls
|
||||
|
||||
Shared TLS configuration and certificate management. Builds a
|
||||
`rustls::ServerConfig` (or an ACME state machine + cert resolver) once,
|
||||
and shares it across multiple transports — quinn, `tokio-rustls`
|
||||
(TCP+TLS), and iroh — so one certificate identity serves QUIC and TCP
|
||||
endpoints simultaneously. One ACME state machine, one cert, N
|
||||
transports.
|
||||
Shared TLS configuration and certificate management — server and
|
||||
client. Builds a `rustls::ServerConfig` (or an ACME state machine +
|
||||
cert resolver) once and shares it across multiple transports — quinn,
|
||||
`tokio-rustls` (TCP+TLS), and iroh — so one certificate identity serves
|
||||
QUIC and TCP endpoints simultaneously. Builds a `rustls::ClientConfig`
|
||||
with ADR-034 verifier selection and ADR-084 crypto provider, shared
|
||||
across all outbound-dialing crates (hub, worker, `CallClient`,
|
||||
`ChannelClient`). One ACME state machine, one cert, N transports;
|
||||
one verifier rule, N clients.
|
||||
|
||||
## What
|
||||
|
||||
@@ -388,33 +391,62 @@ behind a `tcp` feature, as an owned transport on `AlknetEndpoint` (via
|
||||
cert provider, not the accept loop. This keeps `alknet-tls` focused on
|
||||
TLS setup and cert sharing, not transport accept logic.
|
||||
|
||||
### Server-only (for now) — client-side config is a separate question
|
||||
### Client-side — `TlsClientConfig` (ADR-087)
|
||||
|
||||
`alknet-tls` as specified here is **server-side only**:
|
||||
`TlsServerConfig` and its accessors (`for_quinn`, `for_tcp_tls`,
|
||||
`rustls_config`) produce `rustls::ServerConfig`-shaped outputs for
|
||||
inbound listeners. There is no `TlsClientConfig`, no `for_client()`,
|
||||
no client-side cert/verifier helper in this crate.
|
||||
`alknet-tls` provides a client-side config alongside `TlsServerConfig`.
|
||||
A hub dials out to workers it supervises and to other hubs
|
||||
(hub-as-client); `alknet-worker` dials a hub. Both need a
|
||||
`rustls::ClientConfig` with ADR-034's verifier selection and ADR-084's
|
||||
crypto provider. `TlsClientConfig` centralizes this — it is not a
|
||||
future extraction deferred behind the dial seam (OQ-55); it is a
|
||||
present prerequisite for the first hub deployment.
|
||||
|
||||
The client side — the `rustls::ClientConfig` construction used by
|
||||
`CallClient` / `ChannelClient` for outbound dials, including
|
||||
[ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md)'s
|
||||
verifier-selection rule (fingerprint pin for known peers, CA-verify for
|
||||
unknown X.509, fail-closed for unknown raw-key) — currently lives in
|
||||
`alknet-call`'s `FingerprintPinVerifier`. ADR-084 requires the client
|
||||
side to use the same `aws_lc_rs` provider; today that is enforced by
|
||||
**convention** (two crates independently constructing
|
||||
`aws_lc_rs::default_provider()`), not by shared code.
|
||||
```rust
|
||||
pub struct TlsClientConfig {
|
||||
config: rustls::ClientConfig,
|
||||
}
|
||||
|
||||
Whether `alknet-tls` should grow a client-side helper (a
|
||||
`TlsClientConfig` that centralizes verifier selection + provider
|
||||
consistency, so `alknet-call` / a future `AlknetClient` don't each
|
||||
rebuild it) is **deferred** — see OQ-64. The deferral blocks on the
|
||||
`AlknetClient` dial-seam extraction (OQ-55): the client-side TLS helper
|
||||
and the shared dial are the same seam, and extracting either without a
|
||||
second transport's real client dial would bake QUIC in as the client
|
||||
shape — the same welding ADR-065 unwound on the server side. The
|
||||
provider-consistency convention (ADR-084) holds until then.
|
||||
impl TlsClientConfig {
|
||||
/// Build a client TLS config for the given remote identity context.
|
||||
/// Applies ADR-034 verifier selection:
|
||||
/// - known peer (PeerEntry present) + raw key → fingerprint pin
|
||||
/// - known peer (PeerEntry present) + X.509 → fingerprint pin
|
||||
/// - unknown remote + X.509 → CA verification (WebPkiServerVerifier)
|
||||
/// - unknown remote + raw key → fail closed
|
||||
/// Applies ADR-084 crypto provider (aws_lc_rs::default_provider()).
|
||||
pub fn new(verifier_context: ClientVerifierContext) -> Result<Self, TlsError>;
|
||||
}
|
||||
```
|
||||
|
||||
The `ClientVerifierContext` carries the inputs to ADR-034's verifier
|
||||
selection (whether a `PeerEntry` exists for the remote, the expected
|
||||
fingerprint, the remote cert type). The exact struct shape is an
|
||||
implementation detail; the decisions are in ADR-034. The full
|
||||
`TlsError` variant granularity (now covering both server and client
|
||||
errors) is OQ-63 (next session).
|
||||
|
||||
`TlsClientConfig` produces a `rustls::ClientConfig`; the caller (the
|
||||
transport-specific dial helper — `CallClient::connect_quic`, a future
|
||||
`connect_tcp_tls`, etc.) passes it to the transport's connector. The
|
||||
config is transport-agnostic; the dial is not. This is the client-side
|
||||
analogue of ADR-065's server-side separation: the take-over
|
||||
(`spawn_dispatch` / `from_connection`, transport-agnostic) is built
|
||||
now; the dial (transport-specific) is per-transport. The
|
||||
transport-polymorphic dial extraction (`AlknetClient::dial()`) remains
|
||||
deferred (OQ-55) — it is about picking the transport and calling the
|
||||
right connector, not about the TLS config.
|
||||
|
||||
### Iroh — shares the key, not the config (client side too)
|
||||
|
||||
Iroh's client side, like its server side (above), does not consume a
|
||||
`rustls::ClientConfig` — it takes an `iroh::SecretKey` and handles TLS
|
||||
internally. The iroh client dial does not use `TlsClientConfig`. The
|
||||
verifier selection for iroh is fingerprint-pinning by another name:
|
||||
iroh's built-in TLS verifies the remote's `NodeId` (Ed25519 public key)
|
||||
against the expected `NodeId`. An unknown iroh remote fails closed
|
||||
(ADR-034 §3, Assumption 1 — no CA to fall back to). The iroh dial
|
||||
helper applies the same ADR-034 rule via iroh's own API; the
|
||||
consistency is in the rule, not in the type.
|
||||
|
||||
## Crate dependencies (in the dep graph)
|
||||
|
||||
@@ -455,6 +487,7 @@ All design decisions are documented as ADRs in
|
||||
| [083](../../decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as multi-transport accept-loop runner | `AlknetEndpoint` takes no TLS config; TCP+TLS is an owned transport (`with_tcp_tls`); `dispatch` public for SSH/WT; `acme-tls/1` guard moves to shared `dispatch` |
|
||||
| [084](../../decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs crypto provider | `rustls::crypto::aws_lc_rs::default_provider()` on all server + client config paths; matches iroh; FIPS-capable; do not switch to `ring` or process-default without a new ADR |
|
||||
| [086](../../decisions/086-endpoint-types-and-entry-points.md) | Endpoint types and entry points | Three endpoint types (web/native/iroh); split ALPN lists per endpoint type (resolves OQ-62); entry-point vs. endpoint ALPN distinction |
|
||||
| [087](../../decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` not blocked on dial seam | `alknet-tls` provides `TlsClientConfig` (client-side); not deferred behind OQ-55; breaks the circular hedge; hub-as-client is a first-class use case |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -484,12 +517,13 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
- **OQ-63** (open): `TlsError` shape — the error type is referenced in
|
||||
public signatures but not sketched. Needs a variant-granularity
|
||||
decision (single enum vs thin wrapper) before implementation.
|
||||
- **OQ-64** (deferred): Client-side TLS config helper. `alknet-tls` is
|
||||
server-side only as specified; the client-side `ClientConfig` +
|
||||
verifier selection lives in `alknet-call`. Blocked on the
|
||||
`AlknetClient` dial-seam extraction (OQ-55) — extracting the client
|
||||
helper without a second transport's dial would bake QUIC in as the
|
||||
client shape.
|
||||
- **OQ-64** (resolved): `alknet-tls` provides `TlsClientConfig`
|
||||
(ADR-087). Not blocked on the dial-seam extraction (OQ-55) — the TLS
|
||||
config is a prerequisite for the dial, not a consequence of it.
|
||||
Centralizes ADR-034 verifier selection + ADR-084 provider; the
|
||||
hub-as-client requirement makes it a prerequisite for the first hub
|
||||
deployment. The dial seam (OQ-55) remains deferred; the TLS config
|
||||
does not.
|
||||
|
||||
## References
|
||||
|
||||
@@ -507,6 +541,9 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
- `docs/architecture/decisions/086-endpoint-types-and-entry-points.md`
|
||||
— three endpoint types (web/native/iroh); split ALPN lists per
|
||||
endpoint type (resolves OQ-62); entry-point vs. endpoint distinction
|
||||
- `docs/architecture/decisions/087-tlsclientconfig-not-blocked-on-dial.md`
|
||||
— `TlsClientConfig` (client-side); not blocked on the dial seam;
|
||||
breaks the circular hedge; hub-as-client requirement
|
||||
- `docs/architecture/crates/core/endpoint.md` — current endpoint design
|
||||
(TLS section will be amended to point to `alknet-tls`)
|
||||
- `docs/architecture/crates/core/config.md` — `TlsIdentity`, `StaticConfig`
|
||||
|
||||
Reference in new issue
Block a user