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
@@ -286,6 +286,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|
||||
| [084](decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs as the TLS Crypto Provider | Accepted |
|
||||
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Accepted |
|
||||
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Accepted |
|
||||
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
# ADR-087: `TlsClientConfig` Is Not Blocked on the Dial Seam
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (resolves OQ-64)
|
||||
|
||||
## Context
|
||||
|
||||
### The circular hedge
|
||||
|
||||
OQ-64 and OQ-55 were linked in a way that formed a circular dependency:
|
||||
|
||||
- **OQ-64** said: "the client-side TLS helper is blocked on the
|
||||
`AlknetClient` dial-seam extraction (OQ-55), because the TLS helper
|
||||
and the dial are the same seam."
|
||||
- **OQ-55** said: "the dial seam is blocked on a second transport's
|
||||
real dial existing."
|
||||
- A second transport's dial requires a `rustls::ClientConfig` to dial
|
||||
with — which is the client-side TLS helper.
|
||||
|
||||
This is circular: the prerequisite (TLS config) is deferred behind the
|
||||
thing that needs it (the dial). No second transport can dial until it
|
||||
has a TLS config; the TLS config is deferred until a second transport
|
||||
dials. The dial never arrives because the config never arrives because
|
||||
the dial never arrives. Schrödinger's code — required and not required
|
||||
until observed.
|
||||
|
||||
This is the same pattern that stalled the server-side transport
|
||||
generalization before ADR-065 broke it: "we can't generalize until we
|
||||
have two transports, but we can't build the second transport until we
|
||||
generalize." ADR-065 broke it by separating the *Connection* (the
|
||||
take-over, transport-agnostic, built now) from the *dial* (the
|
||||
establishment, transport-specific, per-transport). This ADR does the
|
||||
same on the client side.
|
||||
|
||||
### The two things that were conflated
|
||||
|
||||
The "same seam" claim conflated two distinct concerns:
|
||||
|
||||
1. **`TlsClientConfig`** — the `rustls::ClientConfig` + ADR-034
|
||||
verifier selection (fingerprint pin for known peers, CA-verify for
|
||||
unknown X.509, fail-closed for unknown raw-key) + ADR-084 crypto
|
||||
provider (`aws_lc_rs`). This is **transport-agnostic**. The verifier
|
||||
selection rule (ADR-034 §3) is keyed on `PeerEntry` presence and
|
||||
remote cert type, not on transport. The crypto provider (ADR-084) is
|
||||
the same on all paths. The fingerprint normalization (ADR-030 §6,
|
||||
`ed25519:<hex>` / `SHA256:<hex>`) is transport-agnostic. All
|
||||
decisions are made. There is nothing to discover from a second
|
||||
transport's dial — the rule is the same regardless of whether the
|
||||
dial is QUIC, TCP+TLS, or iroh.
|
||||
|
||||
2. **The dial** (`AlknetClient::dial()`) — transport-specific
|
||||
connection establishment. QUIC dial (`quinn::Endpoint::connect`),
|
||||
TCP+TLS dial (`TcpStream::connect` + `TlsConnector::connect`), iroh
|
||||
dial (`iroh::Endpoint::connect`). Extracting a transport-polymorphic
|
||||
dial from one shape (QUIC) would bake QUIC in as *the* establishment
|
||||
shape — the same welding ADR-065 unwound on the server side. **This
|
||||
is the legitimate deferral** (OQ-55, unchanged).
|
||||
|
||||
The TLS config is a **prerequisite** for the dial, not a consequence of
|
||||
it. You build the `rustls::ClientConfig` first, then you dial with it.
|
||||
The dial passes the config to the transport-specific connector
|
||||
(`quinn::Endpoint::connect_with` takes a `ClientConfig`;
|
||||
`TlsConnector::connect` takes a `ClientConfig`; iroh takes a
|
||||
`SecretKey` — the one exception, see "Iroh" below). The config does not
|
||||
flow *from* the dial; it flows *into* it.
|
||||
|
||||
### The hub makes this non-optional
|
||||
|
||||
A hub **has to** be a client. The hub dials out to workers it
|
||||
supervises; a hub (B) that connects to another hub (A) is a client
|
||||
from A's perspective. The hub README already has
|
||||
`dial_worker_connection` and `supervise_worker` — those are client
|
||||
operations that need a client-side TLS config. The first hub deployment
|
||||
(web + native, per ADR-086) dials workers over QUIC (native endpoint,
|
||||
raw key). That dial needs a `rustls::ClientConfig` with the ADR-034
|
||||
verifier (fingerprint pin for the worker's known Ed25519 key).
|
||||
|
||||
There is no "later" for this. The first hub deployment needs the
|
||||
client-side TLS config. Deferring it behind the dial-seam extraction
|
||||
(OQ-55, which is genuinely blocked on a second transport) means the
|
||||
first hub cannot be built — or, worse, each client rebuilds the
|
||||
verifier selection + provider wiring standalone, and the duplicated
|
||||
boilerplate drifts (one crate uses `aws_lc_rs`, another uses `ring`,
|
||||
the convention breaks silently — exactly the ADR-084 consistency risk
|
||||
the convention was supposed to prevent).
|
||||
|
||||
`alknet-worker` cannot exist without a client (it dials a hub). The
|
||||
hub cannot exist without a client (it dials workers and other hubs).
|
||||
The client-side TLS config is on the critical path for both. It is not
|
||||
a future extraction; it is a present prerequisite.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. `alknet-tls` provides `TlsClientConfig`
|
||||
|
||||
`alknet-tls` grows a client-side config type alongside
|
||||
`TlsServerConfig`:
|
||||
|
||||
```rust
|
||||
pub struct TlsClientConfig {
|
||||
config: rustls::ClientConfig,
|
||||
}
|
||||
|
||||
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 (if known), and the remote cert type (if known). The exact
|
||||
shape of this context is an implementation detail (the decisions are
|
||||
in ADR-034; the struct is a bag of already-decided inputs). It is
|
||||
sketched lightly here; the full variant-granularity of `TlsError` is
|
||||
OQ-63 (the next session).
|
||||
|
||||
This is **not** the dial. `TlsClientConfig` produces a
|
||||
`rustls::ClientConfig`; the caller (the transport-specific dial helper,
|
||||
or `CallClient::connect_quic`, or a future `connect_tcp_tls`) passes
|
||||
it to the transport's connector. The config is transport-agnostic; the
|
||||
dial is not.
|
||||
|
||||
### 2. The dial seam (OQ-55) is unaffected
|
||||
|
||||
OQ-55 (the `AlknetClient` transport-polymorphic dial extraction)
|
||||
remains deferred. The deferral is about the *dial* —
|
||||
transport-specific connection establishment — not about the TLS config.
|
||||
With `TlsClientConfig` in `alknet-tls`, each transport-specific dial
|
||||
helper (`CallClient::connect_quic`, a future `connect_tcp_tls`, a
|
||||
future `connect_iroh`) builds its `TlsClientConfig` and passes it to
|
||||
its transport's connector. The friction (each dial helper calls
|
||||
`TlsClientConfig::new` + its transport's connect) is real but narrow —
|
||||
it's duplicated `TlsClientConfig::new` calls, not duplicated verifier
|
||||
selection logic. When a second transport's dial exists, the dial
|
||||
seam is extractable (OQ-55 unblocked); the TLS config is already
|
||||
shared by then.
|
||||
|
||||
### 3. Iroh is the one exception (shares the key, not the config)
|
||||
|
||||
Iroh's client side, like its server side (ADR-082 §"Iroh: shares the
|
||||
key, not the rustls config"), does not consume a `rustls::ClientConfig`
|
||||
— it takes an `iroh::SecretKey` and handles TLS internally. The iroh
|
||||
client dial does not use `TlsClientConfig`. The `Ed25519SecretKey`
|
||||
(from `StaticConfig`, in core) feeds `iroh::SecretKey::from_bytes`
|
||||
directly, same as the server side.
|
||||
|
||||
The verifier selection for iroh is also different: iroh's built-in TLS
|
||||
verifies the remote's `NodeId` (Ed25519 public key) against the
|
||||
expected `NodeId`. This is fingerprint-pinning by another name — the
|
||||
`NodeId` IS the fingerprint. An unknown iroh remote fails closed (no
|
||||
CA to fall back to — ADR-034 §3, Assumption 1). `TlsClientConfig` does
|
||||
not cover the iroh path; the iroh dial helper applies the same
|
||||
ADR-034 rule (known peer → pin, unknown → fail closed) via iroh's own
|
||||
API.
|
||||
|
||||
### 4. `alknet-tls` is no longer "server-only"
|
||||
|
||||
The TLS README's "Server-only (for now)" section is removed.
|
||||
`alknet-tls` provides both `TlsServerConfig` (inbound) and
|
||||
`TlsClientConfig` (outbound). The server side is unchanged (ADR-082);
|
||||
the client side is added by this ADR.
|
||||
|
||||
The provider-consistency convention (ADR-084: `aws_lc_rs` on all
|
||||
paths) moves from "enforced by convention" to "enforced by
|
||||
`TlsClientConfig::new`" for the rustls-consuming transports (quinn,
|
||||
TCP+TLS). The iroh path uses iroh's built-in `tls-aws-lc-rs` feature
|
||||
(already consistent).
|
||||
|
||||
### 5. The hub-as-client requirement is a first-class use case
|
||||
|
||||
The hub's `dial_worker_connection` / `supervise_worker` (hub README
|
||||
§"Dial (outbound workers)") are client operations. They need a
|
||||
`TlsClientConfig` for the outbound dial. The hub-as-client case is
|
||||
not a "future use case the resolved helper must cover" — it is a
|
||||
present requirement that drives the resolution. A hub that supervises
|
||||
workers dials them over the native endpoint (QUIC, raw key) using a
|
||||
`TlsClientConfig` with the worker's fingerprint pinned (ADR-034 §3,
|
||||
known peer + raw key). A hub that connects to another hub dials it
|
||||
the same way.
|
||||
|
||||
## What this does NOT change
|
||||
|
||||
- **`TlsServerConfig` (ADR-082)** — the server-side config is
|
||||
unchanged. `TlsClientConfig` is a separate type, same crate.
|
||||
- **The dial seam (OQ-55)** — the transport-polymorphic dial extraction
|
||||
remains deferred. This ADR extracts the TLS config, not the dial.
|
||||
When OQ-55 resolves, `AlknetClient::dial()` will call
|
||||
`TlsClientConfig::new` + the transport-specific connector; the
|
||||
config is already shared by then.
|
||||
- **ADR-034 (verifier selection)** — the rule is unchanged. This ADR
|
||||
centralizes its implementation in `TlsClientConfig::new` instead of
|
||||
each client rebuilding it.
|
||||
- **ADR-084 (crypto provider)** — the provider is unchanged. This ADR
|
||||
moves enforcement from convention to code for the client side.
|
||||
- **`CallClient` / `ChannelClient` take-over APIs** —
|
||||
`spawn_dispatch` / `from_connection` are transport-agnostic and
|
||||
decided (ADR-017, ADR-080). They take a pre-established `Connection`.
|
||||
This ADR is about how the caller builds the TLS config *before*
|
||||
establishing that `Connection`, not about the take-over.
|
||||
- **`FingerprintPinVerifier`** — the existing verifier in
|
||||
`alknet-call` is the current implementation of ADR-034's
|
||||
fingerprint-pin path. `TlsClientConfig::new` centralizes the
|
||||
verifier construction (including the CA-verify and fail-closed
|
||||
paths that `FingerprintPinVerifier` does not cover). The
|
||||
`FingerprintPinVerifier` type may move to `alknet-tls` or stay in
|
||||
`alknet-call` and be constructed by `TlsClientConfig::new` — an
|
||||
implementation detail, not an architecture decision.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- The circular dependency is broken. `TlsClientConfig` is buildable
|
||||
today; the dial seam (OQ-55) is no longer blocking it. A second
|
||||
transport's dial can be built using `TlsClientConfig` + the
|
||||
transport's connector, without waiting for the dial-seam extraction.
|
||||
- The hub-as-client requirement is met. The hub's
|
||||
`dial_worker_connection` / `supervise_worker` use
|
||||
`TlsClientConfig::new` for the outbound dial's TLS config. The first
|
||||
hub deployment (web + native) can dial workers over QUIC with the
|
||||
worker's fingerprint pinned.
|
||||
- `alknet-worker` is unblocked on the TLS front. A worker dials a hub
|
||||
using `TlsClientConfig::new` + the transport-specific connector. The
|
||||
dial seam (OQ-55) is about extracting the shared dial, not about
|
||||
blocking the worker from dialing.
|
||||
- Provider consistency (ADR-084) is enforced by code, not convention,
|
||||
for the client side. `TlsClientConfig::new` uses
|
||||
`aws_lc_rs::default_provider()`; every client that uses it gets the
|
||||
right provider. The convention-based risk (one crate drifting to
|
||||
`ring`) is removed.
|
||||
- The duplicated boilerplate (each client rebuilding verifier
|
||||
selection + provider wiring) is centralized. `TlsClientConfig::new`
|
||||
is the single point where ADR-034's rule and ADR-084's provider are
|
||||
applied.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- `alknet-tls` grows a client-side type. The crate is no longer
|
||||
"server-only." This is correct — the crate's purpose is shared TLS
|
||||
config, and the client side is shared across all outbound-dialing
|
||||
crates (hub, worker, `CallClient`, `ChannelClient`).
|
||||
- The iroh client path does not use `TlsClientConfig`. This is
|
||||
unavoidable — iroh has its own TLS. The iroh dial helper applies the
|
||||
same ADR-034 rule via iroh's API. The consistency is in the rule,
|
||||
not in the type.
|
||||
- `TlsError` (OQ-63) now covers both server and client errors. The
|
||||
variant granularity is slightly larger (client-side variants:
|
||||
verifier construction, provider init, unknown-remote fail-closed).
|
||||
OQ-63 is the next session and will account for both.
|
||||
|
||||
## Door type
|
||||
|
||||
**One-way.** `TlsClientConfig` as the shared client-side TLS config in
|
||||
`alknet-tls` is structural — every outbound-dialing crate depends on
|
||||
it. Reversing would mean re-distributing verifier selection + provider
|
||||
wiring across crates, reintroducing the convention-based consistency
|
||||
risk. The `TlsClientConfig::new` signature (takes a verifier context,
|
||||
returns a `rustls::ClientConfig`) is one-way — changing it after
|
||||
consumers exist is a rewrite. The internal implementation (how the
|
||||
verifier context struct is shaped, how `FingerprintPinVerifier` relates
|
||||
to the CA-verify path) is two-way.
|
||||
|
||||
## References
|
||||
|
||||
- OQ-64 (resolved by this ADR) — should `alknet-tls` provide a
|
||||
client-side TLS config helper?
|
||||
- OQ-55 (unaffected — the dial seam remains deferred; this ADR
|
||||
extracts the TLS config, not the dial)
|
||||
- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §3 —
|
||||
verifier selection rule (known peer → fingerprint pin; unknown X.509
|
||||
→ CA verify; unknown raw-key → fail closed)
|
||||
- [ADR-084](084-aws-lc-rs-crypto-provider.md) — aws-lc-rs crypto
|
||||
provider on all paths
|
||||
- [ADR-082](082-alknet-tls-extraction.md) — `TlsServerConfig`
|
||||
(server-side; this ADR adds the client-side analogue)
|
||||
- [ADR-065](065-connection-from-stream-generic-single-stream.md) — the
|
||||
server-side precedent: separate the take-over (transport-agnostic,
|
||||
built now) from the dial (transport-specific, per-transport). This
|
||||
ADR is the client-side analogue.
|
||||
- [ADR-086](086-endpoint-types-and-entry-points.md) — the hub composes
|
||||
endpoint types and dials workers (hub-as-client)
|
||||
- OQ-63 — `TlsError` shape (next session; now covers both server and
|
||||
client variants)
|
||||
- `docs/architecture/crates/hub/README.md` §"Dial (outbound workers)" —
|
||||
the hub-as-client operations that need `TlsClientConfig`
|
||||
@@ -185,7 +185,7 @@ Door type is separate from whether a decision is made. A two-way door is a decis
|
||||
|----|-------|--------|------|-----|
|
||||
| [OQ-62](questions/062-alpn-list-sharing-two-config-hub.md) | Does a Hub Pass the Same ALPN List to Both `TlsServerConfig`s? | resolved | one | high |
|
||||
| [OQ-63](questions/063-tlserror-shape.md) | `TlsError` Shape | open | one | high |
|
||||
| [OQ-64](questions/064-client-side-tls-helper.md) | Should `alknet-tls` Provide a Client-Side TLS Config Helper? | deferred(scope) | two | med |
|
||||
| [OQ-64](questions/064-client-side-tls-helper.md) | Should `alknet-tls` Provide a Client-Side TLS Config Helper? | resolved | one | high |
|
||||
| [OQ-65](questions/065-websocket-carrying-channels.md) | Should WebSocket Carry the Channels Protocol (Not Just the Call Protocol)? | open | one | med |
|
||||
|
||||
## Deferred / Blocked
|
||||
@@ -294,17 +294,13 @@ filtering the tables above.
|
||||
|
||||
### OQ-64: Should `alknet-tls` Provide a Client-Side TLS Config Helper?
|
||||
|
||||
- **Blocked on**: the `AlknetClient` dial-seam extraction (OQ-55). The
|
||||
client-side TLS helper and the shared dial are the same seam — both
|
||||
answer "how does an outbound connection build its
|
||||
`rustls::ClientConfig` + select a verifier (ADR-034) + dial." The
|
||||
blocking condition is the same as OQ-55: a second transport's real
|
||||
client dial existing (TCP+TLS, SSH raw-TCP, HTTP-wrapped call), so the
|
||||
transport-polymorphic client+TLS seam is extractable from two
|
||||
*different* transport implementations, not one QUIC shape. Until then,
|
||||
`alknet-tls` is server-side only; the client side lives in
|
||||
`alknet-call`'s `FingerprintPinVerifier`, with provider consistency
|
||||
(ADR-084) enforced by convention.
|
||||
- **Priority**: medium
|
||||
- **Resolved** (ADR-087): `alknet-tls` provides `TlsClientConfig`. Not
|
||||
blocked on the dial-seam extraction (OQ-55) — the TLS config is a
|
||||
prerequisite for the dial, not a consequence of it. The circular
|
||||
hedge (TLS config deferred behind the dial, dial needs the TLS
|
||||
config) is broken. The hub-as-client requirement makes it a
|
||||
prerequisite for the first hub deployment. See
|
||||
[ADR-087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) and
|
||||
[OQ-64](questions/064-client-side-tls-helper.md).
|
||||
- **Full file**: [OQ-64](questions/064-client-side-tls-helper.md)
|
||||
|
||||
@@ -97,7 +97,7 @@ alknet-vault (standalone — foundational to ACL: key derivation, identity)
|
||||
│ alknet-core ProtocolHandler, endpoint (multi-transport accept loop),
|
||||
│ │ Connection, BidiStreamSource, AuthContext, IdentityProvider,
|
||||
│ │ StaticConfig, DynamicConfig
|
||||
│ ├── alknet-tls TlsServerConfig — shared TLS config across quinn + TCP+TLS + iroh (ADR-082)
|
||||
│ ├── alknet-tls TlsServerConfig + TlsClientConfig — shared TLS config across quinn + TCP+TLS + iroh (ADR-082/087)
|
||||
│ ├── alknet-call CallAdapter on alknet/call, CallClient, OperationRegistry, adapters
|
||||
│ └── alknet-channels
|
||||
│ ├── alknet-channels-core pure multiplexer (wire format, demux/mux) — ADR-081
|
||||
@@ -370,6 +370,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Multi-Transport Accept-Loop Runner | Endpoint takes no TLS config; TCP+TLS is an owned transport; public `dispatch` for SSH/WT |
|
||||
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Core mono-repo (substrate + deployment shapes + foundational handlers + vault) vs. consumer repos (docker, agent) |
|
||||
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type (resolves OQ-62) |
|
||||
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | `alknet-tls` provides client-side TLS config; not deferred behind OQ-55; breaks the circular hedge; hub-as-client is a first-class use case |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -19,41 +19,62 @@
|
||||
and is not the thing being deferred — the shared *dial* is. The deferral
|
||||
is on transport-polymorphism of the dial, not on client count or on the
|
||||
channels protocol's API.
|
||||
- **What is NOT deferred (amended by ADR-087)**: the client-side TLS
|
||||
config (`TlsClientConfig`) is **not** part of this deferral. OQ-64
|
||||
is resolved — `alknet-tls` provides `TlsClientConfig` now, unblocked
|
||||
by this OQ. The TLS config is a **prerequisite** for the dial, not a
|
||||
consequence of it; it is transport-agnostic (ADR-034 verifier
|
||||
selection + ADR-084 provider, both already decided). Each
|
||||
transport-specific dial helper builds its `TlsClientConfig` and
|
||||
passes it to its transport's connector. This OQ defers only the
|
||||
*transport-polymorphic dial extraction* — the shared `AlknetClient::dial()`
|
||||
that picks the transport and calls the right connector. When a second
|
||||
transport's dial exists, the dial seam is extractable; the TLS config
|
||||
is already shared by then.
|
||||
- **Resolution**: Not yet decidable. The shared substance across
|
||||
TLS-carrying transports is ADR-034's verifier-selection rule (PeerEntry
|
||||
presence → fingerprint pin : CA-verify / fail-closed) and the
|
||||
`rustls::ClientConfig` construction — ~20 lines, transport-agnostic in
|
||||
rule. But the *dial* is transport-specific, and we have one of ~5 shapes
|
||||
implemented (QUIC; the others being HTTP, TCP+TLS, WebTransport, raw
|
||||
TCP). Extracting a QUIC-shaped connector to core and naming it
|
||||
`AlknetClient` would bake QUIC in as *the* establishment shape — the
|
||||
same welding ADR-065 unwound on the server side, repeated on the client
|
||||
side. The dial is transport-polymorphic; the shared rule is narrow. Until
|
||||
a second transport's dial exists, the seam between "dial + TLS" (per-
|
||||
transport) and "spawn the dispatcher" (per-crate) is not extractable from
|
||||
two real shapes — it's guessable from one.
|
||||
`rustls::ClientConfig` construction — now centralized in
|
||||
`TlsClientConfig::new` (ADR-087, OQ-64 resolved). What remains
|
||||
transport-specific is the *dial* itself — `quinn::Endpoint::connect`,
|
||||
`TcpStream::connect` + `TlsConnector::connect`, iroh's
|
||||
`Endpoint::connect`. We have one of ~5 shapes implemented (QUIC; the
|
||||
others being HTTP, TCP+TLS, WebTransport, raw TCP). Extracting a
|
||||
QUIC-shaped connector to core and naming it `AlknetClient` would bake
|
||||
QUIC in as *the* establishment shape — the same welding ADR-065
|
||||
unwound on the server side, repeated on the client side. The dial is
|
||||
transport-polymorphic; the shared TLS config is narrow and now
|
||||
extracted (ADR-087). Until a second transport's dial exists, the seam
|
||||
between "dial" (per-transport) and "spawn the dispatcher" (per-crate)
|
||||
is not extractable from two real shapes — it's guessable from one.
|
||||
- **What does NOT block on this**: each crate building its own client
|
||||
standalone, and each transport-specific dial helper. `CallClient`'s
|
||||
transport-agnostic take-over (`spawn_dispatch`) and `ChannelClient`'s
|
||||
transport-agnostic take-over (`from_connection`, ADR-080) are decided;
|
||||
`connect_quic` dials QUIC and calls `from_connection`. The SSH crate's
|
||||
TCP client, the HTTP call client, a `connect_tcp_tls` / `connect_webtransport` helper — each builds its own dial standalone. Core
|
||||
already permits all of this — `Connection::from_stream` / `from_bidi`
|
||||
(ADR-065) handles the non-QUIC transport on the server side, and nothing
|
||||
prevents a client from constructing a `Connection` the same way after
|
||||
its own transport-specific dial. The friction is duplicated boilerplate
|
||||
(each dial helper rebuilds verifier selection), not a missing
|
||||
capability and not a QUIC-welded client API. The bidirectionality
|
||||
criterion (a crate needs a Client type when (a) the endpoint has
|
||||
protocol-level authority — e.g., channels' id allocation — or (b) the
|
||||
protocol needs a reliable establishment interface) is met by each crate
|
||||
independently; `AlknetClient` is the eventual *shared dial+TLS seam*,
|
||||
not a prerequisite for any single client to exist.
|
||||
- **Cross-references**: ADR-034 (verifier selection — currently in
|
||||
`CallClient`, would move to the extracted seam when this is resolved),
|
||||
ADR-065 (server-side transport generalization — the client-side analogue
|
||||
this OQ's deferral avoids preempting), ADR-070 (the `BidiStreamSource`
|
||||
extension point, which is the *Connection* opening and is orthogonal to
|
||||
the *client* establishment question), OQ-CH-14 in
|
||||
standalone with a shared `TlsClientConfig` (ADR-087), and each
|
||||
transport-specific dial helper. `CallClient`'s transport-agnostic
|
||||
take-over (`spawn_dispatch`) and `ChannelClient`'s transport-agnostic
|
||||
take-over (`from_connection`, ADR-080) are decided; `connect_quic`
|
||||
dials QUIC and calls `from_connection`. The SSH crate's TCP client,
|
||||
the HTTP call client, a `connect_tcp_tls` / `connect_webtransport`
|
||||
helper — each builds its `TlsClientConfig` (ADR-087) and its own dial
|
||||
standalone. Core already permits all of this —
|
||||
`Connection::from_stream` / `from_bidi` (ADR-065) handles the
|
||||
non-QUIC transport on the server side, and nothing prevents a client
|
||||
from constructing a `Connection` the same way after its own
|
||||
transport-specific dial. The friction is the duplicated dial
|
||||
boilerplate (each dial helper calls its transport's connector), not
|
||||
duplicated TLS config (that is shared via ADR-087). The
|
||||
bidirectionality criterion (a crate needs a Client type when (a) the
|
||||
endpoint has protocol-level authority — e.g., channels' id allocation
|
||||
— or (b) the protocol needs a reliable establishment interface) is
|
||||
met by each crate independently; `AlknetClient` is the eventual
|
||||
*shared dial seam*, not a prerequisite for any single client to
|
||||
exist.
|
||||
- **Cross-references**: ADR-034 (verifier selection — centralized in
|
||||
`TlsClientConfig::new` per ADR-087), ADR-087 (`TlsClientConfig` is
|
||||
not blocked on this OQ — the TLS config is extracted; only the dial
|
||||
remains deferred), ADR-065 (server-side transport generalization —
|
||||
the client-side analogue this OQ's deferral avoids preempting),
|
||||
ADR-070 (the `BidiStreamSource` extension point, which is the
|
||||
*Connection* opening and is orthogonal to the *client* establishment
|
||||
question), OQ-CH-14 in
|
||||
`docs/research/alknet-channels/phase-0-findings.md` (the research-scope
|
||||
question this core-scope OQ carries forward).
|
||||
@@ -1,55 +1,60 @@
|
||||
# OQ-64: Should `alknet-tls` Provide a Client-Side TLS Config Helper?
|
||||
|
||||
- **Origin**: `docs/architecture/crates/tls/README.md` (the "Server-only
|
||||
for now" section flags this as deferred); ADR-084 (requires the
|
||||
for now" section flagged this as deferred); ADR-084 (requires the
|
||||
client-side `rustls::ClientConfig` to use the same `aws_lc_rs`
|
||||
provider — currently enforced by convention, not shared code).
|
||||
- **Status**: deferred(scope)
|
||||
- **Door type**: two-way (adding a client helper to `alknet-tls` is
|
||||
additive; the risk is not the addition but *extracting it
|
||||
prematurely* and baking in a QUIC-shaped client — see Blocking on)
|
||||
- **Priority**: medium
|
||||
- **Blocked on**: the `AlknetClient` dial-seam extraction (OQ-55). The
|
||||
client-side TLS helper and the shared dial are the same seam: both
|
||||
answer "how does an outbound connection build its
|
||||
`rustls::ClientConfig` + select a verifier (ADR-034) + dial the
|
||||
transport." Extracting the TLS helper without a second transport's
|
||||
real client dial existing would bake the QUIC client's shape into a
|
||||
shared helper — the same welding ADR-065 unwound on the server side.
|
||||
The blocking condition is the same as OQ-55: a second transport's
|
||||
real dial (TCP+TLS, SSH raw-TCP, HTTP-wrapped call) existing, so the
|
||||
transport-polymorphic client+TLS seam is extractable from two
|
||||
*different* transport implementations.
|
||||
- **Resolution**: Not yet decidable. `alknet-tls` is server-side only
|
||||
as specified. The client side — `rustls::ClientConfig` construction +
|
||||
ADR-034 verifier selection (fingerprint pin for known peers, CA-verify
|
||||
for unknown X.509, fail-closed for unknown raw-key) — lives in
|
||||
`alknet-call`'s `FingerprintPinVerifier` today. The
|
||||
provider-consistency requirement (ADR-084: `aws_lc_rs` on all paths)
|
||||
is enforced by convention (two crates independently constructing
|
||||
`aws_lc_rs::default_provider()`) until this OQ is resolved.
|
||||
provider — previously enforced by convention, not shared code).
|
||||
- **Status**: resolved
|
||||
- **Door type**: one-way (`TlsClientConfig` as the shared client-side
|
||||
TLS config in `alknet-tls` is structural — every outbound-dialing
|
||||
crate depends on it. Reversing would re-distribute verifier selection
|
||||
+ provider wiring across crates.)
|
||||
- **Priority**: high (upgraded from medium — the hub-as-client
|
||||
requirement makes this a prerequisite for the first hub deployment,
|
||||
not a future extraction)
|
||||
- **Resolution**: **Yes. `alknet-tls` provides `TlsClientConfig`.** It
|
||||
is not blocked on the dial-seam extraction (OQ-55).
|
||||
|
||||
What does NOT block on this: each client (`CallClient`,
|
||||
`ChannelClient`) building its own `ClientConfig` standalone with the
|
||||
matching provider. The friction is duplicated boilerplate (each
|
||||
client rebuilds verifier selection + provider wiring), not a missing
|
||||
capability and not a QUIC-welded client API. The
|
||||
transport-agnostic take-over (`CallClient::spawn_dispatch`,
|
||||
`ChannelClient::from_connection` — ADR-080) is decided and is not
|
||||
the thing being deferred; only the shared *client TLS config helper*
|
||||
is.
|
||||
The previous deferral linked OQ-64 and OQ-55 as "the same seam,"
|
||||
creating a circular dependency: the TLS config is deferred behind the
|
||||
dial, but the dial needs the TLS config. The circle is broken by
|
||||
separating the two concerns:
|
||||
|
||||
Note on the hub-as-client case: a hub (A) that dials another hub (B)
|
||||
uses a client to do so — from B's perspective A is a worker. The
|
||||
bidirectionality of the call and channels protocols means both sides
|
||||
can be both hub and worker within a connection. This does not change
|
||||
the blocking condition: the shared client TLS helper is still about
|
||||
the *dial*, regardless of whether the dialer is a hub, a worker, or a
|
||||
hub-worker. The hub-as-client case is a use case that the resolved
|
||||
helper must cover, not a reason to resolve it now.
|
||||
- **Cross-references**: OQ-55 (the `AlknetClient` dial-seam extraction
|
||||
— this OQ's blocking condition), ADR-034 (verifier selection — the
|
||||
rule the helper would centralize), ADR-084 (provider consistency —
|
||||
the convention that holds until this is resolved), ADR-065 (the
|
||||
server-side transport generalization this OQ's deferral avoids
|
||||
preempting on the client side)
|
||||
1. **`TlsClientConfig`** — the `rustls::ClientConfig` + ADR-034
|
||||
verifier selection + ADR-084 crypto provider. Transport-agnostic.
|
||||
All decisions are made. Buildable today. It is 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).**
|
||||
|
||||
`TlsClientConfig::new` takes a verifier context (the inputs to
|
||||
ADR-034's rule: `PeerEntry` presence, expected fingerprint, remote
|
||||
cert type) and returns a `rustls::ClientConfig`. The caller
|
||||
(transport-specific dial helper, `CallClient::connect_quic`, a future
|
||||
`connect_tcp_tls`) passes it to the transport's connector. Iroh is
|
||||
the exception — it has its own TLS and does not consume a
|
||||
`rustls::ClientConfig`; the iroh dial helper applies the same
|
||||
ADR-034 rule via iroh's `NodeId` verification API.
|
||||
|
||||
The hub makes this non-optional: a hub dials out to workers it
|
||||
supervises and to other hubs (hub-as-client). The hub's
|
||||
`dial_worker_connection` / `supervise_worker` need a
|
||||
`TlsClientConfig` for the outbound dial. The first hub deployment
|
||||
(web + native) dials workers over QUIC with the worker's fingerprint
|
||||
pinned. There is no "later" — it is on the critical path for the
|
||||
first hub and for `alknet-worker`.
|
||||
|
||||
See [ADR-087](../decisions/087-tlsclientconfig-not-blocked-on-dial.md)
|
||||
for the full decision, including the circular-hedge analysis, the
|
||||
hub-as-client requirement, and the iroh exception.
|
||||
- **Cross-references**: [ADR-087](../decisions/087-tlsclientconfig-not-blocked-on-dial.md)
|
||||
(the decision), [ADR-034](../decisions/034-outgoing-only-x509-and-three-peer-roles.md)
|
||||
§3 (verifier selection — the rule `TlsClientConfig::new`
|
||||
centralizes), [ADR-084](../decisions/084-aws-lc-rs-crypto-provider.md)
|
||||
(provider consistency — enforced by code, not convention, for the
|
||||
client side), [ADR-082](../decisions/082-alknet-tls-extraction.md)
|
||||
(`TlsServerConfig` — the server-side analogue), OQ-55 (the dial seam
|
||||
— remains deferred; this OQ's resolution does not affect it),
|
||||
OQ-63 (`TlsError` shape — now covers both server and client variants)
|
||||
Reference in new issue
Block a user