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 |
|
| [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 |
|
| [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 |
|
| [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
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -282,8 +282,12 @@ all.
|
|||||||
#### Dial (outbound workers) — transport-agnostic
|
#### Dial (outbound workers) — transport-agnostic
|
||||||
|
|
||||||
The hub dials outbound workers via `ChannelClient`, not `CallClient`.
|
The hub dials outbound workers via `ChannelClient`, not `CallClient`.
|
||||||
The dial path mirrors the `from_connection` / `connect_quic` split
|
The hub is a client when it dials out — a hub (B) that connects to
|
||||||
(ADR-080):
|
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
|
```rust
|
||||||
impl Hub {
|
impl Hub {
|
||||||
@@ -299,8 +303,9 @@ impl Hub {
|
|||||||
) -> Result<(PeerId, ChannelClient), HubError>;
|
) -> Result<(PeerId, ChannelClient), HubError>;
|
||||||
|
|
||||||
/// QUIC convenience: dial a worker over QUIC, then
|
/// QUIC convenience: dial a worker over QUIC, then
|
||||||
/// `dial_worker_connection`. Two-way door — additive over
|
/// `dial_worker_connection`. Builds a `TlsClientConfig` (ADR-087)
|
||||||
/// `dial_worker_connection`.
|
/// 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(
|
pub async fn connect_quic_worker(
|
||||||
&self,
|
&self,
|
||||||
addr: SocketAddr,
|
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
|
discover the worker's operations, registers the discovered bundles in
|
||||||
the connection's Layer 2 overlay, and attaches the peer to the
|
the connection's Layer 2 overlay, and attaches the peer to the
|
||||||
aggregated env. `connect_quic_worker` is the "I just want QUIC"
|
aggregated env. `connect_quic_worker` is the "I just want QUIC"
|
||||||
convenience — it calls `ChannelClient::connect_quic`, then
|
convenience — it builds a `TlsClientConfig` (ADR-087) with the
|
||||||
`dial_worker_connection`. A future `connect_tcp_tls_worker` dials
|
worker's fingerprint pinned, calls `ChannelClient::connect_quic`, then
|
||||||
TCP+TLS and calls `dial_worker_connection` the same way. The
|
`dial_worker_connection`. A future `connect_tcp_tls_worker` builds a
|
||||||
one-way-door surface is `dial_worker_connection`; the dial helpers
|
`TlsClientConfig` and dials TCP+TLS the same way. The one-way-door
|
||||||
are two-way-door conveniences.
|
surface is `dial_worker_connection`; the dial helpers are two-way-door
|
||||||
|
conveniences.
|
||||||
|
|
||||||
#### Accept (inbound workers and browsers) — transport-agnostic
|
#### 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 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 |
|
| 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 |
|
| 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
|
## 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-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-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-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) —
|
- alkapi [hub.md](/workspace/@alkdev/alkapi/docs/architecture/hub.md) —
|
||||||
the first hub consumer, the concrete use case that informed this
|
the first hub consumer, the concrete use case that informed this
|
||||||
crate
|
crate
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ last_updated: 2026-07-15
|
|||||||
|
|
||||||
# alknet-tls
|
# alknet-tls
|
||||||
|
|
||||||
Shared TLS configuration and certificate management. Builds a
|
Shared TLS configuration and certificate management — server and
|
||||||
`rustls::ServerConfig` (or an ACME state machine + cert resolver) once,
|
client. Builds a `rustls::ServerConfig` (or an ACME state machine +
|
||||||
and shares it across multiple transports — quinn, `tokio-rustls`
|
cert resolver) once and shares it across multiple transports — quinn,
|
||||||
(TCP+TLS), and iroh — so one certificate identity serves QUIC and TCP
|
`tokio-rustls` (TCP+TLS), and iroh — so one certificate identity serves
|
||||||
endpoints simultaneously. One ACME state machine, one cert, N
|
QUIC and TCP endpoints simultaneously. Builds a `rustls::ClientConfig`
|
||||||
transports.
|
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
|
## 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
|
cert provider, not the accept loop. This keeps `alknet-tls` focused on
|
||||||
TLS setup and cert sharing, not transport accept logic.
|
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**:
|
`alknet-tls` provides a client-side config alongside `TlsServerConfig`.
|
||||||
`TlsServerConfig` and its accessors (`for_quinn`, `for_tcp_tls`,
|
A hub dials out to workers it supervises and to other hubs
|
||||||
`rustls_config`) produce `rustls::ServerConfig`-shaped outputs for
|
(hub-as-client); `alknet-worker` dials a hub. Both need a
|
||||||
inbound listeners. There is no `TlsClientConfig`, no `for_client()`,
|
`rustls::ClientConfig` with ADR-034's verifier selection and ADR-084's
|
||||||
no client-side cert/verifier helper in this crate.
|
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
|
```rust
|
||||||
`CallClient` / `ChannelClient` for outbound dials, including
|
pub struct TlsClientConfig {
|
||||||
[ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md)'s
|
config: rustls::ClientConfig,
|
||||||
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.
|
|
||||||
|
|
||||||
Whether `alknet-tls` should grow a client-side helper (a
|
impl TlsClientConfig {
|
||||||
`TlsClientConfig` that centralizes verifier selection + provider
|
/// Build a client TLS config for the given remote identity context.
|
||||||
consistency, so `alknet-call` / a future `AlknetClient` don't each
|
/// Applies ADR-034 verifier selection:
|
||||||
rebuild it) is **deferred** — see OQ-64. The deferral blocks on the
|
/// - known peer (PeerEntry present) + raw key → fingerprint pin
|
||||||
`AlknetClient` dial-seam extraction (OQ-55): the client-side TLS helper
|
/// - known peer (PeerEntry present) + X.509 → fingerprint pin
|
||||||
and the shared dial are the same seam, and extracting either without a
|
/// - unknown remote + X.509 → CA verification (WebPkiServerVerifier)
|
||||||
second transport's real client dial would bake QUIC in as the client
|
/// - unknown remote + raw key → fail closed
|
||||||
shape — the same welding ADR-065 unwound on the server side. The
|
/// Applies ADR-084 crypto provider (aws_lc_rs::default_provider()).
|
||||||
provider-consistency convention (ADR-084) holds until then.
|
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)
|
## 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` |
|
| [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 |
|
| [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 |
|
| [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
|
## 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
|
- **OQ-63** (open): `TlsError` shape — the error type is referenced in
|
||||||
public signatures but not sketched. Needs a variant-granularity
|
public signatures but not sketched. Needs a variant-granularity
|
||||||
decision (single enum vs thin wrapper) before implementation.
|
decision (single enum vs thin wrapper) before implementation.
|
||||||
- **OQ-64** (deferred): Client-side TLS config helper. `alknet-tls` is
|
- **OQ-64** (resolved): `alknet-tls` provides `TlsClientConfig`
|
||||||
server-side only as specified; the client-side `ClientConfig` +
|
(ADR-087). Not blocked on the dial-seam extraction (OQ-55) — the TLS
|
||||||
verifier selection lives in `alknet-call`. Blocked on the
|
config is a prerequisite for the dial, not a consequence of it.
|
||||||
`AlknetClient` dial-seam extraction (OQ-55) — extracting the client
|
Centralizes ADR-034 verifier selection + ADR-084 provider; the
|
||||||
helper without a second transport's dial would bake QUIC in as the
|
hub-as-client requirement makes it a prerequisite for the first hub
|
||||||
client shape.
|
deployment. The dial seam (OQ-55) remains deferred; the TLS config
|
||||||
|
does not.
|
||||||
|
|
||||||
## References
|
## 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`
|
- `docs/architecture/decisions/086-endpoint-types-and-entry-points.md`
|
||||||
— three endpoint types (web/native/iroh); split ALPN lists per
|
— three endpoint types (web/native/iroh); split ALPN lists per
|
||||||
endpoint type (resolves OQ-62); entry-point vs. endpoint distinction
|
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
|
- `docs/architecture/crates/core/endpoint.md` — current endpoint design
|
||||||
(TLS section will be amended to point to `alknet-tls`)
|
(TLS section will be amended to point to `alknet-tls`)
|
||||||
- `docs/architecture/crates/core/config.md` — `TlsIdentity`, `StaticConfig`
|
- `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-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-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 |
|
| [OQ-65](questions/065-websocket-carrying-channels.md) | Should WebSocket Carry the Channels Protocol (Not Just the Call Protocol)? | open | one | med |
|
||||||
|
|
||||||
## Deferred / Blocked
|
## Deferred / Blocked
|
||||||
@@ -294,17 +294,13 @@ filtering the tables above.
|
|||||||
|
|
||||||
### OQ-64: Should `alknet-tls` Provide a Client-Side TLS Config Helper?
|
### OQ-64: Should `alknet-tls` Provide a Client-Side TLS Config Helper?
|
||||||
|
|
||||||
- **Blocked on**: the `AlknetClient` dial-seam extraction (OQ-55). The
|
- **Resolved** (ADR-087): `alknet-tls` provides `TlsClientConfig`. Not
|
||||||
client-side TLS helper and the shared dial are the same seam — both
|
blocked on the dial-seam extraction (OQ-55) — the TLS config is a
|
||||||
answer "how does an outbound connection build its
|
prerequisite for the dial, not a consequence of it. The circular
|
||||||
`rustls::ClientConfig` + select a verifier (ADR-034) + dial." The
|
hedge (TLS config deferred behind the dial, dial needs the TLS
|
||||||
blocking condition is the same as OQ-55: a second transport's real
|
config) is broken. The hub-as-client requirement makes it a
|
||||||
client dial existing (TCP+TLS, SSH raw-TCP, HTTP-wrapped call), so the
|
prerequisite for the first hub deployment. See
|
||||||
transport-polymorphic client+TLS seam is extractable from two
|
[ADR-087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) and
|
||||||
*different* transport implementations, not one QUIC shape. Until then,
|
[OQ-64](questions/064-client-side-tls-helper.md).
|
||||||
`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
|
|
||||||
- **Full file**: [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),
|
│ alknet-core ProtocolHandler, endpoint (multi-transport accept loop),
|
||||||
│ │ Connection, BidiStreamSource, AuthContext, IdentityProvider,
|
│ │ Connection, BidiStreamSource, AuthContext, IdentityProvider,
|
||||||
│ │ StaticConfig, DynamicConfig
|
│ │ 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-call CallAdapter on alknet/call, CallClient, OperationRegistry, adapters
|
||||||
│ └── alknet-channels
|
│ └── alknet-channels
|
||||||
│ ├── alknet-channels-core pure multiplexer (wire format, demux/mux) — ADR-081
|
│ ├── 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 |
|
| [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) |
|
| [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) |
|
| [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
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -19,41 +19,62 @@
|
|||||||
and is not the thing being deferred — the shared *dial* is. The deferral
|
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
|
is on transport-polymorphism of the dial, not on client count or on the
|
||||||
channels protocol's API.
|
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
|
- **Resolution**: Not yet decidable. The shared substance across
|
||||||
TLS-carrying transports is ADR-034's verifier-selection rule (PeerEntry
|
TLS-carrying transports is ADR-034's verifier-selection rule (PeerEntry
|
||||||
presence → fingerprint pin : CA-verify / fail-closed) and the
|
presence → fingerprint pin : CA-verify / fail-closed) and the
|
||||||
`rustls::ClientConfig` construction — ~20 lines, transport-agnostic in
|
`rustls::ClientConfig` construction — now centralized in
|
||||||
rule. But the *dial* is transport-specific, and we have one of ~5 shapes
|
`TlsClientConfig::new` (ADR-087, OQ-64 resolved). What remains
|
||||||
implemented (QUIC; the others being HTTP, TCP+TLS, WebTransport, raw
|
transport-specific is the *dial* itself — `quinn::Endpoint::connect`,
|
||||||
TCP). Extracting a QUIC-shaped connector to core and naming it
|
`TcpStream::connect` + `TlsConnector::connect`, iroh's
|
||||||
`AlknetClient` would bake QUIC in as *the* establishment shape — the
|
`Endpoint::connect`. We have one of ~5 shapes implemented (QUIC; the
|
||||||
same welding ADR-065 unwound on the server side, repeated on the client
|
others being HTTP, TCP+TLS, WebTransport, raw TCP). Extracting a
|
||||||
side. The dial is transport-polymorphic; the shared rule is narrow. Until
|
QUIC-shaped connector to core and naming it `AlknetClient` would bake
|
||||||
a second transport's dial exists, the seam between "dial + TLS" (per-
|
QUIC in as *the* establishment shape — the same welding ADR-065
|
||||||
transport) and "spawn the dispatcher" (per-crate) is not extractable from
|
unwound on the server side, repeated on the client side. The dial is
|
||||||
two real shapes — it's guessable from one.
|
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
|
- **What does NOT block on this**: each crate building its own client
|
||||||
standalone, and each transport-specific dial helper. `CallClient`'s
|
standalone with a shared `TlsClientConfig` (ADR-087), and each
|
||||||
transport-agnostic take-over (`spawn_dispatch`) and `ChannelClient`'s
|
transport-specific dial helper. `CallClient`'s transport-agnostic
|
||||||
transport-agnostic take-over (`from_connection`, ADR-080) are decided;
|
take-over (`spawn_dispatch`) and `ChannelClient`'s transport-agnostic
|
||||||
`connect_quic` dials QUIC and calls `from_connection`. The SSH crate's
|
take-over (`from_connection`, ADR-080) are decided; `connect_quic`
|
||||||
TCP client, the HTTP call client, a `connect_tcp_tls` / `connect_webtransport` helper — each builds its own dial standalone. Core
|
dials QUIC and calls `from_connection`. The SSH crate's TCP client,
|
||||||
already permits all of this — `Connection::from_stream` / `from_bidi`
|
the HTTP call client, a `connect_tcp_tls` / `connect_webtransport`
|
||||||
(ADR-065) handles the non-QUIC transport on the server side, and nothing
|
helper — each builds its `TlsClientConfig` (ADR-087) and its own dial
|
||||||
prevents a client from constructing a `Connection` the same way after
|
standalone. Core already permits all of this —
|
||||||
its own transport-specific dial. The friction is duplicated boilerplate
|
`Connection::from_stream` / `from_bidi` (ADR-065) handles the
|
||||||
(each dial helper rebuilds verifier selection), not a missing
|
non-QUIC transport on the server side, and nothing prevents a client
|
||||||
capability and not a QUIC-welded client API. The bidirectionality
|
from constructing a `Connection` the same way after its own
|
||||||
criterion (a crate needs a Client type when (a) the endpoint has
|
transport-specific dial. The friction is the duplicated dial
|
||||||
protocol-level authority — e.g., channels' id allocation — or (b) the
|
boilerplate (each dial helper calls its transport's connector), not
|
||||||
protocol needs a reliable establishment interface) is met by each crate
|
duplicated TLS config (that is shared via ADR-087). The
|
||||||
independently; `AlknetClient` is the eventual *shared dial+TLS seam*,
|
bidirectionality criterion (a crate needs a Client type when (a) the
|
||||||
not a prerequisite for any single client to exist.
|
endpoint has protocol-level authority — e.g., channels' id allocation
|
||||||
- **Cross-references**: ADR-034 (verifier selection — currently in
|
— or (b) the protocol needs a reliable establishment interface) is
|
||||||
`CallClient`, would move to the extracted seam when this is resolved),
|
met by each crate independently; `AlknetClient` is the eventual
|
||||||
ADR-065 (server-side transport generalization — the client-side analogue
|
*shared dial seam*, not a prerequisite for any single client to
|
||||||
this OQ's deferral avoids preempting), ADR-070 (the `BidiStreamSource`
|
exist.
|
||||||
extension point, which is the *Connection* opening and is orthogonal to
|
- **Cross-references**: ADR-034 (verifier selection — centralized in
|
||||||
the *client* establishment question), OQ-CH-14 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
|
`docs/research/alknet-channels/phase-0-findings.md` (the research-scope
|
||||||
question this core-scope OQ carries forward).
|
question this core-scope OQ carries forward).
|
||||||
@@ -1,55 +1,60 @@
|
|||||||
# OQ-64: Should `alknet-tls` Provide a Client-Side TLS Config Helper?
|
# OQ-64: Should `alknet-tls` Provide a Client-Side TLS Config Helper?
|
||||||
|
|
||||||
- **Origin**: `docs/architecture/crates/tls/README.md` (the "Server-only
|
- **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`
|
client-side `rustls::ClientConfig` to use the same `aws_lc_rs`
|
||||||
provider — currently enforced by convention, not shared code).
|
provider — previously enforced by convention, not shared code).
|
||||||
- **Status**: deferred(scope)
|
- **Status**: resolved
|
||||||
- **Door type**: two-way (adding a client helper to `alknet-tls` is
|
- **Door type**: one-way (`TlsClientConfig` as the shared client-side
|
||||||
additive; the risk is not the addition but *extracting it
|
TLS config in `alknet-tls` is structural — every outbound-dialing
|
||||||
prematurely* and baking in a QUIC-shaped client — see Blocking on)
|
crate depends on it. Reversing would re-distribute verifier selection
|
||||||
- **Priority**: medium
|
+ provider wiring across crates.)
|
||||||
- **Blocked on**: the `AlknetClient` dial-seam extraction (OQ-55). The
|
- **Priority**: high (upgraded from medium — the hub-as-client
|
||||||
client-side TLS helper and the shared dial are the same seam: both
|
requirement makes this a prerequisite for the first hub deployment,
|
||||||
answer "how does an outbound connection build its
|
not a future extraction)
|
||||||
`rustls::ClientConfig` + select a verifier (ADR-034) + dial the
|
- **Resolution**: **Yes. `alknet-tls` provides `TlsClientConfig`.** It
|
||||||
transport." Extracting the TLS helper without a second transport's
|
is not blocked on the dial-seam extraction (OQ-55).
|
||||||
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.
|
|
||||||
|
|
||||||
What does NOT block on this: each client (`CallClient`,
|
The previous deferral linked OQ-64 and OQ-55 as "the same seam,"
|
||||||
`ChannelClient`) building its own `ClientConfig` standalone with the
|
creating a circular dependency: the TLS config is deferred behind the
|
||||||
matching provider. The friction is duplicated boilerplate (each
|
dial, but the dial needs the TLS config. The circle is broken by
|
||||||
client rebuilds verifier selection + provider wiring), not a missing
|
separating the two concerns:
|
||||||
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.
|
|
||||||
|
|
||||||
Note on the hub-as-client case: a hub (A) that dials another hub (B)
|
1. **`TlsClientConfig`** — the `rustls::ClientConfig` + ADR-034
|
||||||
uses a client to do so — from B's perspective A is a worker. The
|
verifier selection + ADR-084 crypto provider. Transport-agnostic.
|
||||||
bidirectionality of the call and channels protocols means both sides
|
All decisions are made. Buildable today. It is a **prerequisite**
|
||||||
can be both hub and worker within a connection. This does not change
|
for any dial, not a consequence of it.
|
||||||
the blocking condition: the shared client TLS helper is still about
|
2. **The dial** (`AlknetClient::dial()`) — transport-specific
|
||||||
the *dial*, regardless of whether the dialer is a hub, a worker, or a
|
connection establishment. Extracting a transport-polymorphic dial
|
||||||
hub-worker. The hub-as-client case is a use case that the resolved
|
from one shape (QUIC) would bake QUIC in. **Legitimate deferral
|
||||||
helper must cover, not a reason to resolve it now.
|
(OQ-55, unchanged).**
|
||||||
- **Cross-references**: OQ-55 (the `AlknetClient` dial-seam extraction
|
|
||||||
— this OQ's blocking condition), ADR-034 (verifier selection — the
|
`TlsClientConfig::new` takes a verifier context (the inputs to
|
||||||
rule the helper would centralize), ADR-084 (provider consistency —
|
ADR-034's rule: `PeerEntry` presence, expected fingerprint, remote
|
||||||
the convention that holds until this is resolved), ADR-065 (the
|
cert type) and returns a `rustls::ClientConfig`. The caller
|
||||||
server-side transport generalization this OQ's deferral avoids
|
(transport-specific dial helper, `CallClient::connect_quic`, a future
|
||||||
preempting on the client side)
|
`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