docs(arch): ConnectionCredentials decouples dial from call protocol (ADR-091) + migration plan cleanups

ADR-091: ConnectionCredentials — decouple the dial credential bundle
from the call protocol. CallCredentials (call-protocol-level, carries
auth_token) was being used as the dial's credential type, coupling the
dial to the call protocol. The auth_token is a hub-layer identity
correlation mechanism (browsers, alknet/register), not a transport
credential. ConnectionCredentials (transport-level: local_identity +
remote_identity) is the dial's credential bundle; all three dial
signatures unify on &ConnectionCredentials; dial_iroh's node_id
parameter is derived from remote_identity.fingerprint. CallCredentials
stays in alknet-call with auth_token. The shape is validated by the
future dial_ssh pattern (russh's check_server_key + authenticate_publickey
consume the same two dimensions).

Amends ADR-089 §3 (dial signatures) and §5 (move consequence), ADR-087
(input framing). Updates client/tls/call/core crate specs and the
extraction plan's Phase 0/3/4/5.

Migration plan cleanups (findings.md):
- Remove duplicated ordering-rationale bullets (copy-paste artifact)
- TL;DR: six phases -> seven (Phase 0 promoted); four compilable
  intermediate states -> each phase leaves workspace compilable
- Remove inline 'Wait — that's 10, not 8' self-correction; fix
  Category B header to (10 tests)
- Replace contradictory line ranges in Net Phase 5 test impact with
  name-based references
- Decide quinn feature fate: removed (not no-op)
- Note webpki-roots always-present per ADR-088 §5 in Phase 1 dep list
This commit is contained in:
glm-5.2 committed 2026-07-17 05:56:47 +00:00
1 parent 16ae3cf6c9
commit 613bf680cd
9 files changed
+599 -154

No files matched your search

+64 -52
View File
@@ -155,7 +155,7 @@ hiding the client's real IP from the hub.
```rust
impl AlknetClient {
/// QUIC dial. Builds a `TlsClientConfig` from `credentials`
/// QUIC dial. Builds a `TlsClientConfig` from `creds`
/// (ADR-034 verifier selection + ADR-084 provider), dials `addr`
/// on `alpn`, returns a `Connection` via
/// `Connection::from_quinn_with_alpn`. The `server_name` is the
@@ -167,10 +167,10 @@ impl AlknetClient {
addr: SocketAddr,
server_name: &str,
alpn: &[u8],
credentials: &CallCredentials,
creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>;
/// TCP+TLS dial. Builds a `TlsClientConfig` from `credentials`,
/// TCP+TLS dial. Builds a `TlsClientConfig` from `creds`,
/// connects a `TcpStream` to `addr`, wraps with `TlsConnector`
/// using `host` as the SNI, returns a `Connection` via
/// `Connection::from_bidi` (ADR-065). Feature-gated on `tcp`.
@@ -180,21 +180,23 @@ impl AlknetClient {
host: &str,
addr: SocketAddr,
alpn: &[u8],
credentials: &CallCredentials,
creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>;
/// Iroh dial. Dials `node_id` on `alpn` via the iroh endpoint.
/// The iroh path does NOT use `TlsClientConfig` — iroh has its
/// own TLS (shares the `Ed25519SecretKey`, not the rustls config —
/// ADR-087 §3, ADR-089 §3). The verifier is iroh's `NodeId` match
/// (fingerprint pin by another name — ADR-034 §3). An unknown
/// iroh remote fails closed (no CA). Feature-gated on `iroh`.
/// Iroh dial. Dials on `alpn` via the iroh endpoint. The iroh path
/// does NOT use `TlsClientConfig` — iroh has its own TLS (shares the
/// `Ed25519SecretKey`, not the rustls config — ADR-087 §3, ADR-089
/// §3). The local key is extracted from `creds.local_identity`; the
/// remote `NodeId` is derived from `creds.remote_identity.fingerprint`
/// (`ed25519:<hex>` → `NodeId::from_bytes`). The verifier is iroh's
/// `NodeId` match (fingerprint pin by another name — ADR-034 §3).
/// An unknown iroh remote fails closed (no CA). Feature-gated on
/// `iroh`.
#[cfg(feature = "iroh")]
pub async fn dial_iroh(
&self,
node_id: iroh::NodeId,
alpn: &[u8],
local_key: &alknet_core::config::Ed25519SecretKey,
creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>;
}
```
@@ -204,9 +206,12 @@ The two rustls dials (`dial_quic`, `dial_tcp_tls`) share
pin for a known peer, CA-verify for an unknown X.509 remote, fail-closed
for an unknown raw-key remote) and the ADR-084 crypto provider
(`aws_lc_rs`). The iroh dial is the exception: iroh has its own TLS and
takes the `Ed25519SecretKey` directly, not a `rustls::ClientConfig`.
The consistency is in the rule (ADR-034), not in the type — the same
exception as the server side (ADR-082, ADR-087 §3).
takes the `Ed25519SecretKey` directly (extracted from
`creds.local_identity`), not a `rustls::ClientConfig`. The consistency
is in the rule (ADR-034), not in the type — the same exception as the
server side (ADR-082, ADR-087 §3). All three dials take
`&ConnectionCredentials` — the unified transport-level credential
bundle (ADR-091).
### What the dial does NOT do
@@ -266,7 +271,7 @@ invariant. The config's crate location (it can live in `alknet-client`,
detail; the shape is decided (ADR-090 §1).
**The proxy is invisible above the dial.** `Connection`, dispatch,
`CallCredentials`, `TlsClientConfig`, the hub's `supervise_worker`
`ConnectionCredentials`, `TlsClientConfig`, the hub's `supervise_worker`
closure — none know a proxy is in the path. The proxy is purely an
establishment concern, localized to `alknet-client`. A proxied QUIC
connection still yields a `Connection::from_quinn_with_alpn`; a
@@ -328,25 +333,25 @@ the alknet type level — see ADR-090 §"Two distinct SOCKS5 uses".
### Credentials
`AlknetClient`'s dials take a `CallCredentials` bundle — the shared
type from `alknet-core` (moved out of `alknet-call` so the dial does
not depend on the call protocol; see ADR-089 §5). It carries the local
`TlsIdentity`, the optional call-protocol-level `auth_token`, and the
`RemoteIdentity` for verifier selection. The credentials come from
`Capabilities` (ADR-014), never from environment variables — the
no-env-vars invariant. The assembly layer derives them from the vault
at startup and passes them to each dial.
`AlknetClient`'s dials take a `ConnectionCredentials` bundle — the
transport-level credential type from `alknet-core` (ADR-091). It
carries the two dimensions every dial consumes: the local `TlsIdentity`
(presented to the transport's identity layer) and the `RemoteIdentity`
for verifier selection. The credentials come from `Capabilities`
(ADR-014), never from environment variables — the no-env-vars
invariant. The assembly layer derives them from the vault at startup
and passes them to each dial.
**The `auth_token` is stripped at the TLS boundary.** `TlsClientConfig`
and `ClientVerifierContext` are TLS-level types and do not carry the
call-protocol auth token. The dial extracts the TLS-relevant fields
from `CallCredentials` — `tls_identity` → the client-cert
`local_identity`, `remote_identity` → the fingerprint-pin input to
`ClientVerifierContext` — and builds a `TlsClientConfig` from those
alone. The `auth_token` travels with the `Connection` into the
protocol take-over (`spawn_dispatch` / `from_connection`), where it is
sent as the first call-protocol frame when the protocol requires it.
This keeps `alknet-tls` free of any call-protocol coupling.
`ConnectionCredentials` is **not** the call-protocol credential bundle.
The call-protocol `auth_token` (a hub-correlated bearer for browsers
and `alknet/register` — ADR-017 §7) is a per-request field on
`call.requested` payloads, not a transport credential. It stays in the
call-protocol layer (`CallCredentials` in `alknet-call`); the dial does
not carry it. `Dispatcher::resolve_identity` resolves it via
`IdentityProvider::resolve_from_token` at dispatch time; the `from_call`
forwarding handler sets it via `build_forwarded_payload`. This keeps
`alknet-client` free of call-protocol coupling. See
[ADR-091](../../decisions/091-connectioncredentials-decouple-dial-from-call.md).
### The dialable ALPNs
@@ -425,11 +430,13 @@ public key) is verified against the expected `NodeId`, which is
fingerprint-pinning by another name. An unknown iroh remote fails
closed (no CA to fall back to — ADR-034 §3, Assumption 1).
The `dial_iroh` method takes the `Ed25519SecretKey` as a parameter
rather than pulling it from `CallCredentials` because iroh does not use
the `TlsClientConfig` path. The assembly layer reads the key from
`StaticConfig` (in core) and passes it directly, same as the server
side's iroh endpoint construction.
The `dial_iroh` method extracts the key from
`creds.local_identity` (`ConnectionCredentials`) rather than taking a
separate `Ed25519SecretKey` parameter because the dial signature is
unified — all three dials take `&ConnectionCredentials` (ADR-091). The
assembly layer reads the key from `StaticConfig` (in core) and passes
it via `ConnectionCredentials`, same as the server side's iroh endpoint
construction.
### Non-Rust native clients (out of scope)
@@ -557,7 +564,7 @@ don't use a proxy don't pay the dep.
```
alknet-client
├── alknet-core (Connection, CallCredentials, RemoteIdentity,
├── alknet-core (Connection, ConnectionCredentials, RemoteIdentity,
│ Ed25519SecretKey, types)
├── alknet-tls (TlsClientConfig — for quinn + tcp dials)
├── quinn (optional — dial_quic)
@@ -569,14 +576,15 @@ alknet-client
```
`alknet-client` depends on `alknet-tls` (for `TlsClientConfig`) and
`alknet-core` (for `Connection`, `CallCredentials`, `RemoteIdentity`,
and types). It does **not** depend on `alknet-call` or
`alknet-core` (for `Connection`, `ConnectionCredentials`,
`RemoteIdentity`, and types). It does **not** depend on `alknet-call` or
`alknet-channels-call` — the dial is below the protocol.
`CallCredentials` and `RemoteIdentity` live in `alknet-core` (moved
from `alknet-call` per ADR-089 §5 — both the call and channels clients
need them, and the dial must not depend on the call protocol; see
ADR-089 §5). `FingerprintPinVerifier` lives in `alknet-tls` (moved from
`alknet-call` per ADR-087 §5 — it is a TLS concern, and
`ConnectionCredentials` and `RemoteIdentity` live in `alknet-core`
(transport-level credential types, moved from `alknet-call` per
ADR-091 — the dial and the server-side transport construction both
consume them, and the dial must not depend on the call protocol for the
credential type). `FingerprintPinVerifier` lives in `alknet-tls` (moved
from `alknet-call` per ADR-087 §5 — it is a TLS concern, and
`TlsClientConfig::new` constructs it; moving it lets `alknet-call` shed
its direct `rustls` dep entirely).
@@ -606,7 +614,7 @@ alknet-worker (uses AlknetClient to dial a hub)
`AlknetClient` is one producer, but a test can hand them a
`Connection::from_stream` directly. The dependency direction is:
`alknet-client → alknet-tls → alknet-core`; the protocol crates are
parallel, not downstream of the dial. `CallCredentials` and
parallel, not downstream of the dial. `ConnectionCredentials` and
`RemoteIdentity` live in `alknet-core` (not `alknet-call`), so the dial
does not depend on the call protocol for the credential type.
@@ -632,8 +640,8 @@ let client = AlknetClient::new()
// .with_iroh(iroh_endpoint) — if iroh is needed
// 3. Derive credentials from the vault (ADR-014 — no env vars).
let creds = CallCredentials::new()
.with_tls_identity(TlsIdentity::RawKey(local_key))
let creds = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::RawKey(local_key))
.with_remote_identity(RemoteIdentity {
fingerprint: hub_fingerprint, // known peer → fingerprint pin
});
@@ -665,8 +673,9 @@ All design decisions are documented as ADRs in
| ADR | Decision | Summary |
|-----|----------|---------|
| [089](../../decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — native client dial seam | New crate `alknet-client`; client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); resolves OQ-55; `alknet/register` named, wire protocol deferred |
| [089](../../decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — native client dial seam | New crate `alknet-client`; client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); resolves OQ-55; `alknet/register` named, wire protocol deferred (§3/§5 amended by ADR-091 — dial takes `ConnectionCredentials`, not `CallCredentials`) |
| [090](../../decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | `AlknetClient` gains `with_socks5_proxy`; `dial_quic` routes via UDP ASSOCIATE, `dial_tcp_tls` via CONNECT, `dial_iroh` forces relay-only via an HTTP-to-SOCKS5 bridge; OQ-67 resolved; grounded in the quinn-proxy + iroh-proxy PoCs |
| [091](../../decisions/091-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — decouple dial from call protocol | The dial credential bundle is `ConnectionCredentials` (transport-level: `local_identity` + `remote_identity`), not `CallCredentials` (call-protocol-level); all three dial signatures unify on `&ConnectionCredentials`; `dial_iroh`'s `node_id` derived from `remote_identity`; `auth_token` stays in the call-protocol layer; `CallCredentials` stays in `alknet-call` |
## Open Questions
@@ -700,7 +709,10 @@ See [open-questions.md](../../open-questions.md) for full details.
## References
- [ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) —
the decision this spec implements
the decision this spec implements (§3/§5 amended by ADR-091)
- [ADR-091](../../decisions/091-connectioncredentials-decouple-dial-from-call.md)
— `ConnectionCredentials` (the dial credential bundle; decouples the
dial from the call protocol)
- [ADR-090](../../decisions/090-client-dial-socks5-proxy-seam.md) —
the SOCKS5 proxy seam (client-dial privacy)
- [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) —