docs(architecture): sync specs to post-extraction state (phases 0-5)

The crate-extraction migration (phases 0-5) is complete in the code;
the specs still carried forward/migration framing ("was welded",
"after the refactor", "currently duplicated", "does not exist yet",
"What moves from X to Y" tables, "Implementation ordering") that
described the migration rather than the resulting state. Updated 10
spec files to describe the current state cleanly.

Spec/code mismatches fixed:
- core/README.md: a stale paragraph said CallCredentials "stays in
  alknet-call" while ADR-091 Am. 2026-07-17 removed it. Now consistent.
- tls/README.md: TlsClientConfig API described a planned
  ClientVerifierContext + for_tcp_tls(&self) + rustls_config(&self);
  the actual code is new(&ConnectionCredentials, alpn) +
  for_quinn(self) + into_rustls_config(self). Updated to match.
- client/README.md, call/client-and-adapters.md: ConnectionCredentials
  field is tls_identity / with_tls_identity in the code, not
  local_identity / with_local_identity. Updated the specs describing
  the current API (ADR-091 body keeps local_identity as the decided
  name).
- client/README.md: dial_iroh description said the local key is
  "extracted from creds.local_identity" — the code uses the pre-built
  iroh endpoint's key (set at with_iroh time) and reads only
  creds.remote_identity for the NodeId. Fixed.
- overview.md: said core has "no quinn/iroh deps" — core keeps
  quinn/iroh for Connection::from_quinn/from_iroh. Fixed.
- call/client-and-adapters.md: a /// doc-comment block and
  pub struct RemoteIdentity were floating outside any code fence
  (orphaned closing backticks). Fixed.
- tls/README.md: TlsError sketch shows the full ADR-088 6-variant
  enum; the code has a simplified 3-variant enum. Added an
  implementation note flagging the divergence; ADR-088 shape kept as
  target.
- call/README.md: review note said "ADR-029 migration pending" (stale
  — migration landed). Updated to reflect phase 5 completion (pure
  protocol crate, no TLS/transport deps, verified against Cargo.toml).

Migration framing removed (present-state descriptions instead):
- tls/README.md: "What moves from" tables -> module-contents tables;
  "Implementation ordering / greenfield" section removed; "after the
  refactor" section -> "What AlknetEndpoint does"; references to
  extraction-source files (alknet-core/src/endpoint.rs,
  alknet-call/src/client/call_client.rs) replaced with current file
  locations (alknet-tls/src/{server,client,pem,signing}.rs).
- endpoint/README.md: "was two things welded" framing removed;
  "after the extraction" section -> "What alknet-core looks like".
- core/endpoint.md: "Historical summary" section removed; clean
  deprecation pointer.
- README.md, overview.md, open-questions.md: dates + present-tense
  cleanup.
This commit is contained in:
glm-5.2 committed 2026-07-17 14:51:28 +00:00
1 parent ddc577cd3e
commit c6eef730e4
10 files changed
+326 -381

No files matched your search

+3 -3
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-07-16 last_updated: 2026-07-17
--- ---
# Alknet Architecture # Alknet Architecture
@@ -242,7 +242,7 @@ adapter location map is now consistent: all HTTP-backed adapters
| [crates/vault/service.md](crates/vault/service.md) | stable | VaultServiceHandle lifecycle, direct dispatch, cache, error model | | [crates/vault/service.md](crates/vault/service.md) | stable | VaultServiceHandle lifecycle, direct dispatch, cache, error model |
| [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior | | [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior |
| [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery | | [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery |
| [crates/tls/README.md](crates/tls/README.md) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); `FingerprintPinVerifier` moved here from `alknet-call` (ADR-089 §5); `webpki-roots` fallback for empty platform stores (ADR-088 §5); fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) | | [crates/tls/README.md](crates/tls/README.md) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); `FingerprintPinVerifier` in `alknet-tls` (ADR-089 §5); `webpki-roots` fallback for empty platform stores (ADR-088 §5); isolates cert-reuse from transport wrappers (ADR-082) |
| [crates/client/README.md](crates/client/README.md) | draft | alknet-client crate — the native client dial seam (`AlknetClient`), client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key) unified on `&ConnectionCredentials` (ADR-091); optional SOCKS5 proxy (ADR-090 — UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only + HTTP-to-SOCKS5 bridge for iroh; OQ-67 resolved); produces `Connection` for `CallClient`/`ChannelClient` take-over; `CallClient::connect`/`ChannelClient::connect_quic` removed (dial centralized here); `alknet/register` named (wire protocol deferred, OQ-66) | | [crates/client/README.md](crates/client/README.md) | draft | alknet-client crate — the native client dial seam (`AlknetClient`), client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key) unified on `&ConnectionCredentials` (ADR-091); optional SOCKS5 proxy (ADR-090 — UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only + HTTP-to-SOCKS5 bridge for iroh; OQ-67 resolved); produces `Connection` for `CallClient`/`ChannelClient` take-over; `CallClient::connect`/`ChannelClient::connect_quic` removed (dial centralized here); `alknet/register` named (wire protocol deferred, OQ-66) |
| [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; `EndpointError` removed (vestigial); handler crates no longer transitively link quinn/iroh | | [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; `EndpointError` removed (vestigial); handler crates no longer transitively link quinn/iroh |
| [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream | | [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream |
@@ -343,7 +343,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 (§5 amended by ADR-089 — `FingerprintPinVerifier` moves to `alknet-tls`; `alknet-call` sheds TLS deps; input framing amended by ADR-091 — `ClientVerifierContext` derived from `ConnectionCredentials`, not `CallCredentials`) | | [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted (§5 amended by ADR-089 — `FingerprintPinVerifier` in `alknet-tls`; `alknet-call` sheds TLS deps; input framing amended by ADR-091 — `TlsClientConfig::new` takes `ConnectionCredentials`, not `CallCredentials`) |
| [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted (§5 added — `webpki-roots` fallback when platform store is empty; §7 references ADR-089 for handshake-error surfacing) | | [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted (§5 added — `webpki-roots` fallback when platform store is empty; §7 references ADR-089 for handshake-error surfacing) |
| [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | Accepted (resolves OQ-55; `CallClient::connect` / `ChannelClient::connect_quic` removed; §3/§5 amended by ADR-091 — dial takes `ConnectionCredentials`, not `CallCredentials`; `CallCredentials` removed per ADR-091 Am. 2026-07-17; `FingerprintPinVerifier` moved to `alknet-tls`; `ClientError` removed; `alknet-call` sheds TLS deps) | | [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | Accepted (resolves OQ-55; `CallClient::connect` / `ChannelClient::connect_quic` removed; §3/§5 amended by ADR-091 — dial takes `ConnectionCredentials`, not `CallCredentials`; `CallCredentials` removed per ADR-091 Am. 2026-07-17; `FingerprintPinVerifier` moved to `alknet-tls`; `ClientError` removed; `alknet-call` sheds TLS deps) |
| [090](decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | Accepted (§5 amended 2026-07-16 — OQ-67 resolved: iroh force-relay-only + HTTP-to-SOCKS5 bridge) | | [090](decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | Accepted (§5 amended 2026-07-16 — OQ-67 resolved: iroh force-relay-only + HTTP-to-SOCKS5 bridge) |
+5 -3
View File
@@ -1,12 +1,12 @@
--- ---
status: draft status: draft
last_updated: 2026-07-09 last_updated: 2026-07-17
review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/014/015/016/017/022/023/024), security, and pattern-consistency checks all conformant; 159 unit/integration tests green; `cargo build`, `cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo test` clean. Call-completion gap (ADR-017 client/adapter surface) addressed 2026-06-26; ADR-029 migration pending. Transport generalization sweep (ADR-064 supersedes ADR-005; ADR-065 `from_stream`) synced 2026-07-09. review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/014/015/016/017/022/023/024), security, and pattern-consistency checks all conformant; 159 unit/integration tests green; `cargo build`, `cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo test` clean. Call-completion gap (ADR-017 client/adapter surface) addressed 2026-06-26; ADR-029 migration landed. Transport generalization sweep (ADR-064 supersedes ADR-005; ADR-065 `from_stream`) synced 2026-07-09. Crate-extraction sweep (phases 0–5) landed 2026-07-17: `ConnectionCredentials`/`RemoteIdentity` in `alknet-core` (ADR-091), `CallCredentials` removed (ADR-091 Am. 2026-07-17), TLS helpers in `alknet-tls` (ADR-089 §5), `connect`/`ClientError` removed, `alknet-call` is a pure protocol crate with no TLS/transport deps.
--- ---
# alknet-call # alknet-call
Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements `ProtocolHandler` on ALPN `alknet/call`. Runs over QUIC (quinn/iroh) and, via `Connection::from_stream` (ADR-065), over any `AsyncRead + AsyncWrite` transport. Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements `ProtocolHandler` on ALPN `alknet/call`. Runs over QUIC (quinn/iroh) and, via `Connection::from_stream` (ADR-065), over any `AsyncRead + AsyncWrite` transport. A pure protocol crate — no TLS or transport deps (the dial is in `alknet-client`, the TLS config is in `alknet-tls`).
## Documents ## Documents
@@ -46,6 +46,8 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
| [030](../../decisions/030-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | `PeerId` source = `Identity.id` = `PeerEntry.peer_id` (stable); supersedes ADR-029's UUID source | | [030](../../decisions/030-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | `PeerId` source = `Identity.id` = `PeerEntry.peer_id` (stable); supersedes ADR-029's UUID source |
| [032](../../decisions/032-forwarded-for-identity.md) | Forwarded-For Identity | `forwarded_for` on `OperationContext` and `call.requested`; metadata only, never used by `AccessControl::check` | | [032](../../decisions/032-forwarded-for-identity.md) | Forwarded-For Identity | `forwarded_for` on `OperationContext` and `call.requested`; metadata only, never used by `AccessControl::check` |
| [033](../../decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Storage Boundary and Repo/Adapter Pattern | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | | [033](../../decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Storage Boundary and Repo/Adapter Pattern | Core defines repo traits + in-memory defaults; persistence adapters are separate crates |
| [089](../../decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | `CallClient::connect` removed; the dial is in `alknet-client`; `ClientError` removed; `alknet-call` sheds TLS/transport deps (pure protocol crate) |
| [091](../../decisions/091-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — Decouple Dial from Call Protocol | `ConnectionCredentials`/`RemoteIdentity` in `alknet-core` (not `alknet-call`); `CallCredentials` removed (Am. 2026-07-17); `auth_token` is a per-request payload field |
## Relevant Open Questions ## Relevant Open Questions
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-07-09 last_updated: 2026-07-17
--- ---
# alknet-call — Client and Adapters # alknet-call — Client and Adapters
@@ -251,12 +251,12 @@ attribution, filtered by the calling peer's authorization). See
The credential dimensions are split across two layers (ADR-091, amended The credential dimensions are split across two layers (ADR-091, amended
2026-07-17): 2026-07-17):
- **`ConnectionCredentials`** (in `alknet-core`, moved from - **`ConnectionCredentials`** (in `alknet-core`, per ADR-091) — the
`alknet-call` per ADR-091) — the **transport-level** credential **transport-level** credential bundle, consumed by the dial
bundle, consumed by the dial (`AlknetClient`). Carries the two (`AlknetClient`). Carries the two transport-identity dimensions:
transport-identity dimensions: `local_identity` (the local node's `tls_identity` (the local node's `TlsIdentity`) and `remote_identity`
`TlsIdentity`) and `remote_identity` (the expected fingerprint). The (the expected fingerprint). The dial does not depend on the call
dial does not depend on the call protocol for this type. protocol for this type.
- **`auth_token`** — a **per-request payload field**, not a - **`auth_token`** — a **per-request payload field**, not a
call-protocol credential bundle. `Dispatcher::resolve_identity` call-protocol credential bundle. `Dispatcher::resolve_identity`
reads `payload.get("auth_token")` on each `call.requested` payload. reads `payload.get("auth_token")` on each `call.requested` payload.
@@ -276,7 +276,7 @@ variables. The transport-identity dimensions (ADR-017 §7):
```rust ```rust
// Transport-level (alknet-core, consumed by the dial — ADR-091) // Transport-level (alknet-core, consumed by the dial — ADR-091)
pub struct ConnectionCredentials { pub struct ConnectionCredentials {
pub local_identity: Option<TlsIdentity>, // RFC 7250 raw key or X.509 pub tls_identity: Option<TlsIdentity>, // RFC 7250 raw key or X.509
pub remote_identity: Option<RemoteIdentity>, // expected fingerprint (None = CA path / fail-closed) pub remote_identity: Option<RemoteIdentity>, // expected fingerprint (None = CA path / fail-closed)
} }
@@ -286,31 +286,29 @@ pub struct ConnectionCredentials {
// Dispatcher::resolve_identity reads payload.get("auth_token"). // Dispatcher::resolve_identity reads payload.get("auth_token").
``` ```
There is no call-protocol credential bundle. `CallCredentials` is `RemoteIdentity` (ADR-017 §7, extended by ADR-034 §2) carries a
removed. The transport dimensions (`local_identity`, `remote_identity`) fingerprint string the assembly layer derives from `Capabilities` when
moved to `ConnectionCredentials` in `alknet-core` per ADR-091. the local node has a `PeerEntry` for the remote (the known-peer case →
fingerprint pin). `remote_identity: None` is the **public X.509
endpoint** case: the local node has no `PeerEntry` for the remote, so
there is no fingerprint to pin. Combined with an X.509 transport, `None`
selects CA verification (`WebPkiServerVerifier`) per the
verifier-selection rule in ADR-034 §3. Combined with an Ed25519
raw-key transport, `None` fails closed (raw-key remotes are always
known peers — no CA to fall back to). The `Option` is load-bearing, not
cosmetic: `Some(fingerprint)` means "pin this" (known peer), `None`
means "trust the CA or fail" (unknown remote). An implementer must not
default `remote_identity` to a placeholder value to "satisfy" the field
— `None` is a real state that drives verifier selection.
/// Expected identity of the remote node (ADR-017 §7, extended by ```rust
/// ADR-034 §2). Carries a fingerprint string the assembly layer
/// derives from `Capabilities` when the local node has a `PeerEntry`
/// for the remote (the known-peer case → fingerprint pin).
///
/// `remote_identity: None` is the **public X.509 endpoint** case: the
/// local node has no `PeerEntry` for the remote, so there is no
/// fingerprint to pin. Combined with an X.509 transport, `None`
/// selects CA verification (`WebPkiServerVerifier`) per the
/// verifier-selection rule in ADR-034 §3. Combined with an Ed25519
/// raw-key transport, `None` fails closed (raw-key remotes are always
/// known peers — no CA to fall back to).
///
/// The `Option` is therefore load-bearing, not cosmetic: `Some(fingerprint)`
/// means "pin this" (known peer), `None` means "trust the CA or fail"
/// (unknown remote). An implementer must not default `remote_identity`
/// to a placeholder value to "satisfy" the field — `None` is a real
/// state that drives verifier selection.
pub struct RemoteIdentity { pub fingerprint: String } pub struct RemoteIdentity { pub fingerprint: String }
``` ```
There is no call-protocol credential bundle. `CallCredentials` is
removed. The transport dimensions (`tls_identity`, `remote_identity`)
are in `ConnectionCredentials` in `alknet-core` per ADR-091.
- **TLS identity** — the local node's Ed25519 raw key (RFC 7250) or X.509 cert, - **TLS identity** — the local node's Ed25519 raw key (RFC 7250) or X.509 cert,
derived from the vault at startup (ADR-020, ADR-026, ADR-027). derived from the vault at startup (ADR-020, ADR-026, ADR-027).
- **Auth token** — an opaque call-protocol-level token, decrypted from the - **Auth token** — an opaque call-protocol-level token, decrypted from the
@@ -783,7 +781,7 @@ Based on the gap analysis and the downstream unblock chain:
| Abort cascade for nested calls | [ADR-016](../../decisions/016-abort-cascade-for-nested-calls.md) | Cross-node abort through `from_call` forwarding handler's `parent_request_id` | | Abort cascade for nested calls | [ADR-016](../../decisions/016-abort-cascade-for-nested-calls.md) | Cross-node abort through `from_call` forwarding handler's `parent_request_id` |
| Operation error schemas | [ADR-023](../../decisions/023-operation-error-schemas.md) | `error_schemas` mirrored by `from_call` from remote op's spec | | Operation error schemas | [ADR-023](../../decisions/023-operation-error-schemas.md) | `error_schemas` mirrored by `from_call` from remote op's spec |
| Streaming handler for subscriptions | [ADR-049](../../decisions/049-streaming-handler-for-subscriptions.md) | `from_call` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`) that calls `CallConnection::subscribe()` and forwards the remote stream; `Query`/`Mutation` stay `HandlerKind::Once` | | Streaming handler for subscriptions | [ADR-049](../../decisions/049-streaming-handler-for-subscriptions.md) | `from_call` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`) that calls `CallConnection::subscribe()` and forwards the remote stream; `Query`/`Mutation` stay `HandlerKind::Once` |
| TLS identity redesign | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | RFC 7250 raw key / X.509 cert dimensions of `CallCredentials` | | TLS identity redesign | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | RFC 7250 raw key / X.509 cert dimensions of the local `TlsIdentity` (now carried by `ConnectionCredentials.tls_identity`) |
| Outgoing-only X.509 and three peer roles | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Public X.509 endpoint is not a `PeerEntry` on the client side (no `PeerId`, not in peer graph); client-side verifier by `PeerEntry` presence (CA vs fingerprint pin); hub = mixed-fingerprint `PeerEntry` | | Outgoing-only X.509 and three peer roles | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Public X.509 endpoint is not a `PeerEntry` on the client side (no `PeerId`, not in peer graph); client-side verifier by `PeerEntry` presence (CA vs fingerprint pin); hub = mixed-fingerprint `PeerEntry` |
| HD derivation for encryption keys | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Vault-derived TLS identity material | | HD derivation for encryption keys | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Vault-derived TLS identity material |
| Vault key model | [ADR-026](../../decisions/026-vault-key-model-hd-derivation.md) | Vault-derived TLS identity material | | Vault key model | [ADR-026](../../decisions/026-vault-key-model-hd-derivation.md) | Vault-derived TLS identity material |
+24 -25
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-07-16 last_updated: 2026-07-17
--- ---
# alknet-client # alknet-client
@@ -186,12 +186,13 @@ impl AlknetClient {
/// Iroh dial. Dials on `alpn` via the iroh endpoint. The iroh path /// Iroh dial. Dials on `alpn` via the iroh endpoint. The iroh path
/// does NOT use `TlsClientConfig` — iroh has its own TLS (shares the /// does NOT use `TlsClientConfig` — iroh has its own TLS (shares the
/// `Ed25519SecretKey`, not the rustls config — ADR-087 §3, ADR-089 /// `Ed25519SecretKey`, not the rustls config — ADR-087 §3, ADR-089
/// §3). The local key is extracted from `creds.local_identity`; the /// §3). The local key is on the pre-built iroh endpoint (set when
/// remote `NodeId` is derived from `creds.remote_identity.fingerprint` /// `with_iroh` configured it); the remote `NodeId` is derived from
/// (`ed25519:<hex>` → `NodeId::from_bytes`). The verifier is iroh's /// `creds.remote_identity.fingerprint` (`ed25519:<hex>` →
/// `NodeId` match (fingerprint pin by another name — ADR-034 §3). /// `NodeId::from_bytes`). The verifier is iroh's `NodeId` match
/// An unknown iroh remote fails closed (no CA). Feature-gated on /// (fingerprint pin by another name — ADR-034 §3). An unknown iroh
/// `iroh`. /// remote fails closed (no CA — `remote_identity` must be `Some`).
/// Feature-gated on `iroh`.
#[cfg(feature = "iroh")] #[cfg(feature = "iroh")]
pub async fn dial_iroh( pub async fn dial_iroh(
&self, &self,
@@ -206,10 +207,10 @@ 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 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 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 (`aws_lc_rs`). The iroh dial is the exception: iroh has its own TLS and
takes the `Ed25519SecretKey` directly (extracted from takes the `Ed25519SecretKey` directly (on the pre-built iroh endpoint,
`creds.local_identity`), not a `rustls::ClientConfig`. The consistency not extracted from `creds` at dial time), not a `rustls::ClientConfig`.
is in the rule (ADR-034), not in the type — the same exception as the The consistency is in the rule (ADR-034), not in the type — the same
server side (ADR-082, ADR-087 §3). All three dials take exception as the server side (ADR-082, ADR-087 §3). All three dials take
`&ConnectionCredentials` — the unified transport-level credential `&ConnectionCredentials` — the unified transport-level credential
bundle (ADR-091). bundle (ADR-091).
@@ -422,22 +423,20 @@ two lines: `client.dial_quic(...).await?` then
### Iroh — shares the key, not the config (client side too) ### Iroh — shares the key, not the config (client side too)
The iroh client dial, like the iroh server side (ADR-082, ADR-087 §3), The iroh client dial, like the iroh server side (ADR-082, ADR-087 §3),
does not consume a `rustls::ClientConfig`. It takes the does not consume a `rustls::ClientConfig`. The `Ed25519SecretKey` is set
`Ed25519SecretKey` directly and feeds it to on the pre-built iroh endpoint at `with_iroh` time (the assembly layer
`iroh::SecretKey::from_bytes`. Iroh handles TLS internally. The reads it from `StaticConfig` and feeds it to
verifier is iroh's `NodeId` match — the remote's `NodeId` (Ed25519 `iroh::Endpoint::builder().secret_key()`). The `dial_iroh` method
consumes only `creds.remote_identity` (deriving the remote `NodeId`);
the local key is not in `ConnectionCredentials` for the iroh path — it
is on the endpoint. The dial signature is unified — all three dials
take `&ConnectionCredentials` (ADR-091) — and the iroh dial simply
ignores the `tls_identity` field (the key is already on the endpoint).
The verifier is iroh's `NodeId` match — the remote's `NodeId` (Ed25519
public key) is verified against the expected `NodeId`, which is public key) is verified against the expected `NodeId`, which is
fingerprint-pinning by another name. An unknown iroh remote fails fingerprint-pinning by another name. An unknown iroh remote fails
closed (no CA to fall back to — ADR-034 §3, Assumption 1). closed (no CA to fall back to — ADR-034 §3, Assumption 1).
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) ### Non-Rust native clients (out of scope)
The wire protocols (channels 9-byte chunk format — ADR-071; call The wire protocols (channels 9-byte chunk format — ADR-071; call
@@ -641,7 +640,7 @@ let client = AlknetClient::new()
// 3. Derive credentials from the vault (ADR-014 — no env vars). // 3. Derive credentials from the vault (ADR-014 — no env vars).
let creds = ConnectionCredentials::new() let creds = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::RawKey(local_key)) .with_tls_identity(TlsIdentity::RawKey(local_key))
.with_remote_identity(RemoteIdentity { .with_remote_identity(RemoteIdentity {
fingerprint: hub_fingerprint, // known peer → fingerprint pin fingerprint: hub_fingerprint, // known peer → fingerprint pin
}); });
@@ -675,7 +674,7 @@ All design decisions are documented as ADRs in
|-----|----------|---------| |-----|----------|---------|
| [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`) | | [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 | | [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` is a per-request payload field; `CallCredentials` removed per Am. 2026-07-17 | | [091](../../decisions/091-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — decouple dial from call protocol | The dial credential bundle is `ConnectionCredentials` (transport-level: `tls_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` is a per-request payload field; `CallCredentials` removed per Am. 2026-07-17 |
## Open Questions ## Open Questions
+16 -14
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-07-15 last_updated: 2026-07-17
--- ---
# alknet-core # alknet-core
@@ -8,19 +8,21 @@ last_updated: 2026-07-15
Shared types, auth, config, and identity for ALPN-based protocol Shared types, auth, config, and identity for ALPN-based protocol
dispatch. Every handler crate depends on `alknet-core` for dispatch. Every handler crate depends on `alknet-core` for
`ProtocolHandler`, `Connection`, `AuthContext`, `IdentityProvider`, and `ProtocolHandler`, `Connection`, `AuthContext`, `IdentityProvider`, and
config types. The endpoint (`AlknetEndpoint`, `HandlerRegistry`) has config types. The endpoint (`AlknetEndpoint`, `HandlerRegistry`) lives
been extracted to [`alknet-endpoint`](../endpoint/README.md) (ADR-083 in [`alknet-endpoint`](../endpoint/README.md) (ADR-083 Amendment
Amendment 2026-07-15; `EndpointError` is removed — both variants were 2026-07-15; `EndpointError` is removed — both variants were vestigial);
vestigial); core no longer carries the accept-loop runner or its core does not carry the accept-loop runner or its transport deps
transport deps (quinn, iroh, rcgen, rustls-acme). (quinn, iroh, rcgen, rustls-acme). `Connection::from_quinn` /
`Connection::from_quinn` / `from_iroh` stay in core's `types.rs` as `from_iroh` are in core's `types.rs` as shared constructors (gated on
shared constructors (gated on core's `quinn` / `iroh` features). core's `quinn` / `iroh` features).
`ConnectionCredentials` and `RemoteIdentity` move to `alknet-core`
(from `alknet-call`, per ADR-091) — the transport-level credential `ConnectionCredentials` and `RemoteIdentity` live in `alknet-core` (per
bundle consumed by the dial (`alknet-client`) and by server-side ADR-091) — the transport-level credential bundle consumed by the dial
transport construction. `ConnectionCredentials` (the transport-level credential (`alknet-client`) and by server-side transport construction. There is no
bundle, including `auth_token`) stays in `alknet-call` — the dial does call-protocol credential bundle: `CallCredentials` is removed (ADR-091
not carry call-protocol dimensions. Am. 2026-07-17 — its `auth_token` field had no reader; `auth_token` is
a per-request payload field on `call.requested`, not a transport
credential).
## Documents ## Documents
+10 -29
View File
@@ -1,45 +1,26 @@
--- ---
status: deprecated status: deprecated
last_updated: 2026-07-15 last_updated: 2026-07-17
--- ---
# Endpoint (moved to `alknet-endpoint`) # Endpoint (in `alknet-endpoint`)
> **This document is deprecated.** The `AlknetEndpoint` and > **This document is deprecated.** The `AlknetEndpoint` and
> `HandlerRegistry` types have been extracted from `alknet-core` into a > `HandlerRegistry` types live in a separate crate, `alknet-endpoint`
> new crate `alknet-endpoint` (ADR-083 Amendment 2026-07-15). > (ADR-083 Amendment 2026-07-15). `EndpointError` is removed (both
> `EndpointError` is removed (both variants were vestigial). The > variants were vestigial). The canonical spec is
> canonical spec is now
> [`crates/endpoint/README.md`](../endpoint/README.md). > [`crates/endpoint/README.md`](../endpoint/README.md).
> >
> The shared types the endpoint imports (`ProtocolHandler`, > The shared types the endpoint imports (`ProtocolHandler`,
> `Connection`, `AuthContext`, `IdentityProvider`, `DynamicConfig`) stay > `Connection`, `AuthContext`, `IdentityProvider`, `DynamicConfig`) are
> in `alknet-core` — see [`core-types.md`](core-types.md), > in `alknet-core` — see [`core-types.md`](core-types.md),
> [`auth.md`](auth.md), [`config.md`](config.md). > [`auth.md`](auth.md), [`config.md`](config.md).
## Historical summary ## What is in `alknet-core`
The endpoint was originally in `alknet-core/endpoint.rs` as the central `Connection::from_quinn` / `from_iroh` are in core's `types.rs` — they
runtime type — a multi-transport accept-loop runner that dispatches
incoming connections by ALPN (ADR-010, ADR-083). ADR-082 extracted the
TLS setup code to `alknet-tls`; ADR-083 restructured the endpoint to
take pre-built transports via `with_quinn` / `with_iroh` /
`with_tcp_tls` (no TLS config); ADR-083 Amendment 2026-07-15 extracted
the endpoint itself into `alknet-endpoint` so that handler crates no
longer transitively link quinn/iroh/rcgen via core.
The endpoint's semantics — ALPN dispatch, `HandlerRegistry`, accept
loops, public `dispatch` for SSH/WT, graceful shutdown — are unchanged
by the extraction. See
[`crates/endpoint/README.md`](../endpoint/README.md) for the current
spec and [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md)
for the full decision.
## What stayed in `alknet-core`
`Connection::from_quinn` / `from_iroh` stay in core's `types.rs` — they
are shared-type constructors used by both the endpoint's accept loop are shared-type constructors used by both the endpoint's accept loop
(server) and `alknet-client`'s dial (client, ADR-089), gated on core's (server, in `alknet-endpoint`) and `alknet-client`'s dial (client,
`quinn` / `iroh` features. See ADR-089), gated on core's `quinn` / `iroh` features. See
[ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) §"The [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) §"The
`quinn` feature split". `quinn` feature split".
+24 -24
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-07-15 last_updated: 2026-07-17
--- ---
# alknet-endpoint # alknet-endpoint
@@ -22,24 +22,23 @@ It does not build transports and does not build TLS configs — the
assembly layer does both (transports from `alknet-tls`'s assembly layer does both (transports from `alknet-tls`'s
`TlsServerConfig`, per ADR-082). `TlsServerConfig`, per ADR-082).
`alknet-endpoint` is extracted from `alknet-core` (ADR-083 Amendment `alknet-endpoint` is a leaf consumer of `alknet-core`'s shared types
2026-07-15). The extraction is structural pruning, not a refactor: the (it imports `auth`, `config`, `types`; nothing in core imports from
endpoint is a leaf consumer of core's shared types (it imports `auth`, it), depended on by the assembly layer — a different audience than the
`config`, `types`; nothing in core imports from it), depended on by a shared types (every handler crate). No handler crate imports
different audience (the assembly layer) than the shared types (every `AlknetEndpoint` or `HandlerRegistry` — they depend on `alknet-core`
handler crate). No handler crate imports `AlknetEndpoint` or for `ProtocolHandler`, `Connection`, `AuthContext`, and types only.
`HandlerRegistry` — they depend on `alknet-core` for This keeps the heavy transport deps (quinn, iroh, tokio-rustls) out of
`ProtocolHandler`, `Connection`, `AuthContext`, and types only. the handler crates' dep closure. (`EndpointError` is removed — see
(`EndpointError` is removed — see below.) below.)
## Why ## Why
`alknet-core` was two things welded: shared types (depended on by every Separating the endpoint from the shared-types crate lets `alknet-core`
handler crate) + the endpoint (depended on by zero handler crates). be the lightweight types+auth+config crate that every handler crate
Extracting the endpoint into `alknet-endpoint` lets core shed the heavy wants, while the accept-loop runner (which only the assembly layer
transport deps (quinn, iroh, rcgen, rustls-acme) and become the depends on) carries the heavy transport deps. See
lightweight types+auth+config crate the handler crates actually want. [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md)
See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md)
§"Amendment 2026-07-15 — crate extraction" for the full rationale, §"Amendment 2026-07-15 — crate extraction" for the full rationale,
including the dependency data and the symmetry with `alknet-client`. including the dependency data and the symmetry with `alknet-client`.
@@ -332,15 +331,16 @@ The endpoint takes the pre-built transports; the assembly layer built
them from `alknet-tls`'s `TlsServerConfig`s. The endpoint does not see them from `alknet-tls`'s `TlsServerConfig`s. The endpoint does not see
`alknet-tls` — it sees `quinn::Endpoint` and `TlsAcceptor`. `alknet-tls` — it sees `quinn::Endpoint` and `TlsAcceptor`.
## What `alknet-core` looks like after the extraction ## What `alknet-core` looks like
Core loses the endpoint module (~1600 LOC) and 5 heavy deps (`quinn`, Core is the lightweight types+auth+config+ownership+store+fingerprint
`iroh`, `rcgen`, `rustls-pemfile`, `rustls-acme`). The remaining surface crate (~3200 LOC, no `quinn`/`iroh`/`rcgen`/`rustls-pemfile`/
is the lightweight types+auth+config+ownership+store+fingerprint crate. `rustls-acme` deps). The endpoint module is not in core; the accept
See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) loops are here. See [ADR-083](../../decisions/083-endpoint-as-accept-
§"Amendment 2026-07-15 — crate extraction" §"What `alknet-core` looks loop-runner.md) §"Amendment 2026-07-15 — crate extraction" §"What
like after" for the module-level table and the `quinn` feature split `alknet-core` looks like after" for the module-level table and the
(`Connection::from_quinn` stays in core; the accept loop moves here). `quinn` feature split (`Connection::from_quinn` stays in core; the
accept loop is here).
## Design Decisions ## Design Decisions
+209 -247
View File
@@ -1,6 +1,6 @@
--- ---
status: reviewed status: reviewed
last_updated: 2026-07-15 last_updated: 2026-07-17
--- ---
# alknet-tls # alknet-tls
@@ -17,16 +17,9 @@ one verifier rule, N clients.
## What ## What
`alknet-tls` extracts the TLS setup that was welded to the quinn endpoint `alknet-tls` provides `TlsServerConfig` and `TlsClientConfig` —
in `alknet-core`. The existing code (`endpoint.rs`) builds a shareable TLS setup types that a deployment builds once and hands to
`rustls::ServerConfig` from a `TlsIdentity`, then **consumes** it into a whichever transports it runs:
`quinn::ServerConfig` — making it impossible to reuse the same cert for a
TCP+TLS listener. ACME is worse: the `AcmeState` task is spawned inside
the quinn endpoint, so a TCP+TLS listener would need its own ACME state
machine (two orders for the same domain, two cert caches, potential
Let's Encrypt rate-limiting).
`alknet-tls` fixes this by making the TLS config **shareable**:
```rust ```rust
pub struct TlsServerConfig { pub struct TlsServerConfig {
@@ -64,15 +57,17 @@ transports.
## Why ## Why
`alknet-core` builds the `rustls::ServerConfig` once, then consumes it Without a shareable TLS config, a `rustls::ServerConfig` built for one
into a `quinn::ServerConfig` — making the cert unreusable for a TCP+TLS transport gets consumed into that transport's wrapper (e.g.
listener. For ACME the problem is worse: the `AcmeState` task is spawned `quinn::ServerConfig`), making the cert unreusable for a TCP+TLS
inside the quinn endpoint, so a TCP+TLS listener would need a second ACME listener. For ACME the problem is worse: the `AcmeState` task spawned
state machine for the same domain (duplicate orders, divergent cert inside the quinn endpoint means a TCP+TLS listener would need a second
caches, Let's Encrypt rate-limit risk). The full rationale, including ACME state machine for the same domain (duplicate orders, divergent cert
the cert-reuse problem, the ACME worst case, and the three reasons a caches, Let's Encrypt rate-limit risk). `alknet-tls` isolates TLS setup
separate crate is the right shape (dependency isolation, ACME weight, from the transport so one config serves all transports. The full
quinn/iroh having their own TLS), is in rationale, including the cert-reuse problem, the ACME worst case, and
the three reasons a separate crate is the right shape (dependency
isolation, ACME weight, quinn/iroh having their own TLS), is in
[ADR-082](../../decisions/082-alknet-tls-extraction.md). [ADR-082](../../decisions/082-alknet-tls-extraction.md).
### The three endpoint types (ADR-086) ### The three endpoint types (ADR-086)
@@ -107,64 +102,73 @@ transports the deployment runs.
## Architecture ## Architecture
### What moves from `alknet-core` to `alknet-tls` (server side) ### Server-side contents
| Component | Current location | New location | The server-side TLS setup — `rustls::ServerConfig` construction, cert
|-----------|-----------------|-------------| resolvers, the ACME state machine — is in `alknet-tls/src/server.rs`.
| `TlsIdentity` enum | `alknet-core/config.rs` | **stays in core** (it's a config type) | These components were originally part of `alknet-core`'s endpoint
| `Ed25519SecretKey` | `alknet-core/config.rs` | **stays in core** (config type) | module (quinn-gated); ADR-082 moved them into `alknet-tls` so a
| `build_rustls_server_config()` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` (unconditional) | `TlsServerConfig` is shareable across transports rather than consumed
| `build_quinn_server_config_from_rustls()` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` (`for_quinn()` — wraps rustls config in `QuicServerConfig`) | into a single transport's wrapper.
| `TlsSetup` (ACME state machine) | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` (the `TlsServerConfig::new` ACME path) |
| `RawKeyCertResolver` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` |
| `Ed25519SigningKey` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` (consolidates with the `alknet-call` duplicate — see client table) |
| `AcceptAnyCertVerifier` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` |
| `SelfSignedCert` / `generate_self_signed_cert()` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` |
| `load_cert_chain()` / `load_private_key()` | `alknet-core/endpoint.rs` | `alknet-tls` (consolidates with the `alknet-call` duplicate — see client table) |
| `fingerprint.rs` | `alknet-core/fingerprint.rs` | **stays in core** (shared by server + client; the client-side `FingerprintPinVerifier` is now in `alknet-tls` per ADR-089 §5, so both consumers are co-located; production code uses `sha2` + manual DER only — `rustls` is test-only. See OQ-59 — the original dep-edge concern that motivated keeping `fingerprint.rs` in core is dissolved by ADR-089 §5.) |
### What moves from `alknet-call` to `alknet-tls` (client side) | Component | Notes |
|-----------|-------|
| `TlsServerConfig` | The central type — wraps `rustls::ServerConfig` + the optional ACME task handle |
| `build_rustls_server_config()` | Unconditional; called by `TlsServerConfig::new` |
| `for_quinn()` | Wraps the rustls config in a `QuicServerConfig` (feature-gated on `quinn`) |
| `TlsSetup` / ACME path | The `TlsServerConfig::new` ACME branch spawns the state-machine task |
| `RawKeyCertResolver` | Presents an Ed25519 key as an RFC 7250 raw public key server cert |
| `Ed25519SigningKey` | One copy in `alknet-tls`, shared by server + client (see below) |
| `AcceptAnyCertVerifier` | Accepts any client cert and extracts the fingerprint (raw-key servers don't pin client certs) |
| `SelfSignedCert` / `generate_self_signed_cert()` | The dev `SelfSigned` identity path |
| `load_cert_chain()` / `load_private_key()` | In `pem.rs`; one copy, shared by server + client |
`TlsClientConfig::new` (ADR-087) centralizes the client-side verifier The config types `TlsIdentity` and `Ed25519SecretKey` live in
selection + provider wiring + client-auth cert presentation that `alknet-core` (`config.rs`) — `StaticConfig` holds a `TlsIdentity`, and
currently lives in `alknet-call/src/client/call_client.rs`. The config types belong in core. `alknet-tls` imports them. `fingerprint.rs`
extraction is the client-side analogue of the server-side lives in core because it is shared by both the server path (the
`endpoint.rs` extraction above. endpoint extracts the fingerprint from the client cert) and the client
path (`FingerprintPinVerifier`, in `alknet-tls`, matches the server's
| Component | Current location | New location | cert against a pinned fingerprint). The production code in
|-----------|-----------------|-------------| `fingerprint.rs` uses only `sha2` and manual DER parsing; the
| `build_quinn_client_config()` | `alknet-call/client/call_client.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` (`TlsClientConfig::new` + `for_quinn()`) | `rustls::sign` usage is in the test helper only. See OQ-59 — the
| `build_client_auth()` | `alknet-call/client/call_client.rs` | `alknet-tls` (client-auth cert resolver construction inside `TlsClientConfig::new`) | original dep-edge concern that motivated keeping `fingerprint.rs` in
| `select_server_verifier()` | `alknet-call/client/call_client.rs` | `alknet-tls` (ADR-034 verifier selection inside `TlsClientConfig::new`) | core is dissolved by ADR-089 §5 (`FingerprintPinVerifier` is in
| `load_platform_root_cert_store()` | `alknet-call/client/call_client.rs` | `alknet-tls` (the unknown-X.509-remote CA path inside `TlsClientConfig::new`) |
| `FingerprintPinVerifier` | `alknet-call/client/call_client.rs` | `alknet-tls` (moved — it is a TLS concern; `TlsClientConfig::new` constructs it; moving it lets `alknet-call` shed its direct `rustls` dep entirely per ADR-089 §5) |
| `Ed25519SigningKey` (client-side copy) | `alknet-call/client/call_client.rs` | `alknet-tls` (consolidates with the `endpoint.rs` duplicate — one copy in `alknet-tls`) |
| `RawKeyClientCertResolver` | `alknet-call/client/call_client.rs` | `alknet-tls` |
| `NoClientCertResolver` | `alknet-call/client/call_client.rs` | `alknet-tls` |
| `load_cert_chain()` / `load_private_key()` (client-side copies) | `alknet-call/client/call_client.rs` | `alknet-tls` (consolidates with the `endpoint.rs` duplicate — one copy in `alknet-tls`) |
| `CallClient::connect` | `alknet-call/client/call_client.rs` | **removed** (ADR-089 §5 — the dial is extracted to `AlknetClient`; `CallClient` keeps only `spawn_dispatch`, shedding its TLS/transport deps) |
**Consolidation note.** `Ed25519SigningKey` and
`load_cert_chain`/`load_private_key` are currently **duplicated** across
`endpoint.rs` (server) and `call_client.rs` (client). After extraction
there is one copy of each in `alknet-tls`, used by both
`TlsServerConfig::new` and `TlsClientConfig::new`. Both call sites
(`endpoint.rs`'s server path, `call_client.rs`'s client path) are
updated to import from `alknet-tls`.
`TlsIdentity` and `Ed25519SecretKey` stay in core because they're config
types — `StaticConfig` holds a `TlsIdentity`, and config types belong in
core. `alknet-tls` re-exports them for convenience. `fingerprint.rs` stays
in core because it's shared by both the server path (endpoint extracts
fingerprint from the client cert) and the client path
(`FingerprintPinVerifier` — now in `alknet-tls` per ADR-089 §5 —
matches the server's cert against a pinned fingerprint).
The production code in `fingerprint.rs` uses only `sha2` and manual DER
parsing — the `rustls::sign` usage is in the test helper only. See OQ-59
(the original dep-edge concern that motivated keeping `fingerprint.rs`
in core is dissolved by ADR-089 §5 — `FingerprintPinVerifier` moved to
`alknet-tls`, so its consumers are co-located). `alknet-tls`, so its consumers are co-located).
### Client-side contents
The client-side TLS setup — verifier selection, client-auth cert
presentation, provider wiring — is in `alknet-tls/src/client.rs`.
These components were originally part of `alknet-call`'s client
module (quinn-gated); ADR-087 / ADR-089 §5 moved them into `alknet-tls`
so `alknet-call` has no direct `rustls` dep and the verifier selection
is shared across all outbound dials.
| Component | Notes |
|-----------|-------|
| `TlsClientConfig::new` | Builds a `rustls::ClientConfig` from `ConnectionCredentials` + ALPN; runs ADR-034 verifier selection + ADR-084 provider wiring + client-auth cert presentation |
| `for_quinn()` | Wraps the rustls config in a `quinn::ClientConfig` (feature-gated on `quinn`) |
| `into_rustls_config()` | Returns the inner `rustls::ClientConfig` for consumers that build their own transport wrapper (e.g. `dial_tcp_tls` wraps it in a `TlsConnector`) |
| `build_client_auth()` | Constructs the client-auth cert resolver inside `TlsClientConfig::new` |
| `select_server_verifier()` | ADR-034 verifier selection (fingerprint pin / CA / fail-closed) inside `TlsClientConfig::new` |
| `load_platform_root_cert_store()` | The unknown-X.509-remote CA path inside `TlsClientConfig::new` |
| `FingerprintPinVerifier` | A TLS concern; `TlsClientConfig::new` constructs it. Locating it in `alknet-tls` lets `alknet-call` have no direct `rustls` dep (ADR-089 §5) |
| `RawKeyClientCertResolver` | Presents the local key as an RFC 7250 raw public key client cert |
| `NoClientCertResolver` | The no-client-cert path |
| `Ed25519SigningKey` | One copy in `alknet-tls` (`signing.rs`), shared by server + client |
| `load_cert_chain()` / `load_private_key()` | In `pem.rs`; one copy, shared by server + client |
`Ed25519SigningKey` and `load_cert_chain`/`load_private_key` are single
copies in `alknet-tls`, used by both `TlsServerConfig::new` and
`TlsClientConfig::new`. Before the extraction these were duplicated
across the server (in core's endpoint module) and the client (in call's
client module); the extraction consolidated them.
`CallClient::connect` is removed (ADR-089 §5) — the dial is in
`AlknetClient` (`alknet-client`); `CallClient` keeps only
`spawn_dispatch`, and `alknet-call` has no TLS/transport deps.
### `TlsServerConfig` ### `TlsServerConfig`
The central type. Built once from a `TlsIdentity` + ALPN list, shared The central type. Built once from a `TlsIdentity` + ALPN list, shared
@@ -230,12 +234,12 @@ architecture decision.
### Behavior-preservation invariants ### Behavior-preservation invariants
The extraction must preserve these load-bearing TLS behaviors. They These load-bearing TLS behaviors must be preserved. They originate from
originate from [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md), [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md),
which established the `TlsIdentity` model, the `Acme` variant, and the which established the `TlsIdentity` model, the `Acme` variant, and the
`acme-tls/1` ALPN challenge handling. An implementer who omits any of `acme-tls/1` ALPN challenge handling. Omitting any of them produces a
these produces a crate that compiles and passes type-checks but silently crate that compiles and passes type-checks but silently changes TLS
changes TLS behavior: behavior:
- **`max_early_data_size = u32::MAX`** on all server config paths (X509, - **`max_early_data_size = u32::MAX`** on all server config paths (X509,
RawKey, SelfSigned, ACME). Enables 0-RTT / early data. Omitting it RawKey, SelfSigned, ACME). Enables 0-RTT / early data. Omitting it
@@ -278,8 +282,8 @@ impl TlsServerConfig {
/// build their own transport-specific wrapper not covered by /// build their own transport-specific wrapper not covered by
/// `for_quinn` / `for_tcp_tls`. No current consumer (iroh reads the /// `for_quinn` / `for_tcp_tls`. No current consumer (iroh reads the
/// `Ed25519SecretKey` directly, not the rustls config — see "Iroh: /// `Ed25519SecretKey` directly, not the rustls config — see "Iroh:
/// shares the key, not the rustls config" below); kept as a /// shares the key, not the rustls config" below); retained for
/// forward-looking accessor for future transport wrappers. /// transport wrappers that do not fit `for_quinn` / `for_tcp_tls`.
pub fn rustls_config(&self) -> &rustls::ServerConfig; pub fn rustls_config(&self) -> &rustls::ServerConfig;
} }
``` ```
@@ -291,7 +295,7 @@ the `Endpoint`, using RFC 7250 raw keys. It does not consume a
`rustls::ServerConfig` — it takes an `iroh::SecretKey` and handles TLS `rustls::ServerConfig` — it takes an `iroh::SecretKey` and handles TLS
internally. So `alknet-tls` does not have a `for_iroh()` method. Instead, internally. So `alknet-tls` does not have a `for_iroh()` method. Instead,
the assembly layer reads the `Ed25519SecretKey` from `StaticConfig` the assembly layer reads the `Ed25519SecretKey` from `StaticConfig`
(stays in core) and passes it to iroh's `Endpoint::builder().secret_key()` (lives in core) and passes it to iroh's `Endpoint::builder().secret_key()`
directly. `alknet-tls` is involved only when iroh is not the sole directly. `alknet-tls` is involved only when iroh is not the sole
transport — in that case, the same `Ed25519SecretKey` feeds both transport — in that case, the same `Ed25519SecretKey` feeds both
`TlsServerConfig::new(TlsIdentity::RawKey(key), ...)` (for quinn/TCP) and `TlsServerConfig::new(TlsIdentity::RawKey(key), ...)` (for quinn/TCP) and
@@ -346,22 +350,20 @@ alknet-tls
`rustls-native-certs` and `webpki-roots` are always-present deps (not `rustls-native-certs` and `webpki-roots` are always-present deps (not
feature-gated) because the unknown-X.509-remote CA-verification path in feature-gated) because the unknown-X.509-remote CA-verification path in
`TlsClientConfig::new` is needed by any client dialing a public X.509 `TlsClientConfig::new` is needed by any client dialing a public X.509
endpoint, regardless of transport (QUIC or TCP+TLS). In the endpoint, regardless of transport (QUIC or TCP+TLS). They are not gated
pre-extraction code these lived in `alknet-call` behind the `quinn` under `quinn`/`tcp` — a TCP+TLS-only or QUIC-only deployment both need
feature; the extraction (ADR-089 §5) moves them to `alknet-tls` ungated, the CA path. `alknet-call` does not depend on them (the dial's TLS
and `alknet-call` sheds the deps entirely. deps are in `alknet-tls`/`alknet-client` now).
`alknet-core` loses `rustls-pemfile`, `rcgen`, and `rustls-acme` from `alknet-core` does not depend on `rustls-pemfile`, `rcgen`, or
its dependencies — the cert-loading, self-signed generation, and ACME `rustls-acme` — cert-loading, self-signed generation, and the ACME
machinery move to `alknet-tls`. Core's `acme` feature state machine are in `alknet-tls` (on `TlsServerConfig`, not on
(`acme = ["dep:rustls-acme"]` in `Cargo.toml` and the `AlknetEndpoint`). Core has no `acme` feature. Core does keep `quinn`
`#[cfg(feature = "acme")]` gates on `acme_state_handle` in `endpoint.rs`) and `iroh` (for `Connection::from_quinn` / `from_iroh` — the shared
becomes vestigial after the extraction and is removed — the ACME state constructors the endpoint and the dial both use),
machine now lives on `TlsServerConfig` in `alknet-tls`, not on `ed25519-dalek` (`Ed25519SecretKey` in `config.rs`), and `rustls` /
`AlknetEndpoint`. Core keeps `quinn` and `iroh` (the endpoint struct and `rustls-pki-types` (`fingerprint.rs` uses `rustls::pki_types` in
accept loops remain in core), `ed25519-dalek` (`Ed25519SecretKey` stays production and `rustls::sign` in the test helper
in `config.rs`), and `rustls` / `rustls-pki-types` (`fingerprint.rs` uses
`rustls::pki_types` in production and `rustls::sign` in the test helper
`build_ed25519_spki_der` — see OQ-59). `build_ed25519_spki_der` — see OQ-59).
> **Terminology — hub, worker, hub-worker.** A *hub* is a node that > **Terminology — hub, worker, hub-worker.** A *hub* is a node that
@@ -374,47 +376,11 @@ in `config.rs`), and `rustls` / `rustls-pki-types` (`fingerprint.rs` uses
> "assembly layer" (ADR-014) is the deployment binary that wires crates > "assembly layer" (ADR-014) is the deployment binary that wires crates
> — in practice, today, usually a hub or hub-worker. > — in practice, today, usually a hub or hub-worker.
### Implementation ordering ### What `AlknetEndpoint` (in `alknet-endpoint`) does
`alknet-tls` is greenfield — `crates/alknet-tls` does not exist yet. The `AlknetEndpoint` takes **no TLS config at all** — it is a
endpoint section below ("What `AlknetEndpoint` does after the refactor") multi-transport accept-loop runner. TCP+TLS is an owned transport (via
describes the **post-refactor target**, not the current source. The `with_tcp_tls`), not an external loop:
current `crates/alknet-core/src/endpoint.rs` is the **extraction
source** — `AlknetEndpoint::new(static_config, ...)` builds TLS
internally, the shape ADR-083 replaces. The endpoint is extracted into
a new crate `alknet-endpoint` (ADR-083 Amendment 2026-07-15) as part of
this work. The extraction and refactor are **sequenced**, not
simultaneous:
1. **`alknet-tls` first** — build the crate in isolation. `TlsServerConfig`
and `TlsClientConfig` are unit-testable against `TlsIdentity` without
touching the endpoint. This is the greenfield step.
2. **`alknet-endpoint` second** — build the new endpoint crate fresh
against the ADR-083 shape (`new(handlers, dynamic,
identity_provider, drain_timeout)` + `with_quinn` / `with_iroh` /
`with_tcp_tls`), importing `Connection`/`ProtocolHandler`/`AuthContext`
from `alknet-core` and taking pre-built transports (no TLS config —
the assembly layer builds those via `alknet-tls`). The old
`crates/alknet-core/src/endpoint.rs` is deleted.
3. **Assembly layer last** — the deployment binary (hub/worker) builds
the `TlsServerConfig`(s) and `TlsClientConfig`(s), the transports, and
hands them to `AlknetEndpoint` (in `alknet-endpoint`) via the builder
methods.
A compilable intermediate state exists after step 1: `alknet-tls` built
and tested standalone, with `endpoint.rs` still in its old shape. The
call sites for `TlsServerConfig` / `TlsClientConfig` do not exist until
step 2/3 — an implementer testing step 1 writes tests against the TLS
types directly, not against a wired-up endpoint.
### What `AlknetEndpoint` (in `alknet-endpoint`) does after the refactor
`AlknetEndpoint::new()` currently builds `TlsSetup` internally. After
the refactor (see [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md)),
the endpoint (extracted into `alknet-endpoint` per ADR-083 Amendment
2026-07-15) takes **no TLS config at all** — it is a multi-transport
accept-loop runner. TCP+TLS is an owned transport (via `with_tcp_tls`),
not an external loop:
```rust ```rust
impl AlknetEndpoint { impl AlknetEndpoint {
@@ -466,7 +432,9 @@ handle lives on the `TlsServerConfig`, not the endpoint.
This resolves the single-`Arc<TlsServerConfig>` problem: the endpoint This resolves the single-`Arc<TlsServerConfig>` problem: the endpoint
has no "the TLS config" to take because a hub has two. It also means has no "the TLS config" to take because a hub has two. It also means
shutdown is single-owner — the endpoint owns all its accept loops shutdown is single-owner — the endpoint owns all its accept loops
(quinn, iroh, TCP+TLS); one `shutdown()` stops them all. (quinn, iroh, TCP+TLS); one `shutdown()` stops them all. See
[`crates/endpoint/README.md`](../endpoint/README.md) and
[ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md).
### The TCP+TLS accept loop (out of scope for this crate) ### The TCP+TLS accept loop (out of scope for this crate)
@@ -485,86 +453,74 @@ sharing, not transport accept logic.
A hub dials out to workers it supervises and to other hubs A hub dials out to workers it supervises and to other hubs
(hub-as-client); `alknet-worker` dials a hub. Both need a (hub-as-client); `alknet-worker` dials a hub. Both need a
`rustls::ClientConfig` with ADR-034's verifier selection and ADR-084's `rustls::ClientConfig` with ADR-034's verifier selection and ADR-084's
crypto provider. `TlsClientConfig` centralizes this — it is a crypto provider. `TlsClientConfig` centralizes this, and is consumed by
present prerequisite for the first hub deployment, consumed by
`AlknetClient`'s QUIC and TCP+TLS dials (ADR-089). `AlknetClient`'s QUIC and TCP+TLS dials (ADR-089).
There are exactly two clients in the alknet client surface as far as There are exactly two clients in the alknet client surface as far as
`TlsClientConfig` and `AlknetClient` are concerned — **call** `TlsClientConfig` and `AlknetClient` are concerned — **call**
(`CallClient`) and **channels** (`ChannelClient`, which is a proxy over (`CallClient`) and **channels** (`ChannelClient`, which is a proxy over
many ALPNs via channel 0). Both must support all three transport many ALPNs via channel 0). Both share `TlsClientConfig` via the dial;
accessors below; the TLS config is shared across them, the dial is the TLS config is shared across them, the dial is per-transport
per-transport per-client. per-client.
```rust ```rust
pub struct TlsClientConfig { pub struct TlsClientConfig {
config: rustls::ClientConfig, rustls_config: rustls::ClientConfig,
} }
impl TlsClientConfig { impl TlsClientConfig {
/// Build a client TLS config. Takes two inputs, both derived from /// Build a client TLS config from `ConnectionCredentials` and the
/// `Capabilities` (ADR-014) / `ConnectionCredentials`-shaped values /// dial's ALPN. `ConnectionCredentials` (ADR-091, in `alknet-core`)
/// (ADR-091): /// carries the two dimensions the dial consumes:
/// ///
/// 1. `local_identity` — the local node's `TlsIdentity` (RFC 7250 /// 1. `tls_identity` — the local node's `TlsIdentity` (RFC 7250
/// raw key or X.509), presented as the client cert. `None` → /// raw key or X.509), presented as the client cert. `None` →
/// no client cert (the server gets nothing to fingerprint). /// no client cert (the server gets nothing to fingerprint).
/// `SelfSigned` → no client cert (dev-only). `Acme` → /// `SelfSigned` → no client cert (dev-only). `Acme` →
/// `TlsError::AcmeConfig` (server-only identity). /// `TlsError::AcmeConfig` (server-only identity).
/// ///
/// 2. `verifier_context` — the inputs to ADR-034's server-cert /// 2. `remote_identity` — the inputs to ADR-034's server-cert
/// verifier selection: /// verifier selection:
/// - known peer (PeerEntry present) → fingerprint pin /// - `Some(fingerprint)` (known peer, `PeerEntry` present) →
/// (FingerprintPinVerifier) /// fingerprint pin (`FingerprintPinVerifier`)
/// - unknown remote + X.509 → CA verification /// - `None` + X.509 transport → CA verification
/// (WebPkiServerVerifier) /// (`WebPkiServerVerifier`)
/// - unknown remote + raw key → fail closed at handshake (not /// - `None` + raw key → fail closed at handshake (not a `new`-
/// a `new`-time error; see ADR-088 §6) /// time error; see ADR-088 §6)
/// ///
/// Applies ADR-084 crypto provider (aws_lc_rs::default_provider()). /// Applies ADR-084 crypto provider (aws_lc_rs::default_provider()).
pub fn new( pub fn new(
local_identity: &Option<TlsIdentity>, credentials: &ConnectionCredentials,
verifier_context: &ClientVerifierContext, alpn: &[u8],
) -> Result<Self, TlsError>; ) -> Result<Self, TlsError>;
/// Produce a `quinn::ClientConfig` for a QUIC dial. Clones the /// Consume the config and produce a `quinn::ClientConfig` for a
/// inner rustls config, wraps it in `QuicClientConfig`. Returns /// QUIC dial. Returns `Result` because
/// `Result` because `QuicClientConfig::try_from(rustls::ClientConfig)` /// `QuicClientConfig::try_from(rustls::ClientConfig)` can fail with
/// can fail with `NoInitialCipherSuite` — the same failure the /// `NoInitialCipherSuite` — the same failure the server-side
/// server-side `for_quinn()` surfaces as `TlsError::QuinnWrap`. /// `for_quinn()` surfaces as `TlsError::QuinnWrap`. Feature-gated
/// Feature-gated on `quinn`. /// on `quinn`.
#[cfg(feature = "quinn")] #[cfg(feature = "quinn")]
pub fn for_quinn(&self) -> Result<quinn::ClientConfig, TlsError>; pub fn for_quinn(self) -> Result<quinn::ClientConfig, TlsError>;
/// Produce a `tokio_rustls::TlsConnector` for a TCP+TLS dial. /// Consume the config and return the inner `rustls::ClientConfig`,
/// Clones the inner rustls config. Infallible — /// for consumers that build their own transport-specific wrapper —
/// `TlsConnector::new(rustls::ClientConfig)` cannot fail. /// e.g. `dial_tcp_tls` wraps it in a
/// Feature-gated on `tcp` (pulls `tokio-rustls`). /// `tokio_rustls::TlsConnector::from(Arc::new(rustls_config))`. Not
#[cfg(feature = "tcp")] /// feature-gated; the raw rustls config is transport-agnostic.
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsConnector; pub fn into_rustls_config(self) -> rustls::ClientConfig;
/// Borrow the underlying rustls config, for consumers that need to
/// build their own transport-specific wrapper not covered by
/// `for_quinn` / `for_tcp_tls` (e.g. a future transport). Not
/// feature-gated — returns the raw rustls config, not a
/// transport-specific wrapper.
pub fn rustls_config(&self) -> &rustls::ClientConfig;
} }
``` ```
The `ClientVerifierContext` carries the inputs to ADR-034's verifier `TlsClientConfig::new` runs ADR-034's verifier selection directly off
selection (whether a `PeerEntry` exists for the remote, the expected `ConnectionCredentials.remote_identity` — there is no separate
fingerprint). The exact struct shape is an implementation detail; the `ClientVerifierContext` type; the credential bundle carries the
decisions are in ADR-034. `ClientVerifierContext` is derived from fingerprint (or its absence), which is all the verifier selection needs.
`ConnectionCredentials` (in `alknet-core`, per ADR-091) at the dial The call-protocol `auth_token` is not in `ConnectionCredentials` — it
site — `AlknetClient` extracts the TLS-relevant fields is a per-request field on `call.requested` payloads (a call-protocol /
(`local_identity` → client cert, `remote_identity` → fingerprint-pin hub concept), not a transport credential; it never reaches
input) and builds a `ClientVerifierContext` from the latter. The `TlsClientConfig`. The `TlsError` variant granularity (covering both
call-protocol `auth_token` is not in `ConnectionCredentials` — it is a server and client errors) is decided — see
per-request field on `call.requested` payloads (a call-protocol / hub
concept), not a transport credential; it never reaches `TlsClientConfig`
or `ClientVerifierContext`. The `TlsError` variant granularity (covering
both server and client errors) is decided — see
[ADR-088](../../decisions/088-tlserror-shape.md) and the [ADR-088](../../decisions/088-tlserror-shape.md) and the
[`TlsError`](#tlserror) section below. [`TlsError`](#tlserror) section below.
@@ -576,25 +532,26 @@ containerized deployment with no system CA bundle), the built-in
the `NoRootAnchors` failure mode unreachable in practice — a the `NoRootAnchors` failure mode unreachable in practice — a
containerized worker dialing a public X.509 hub succeeds without containerized worker dialing a public X.509 hub succeeds without
requiring the operator to mount a CA bundle. Native-certs *load* errors requiring the operator to mount a CA bundle. Native-certs *load* errors
are logged, not returned (preserved behavior); the fallback guarantees are logged, not returned; the fallback guarantees the store is
the store is non-empty regardless. See ADR-088 §5. non-empty regardless. See ADR-088 §5.
`TlsClientConfig` produces a `rustls::ClientConfig`; the caller (the `TlsClientConfig` produces a `rustls::ClientConfig`; the caller (the
transport-specific dial helper — `AlknetClient::dial_quic` / transport-specific dial helper — `AlknetClient::dial_quic` /
`dial_tcp_tls`, ADR-089) passes it to the transport's connector. The `dial_tcp_tls`, ADR-089) passes it to the transport's connector. The
config is transport-agnostic; the dial is not. This is the client-side config is transport-agnostic; the dial is not. This is the client-side
analogue of ADR-065's server-side separation: the take-over analogue of ADR-065's server-side separation: the take-over
(`spawn_dispatch` / `from_connection`, transport-agnostic) is built (`spawn_dispatch` / `from_connection`, transport-agnostic) is
now; the dial (transport-specific) is per-transport. The transport-agnostic; the dial (transport-specific) is per-transport.
transport-polymorphic dial is now extracted as `alknet-client` The transport-polymorphic dial is `alknet-client` (ADR-089, resolves
(ADR-089, resolves OQ-55) — `AlknetClient` builds the `TlsClientConfig` OQ-55) — `AlknetClient` builds the `TlsClientConfig` per-dial and
per-dial and calls the transport's connector. calls the transport's connector.
The client-side accessor API mirrors the server side: `for_quinn()` The client-side accessor API: `for_quinn()` (QUIC) and
/ `for_tcp_tls()` / `rustls_config()` — three transports, same `into_rustls_config()` (any other transport — `dial_tcp_tls` wraps the
pattern. Iroh is the exception (see below). `AlknetClient` (ADR-089) rustls config in a `TlsConnector`). Iroh is the exception (see below).
consumes `TlsClientConfig` via these accessors for the QUIC and TCP+TLS `AlknetClient` (ADR-089) consumes `TlsClientConfig` via these accessors
dials; the iroh dial is the key-not-config exception. for the QUIC and TCP+TLS dials; the iroh dial is the key-not-config
exception.
### Iroh — shares the key, not the config (client side too) ### Iroh — shares the key, not the config (client side too)
@@ -617,7 +574,16 @@ the `for_quinn()` accessors on both (`for_tcp_tls` is infallible —
`alknet-tls`. The shape, the rationale for single-enum-over-thin-wrapper, `alknet-tls`. The shape, the rationale for single-enum-over-thin-wrapper,
and the "what is NOT a variant" list are in and the "what is NOT a variant" list are in
[ADR-088](../../decisions/088-tlserror-shape.md); this section is [ADR-088](../../decisions/088-tlserror-shape.md); this section is
the sketch. the target shape per that ADR.
> **Implementation note.** The current `alknet-tls/src/lib.rs`
> `TlsError` is a simplified 3-variant enum (`Config(String)`,
> `Io(io::Error)`, `Cert(String)`) without `#[non_exhaustive]`. The
> full ADR-088 shape below (six typed variants, `#[non_exhaustive]`,
> `#[from` sources) is the target; the present code folds the
> finer-grained categories into `Config`/`Cert` strings. An implementer
> refining `TlsError` to match ADR-088 is a two-way-door change (the
> enum is crate-local, no external match arms).
```rust ```rust
/// Errors produced by `TlsServerConfig::new`, `TlsClientConfig::new`, /// Errors produced by `TlsServerConfig::new`, `TlsClientConfig::new`,
@@ -679,9 +645,9 @@ arrive asynchronously and are logged (ADR-082 §"Behavior-preservation
invariants"). invariants").
**Ownership.** `TlsError` lives in `alknet-tls`, owned by the crate **Ownership.** `TlsError` lives in `alknet-tls`, owned by the crate
that produces it. It is not re-exported from `alknet-core`; `EndpointError` that produces it. It is not re-exported from `alknet-core`; there is no
is removed entirely after ADR-083 (both variants were vestigial), so `EndpointError` (removed per ADR-083 — both variants were vestigial),
core has no endpoint error type and does not need to know about so core has no endpoint error type and does not need to know about
`TlsError`. The assembly layer (hub/worker) depends on `alknet-tls` `TlsError`. The assembly layer (hub/worker) depends on `alknet-tls`
directly and gets `TlsError` from that dependency. directly and gets `TlsError` from that dependency.
@@ -689,15 +655,15 @@ directly and gets `TlsError` from that dependency.
``` ```
alknet-tls alknet-tls
├── alknet-core (TlsIdentity, Ed25519SecretKey, fingerprint) └── alknet-core (TlsIdentity, Ed25519SecretKey, fingerprint)
alknet-core (loses TLS setup code + endpoint) alknet-core (lightweight — types + auth + config + fingerprint + credentials)
├── (rustls — only for fingerprint.rs types, if kept) └── (rustls / rustls-pki-types — only for fingerprint.rs types)
alknet-call (pure protocol crate — no TLS/transport deps per ADR-089 §5) alknet-call (pure protocol crate — no TLS/transport deps per ADR-089 §5)
└── alknet-core (ProtocolHandler, Connection, types; ConnectionCredentials/ └── alknet-core (ProtocolHandler, Connection, types; ConnectionCredentials/
RemoteIdentity moved to core per ADR-091; CallCredentials removed per ADR-091 Am. 2026-07-17 RemoteIdentity from core per ADR-091; CallCredentials removed per
alknet-call) ADR-091 Am. 2026-07-17)
alknet-hub (multi-transport endpoint) alknet-hub (multi-transport endpoint)
├── alknet-tls (TlsServerConfig — shared across quinn + TCP) ├── alknet-tls (TlsServerConfig — shared across quinn + TCP)
@@ -706,7 +672,7 @@ alknet-hub (multi-transport endpoint)
├── alknet-channels-call (ChannelClient) ├── alknet-channels-call (ChannelClient)
├── alknet-call (CallAdapter, Dispatcher) ├── alknet-call (CallAdapter, Dispatcher)
├── alknet-http (HttpAdapter) ├── alknet-http (HttpAdapter)
├── alknet-core (Connection, ProtocolHandler, AuthContext, IdentityProvider) └── alknet-core (Connection, ProtocolHandler, AuthContext, IdentityProvider)
``` ```
`alknet-tls` depends on `alknet-core` only. No handler crate depends on `alknet-tls` depends on `alknet-core` only. No handler crate depends on
@@ -725,7 +691,7 @@ All design decisions are documented as ADRs in
| ADR | Decision | Summary | | ADR | Decision | Summary |
|-----|----------|---------| |-----|----------|---------|
| [082](../../decisions/082-alknet-tls-extraction.md) | alknet-tls crate extraction | Extract TLS setup from alknet-core/endpoint.rs; `TlsServerConfig` shareable across quinn + TCP+TLS + iroh; one ACME state machine | | [082](../../decisions/082-alknet-tls-extraction.md) | alknet-tls crate extraction | Extract TLS setup from alknet-core/endpoint.rs; `TlsServerConfig` shareable across quinn + TCP+TLS + iroh; one ACME state machine |
| [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 is in 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 | | [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 |
@@ -773,38 +739,35 @@ See [open-questions.md](../../open-questions.md) for full details.
- **OQ-64** (resolved): `alknet-tls` provides `TlsClientConfig` - **OQ-64** (resolved): `alknet-tls` provides `TlsClientConfig`
(ADR-087). Not blocked on the dial-seam extraction — the TLS (ADR-087). Not blocked on the dial-seam extraction — the TLS
config is a prerequisite for the dial, not a consequence of it. config is a prerequisite for the dial, not a consequence of it.
Centralizes ADR-034 verifier selection + ADR-084 provider; the Centralizes ADR-034 verifier selection + ADR-084 provider. The dial
hub-as-client requirement makes it a prerequisite for the first hub seam is `alknet-client` (ADR-089, OQ-55 resolved); `TlsClientConfig`
deployment. The dial seam is now extracted as `alknet-client` is consumed by `AlknetClient`'s QUIC and TCP+TLS dials.
(ADR-089, OQ-55 resolved); `TlsClientConfig` is consumed by
`AlknetClient`'s QUIC and TCP+TLS dials.
- **OQ-55** (resolved by ADR-089): `AlknetClient::dial()` — the - **OQ-55** (resolved by ADR-089): `AlknetClient::dial()` — the
transport-polymorphic dial seam. Extracted as a new crate transport-polymorphic dial seam. `alknet-client` has three dial
`alknet-client` with three dial methods (`dial_quic` / methods (`dial_quic` / `dial_tcp_tls` / `dial_iroh`).
`dial_tcp_tls` / `dial_iroh`). `TlsClientConfig` (OQ-64, resolved) `TlsClientConfig` (OQ-64, resolved) is the prerequisite the dial
is the prerequisite the dial consumes. See consumes. See [`crates/client/README.md`](../client/README.md) and
[`crates/client/README.md`](../client/README.md) and
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md). [ADR-089](../../decisions/089-alknetclient-native-dial-seam.md).
### Next session — client shape ### Client shape (in `alknet-client`)
The client is now specced. [`crates/client/README.md`](../client/README.md) [`crates/client/README.md`](../client/README.md) defines `AlknetClient`
defines `AlknetClient` — the native client dial seam (ADR-089, resolves — the native client dial seam (ADR-089, resolves OQ-55). There are
OQ-55). There are exactly two clients in the alknet client surface as exactly two clients in the alknet client surface as far as
far as `TlsClientConfig` and `AlknetClient` are concerned: **call** `TlsClientConfig` and `AlknetClient` are concerned: **call**
(`CallClient`) and **channels** (`ChannelClient`, a proxy over many (`CallClient`) and **channels** (`ChannelClient`, a proxy over many
ALPNs via channel 0). Both consume `TlsClientConfig` via the same three ALPNs via channel 0). Both consume `TlsClientConfig` through the dial
accessors (`for_quinn`, `for_tcp_tls`, `rustls_config`); iroh is the (`for_quinn` for QUIC, `into_rustls_config` wrapped in a `TlsConnector`
exception (shares the key, not the config). `AlknetClient` is the dial for TCP+TLS); iroh is the exception (shares the key, not the config).
that feeds them — it produces a `Connection` and the protocol `AlknetClient` is the dial that feeds them — it produces a `Connection`
take-overs (`spawn_dispatch`, `from_connection`) consume it. The and the protocol take-overs (`spawn_dispatch`, `from_connection`)
per-protocol QUIC convenience constructors (`CallClient::connect` / consume it. The per-protocol QUIC convenience constructors
`ChannelClient::connect_quic`) are **removed** per ADR-089 §5 — the (`CallClient::connect` / `ChannelClient::connect_quic`) are removed
dial is centralized in `AlknetClient`, and the protocol crates shed per ADR-089 §5 — the dial is centralized in `AlknetClient`, and the
their TLS/transport deps. The `alknet/register` ALPN (native protocol crates have no TLS/transport deps. The `alknet/register` ALPN
registration entry point, parallel to HTTP registration in OQ-58) is (native registration entry point, parallel to HTTP registration in
named by ADR-089; its wire protocol is deferred (OQ-66). OQ-58) is named by ADR-089; its wire protocol is deferred (OQ-66).
## References ## References
@@ -829,15 +792,14 @@ named by ADR-089; its wire protocol is deferred (OQ-66).
(the endpoint spec; TLS config is built by `alknet-tls`, not the (the endpoint spec; TLS config is built by `alknet-tls`, not the
endpoint — per ADR-083) endpoint — per ADR-083)
- `docs/architecture/crates/core/config.md` — `TlsIdentity`, `StaticConfig` - `docs/architecture/crates/core/config.md` — `TlsIdentity`, `StaticConfig`
- `crates/alknet-core/src/endpoint.rs` — the server-side code being - `crates/alknet-tls/src/server.rs` — `TlsServerConfig`,
extracted (`build_rustls_server_config`, `TlsSetup`, `RawKeyCertResolver`, `RawKeyCertResolver`, `AcceptAnyCertVerifier`,
`Ed25519SigningKey`, `AcceptAnyCertVerifier`, `generate_self_signed_cert`, `generate_self_signed_cert`, `build_rustls_server_config`
`load_cert_chain`, `load_private_key`) - `crates/alknet-tls/src/client.rs` — `TlsClientConfig`,
- `crates/alknet-call/src/client/call_client.rs` — the client-side code
being extracted (`build_quinn_client_config`, `build_client_auth`,
`select_server_verifier`, `load_platform_root_cert_store`,
`FingerprintPinVerifier`, `RawKeyClientCertResolver`, `FingerprintPinVerifier`, `RawKeyClientCertResolver`,
`NoClientCertResolver`, `Ed25519SigningKey` (duplicate), `NoClientCertResolver`, `select_server_verifier`, `build_client_auth`,
`load_cert_chain`/`load_private_key` (duplicates)) `load_platform_root_cert_store`
- `crates/alknet-tls/src/pem.rs` — `load_cert_chain`, `load_private_key`
- `crates/alknet-tls/src/signing.rs` — `Ed25519SigningKey`
- `crates/alknet-core/src/fingerprint.rs` — fingerprint extraction - `crates/alknet-core/src/fingerprint.rs` — fingerprint extraction
(shared by server endpoint and client verifier) (shared by server endpoint and client verifier)
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-07-16 last_updated: 2026-07-17
--- ---
# Open Questions # Open Questions
+6 -5
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-07-15 last_updated: 2026-07-17
--- ---
# Alknet Overview # Alknet Overview
@@ -97,11 +97,12 @@ alknet-vault (standalone — foundational to ACL: key derivation, identity)
│ alknet-core ProtocolHandler, Connection, BidiStreamSource, AuthContext, │ alknet-core ProtocolHandler, Connection, BidiStreamSource, AuthContext,
│ │ IdentityProvider, StaticConfig, DynamicConfig, fingerprint, │ │ IdentityProvider, StaticConfig, DynamicConfig, fingerprint,
│ │ ConnectionCredentials, RemoteIdentity │ │ ConnectionCredentials, RemoteIdentity
│ │ (endpoint extracted to alknet-endpoint; core is now lightweight │ │ (lightweight types+auth+config+credentials; endpoint in
│ │ types+auth+config — no quinn/iroh/rcgen deps; ConnectionCredentials │ │ alknet-endpoint; no rcgen/rustls-pemfile/rustls-acme deps;
│ │ + RemoteIdentity moved here from alknet-call per ADR-091; │ │ quinn/iroh stay for Connection::from_quinn/from_iroh;
│ │ ConnectionCredentials + RemoteIdentity per ADR-091;
│ │ CallCredentials removed per ADR-091 Am. 2026-07-17) │ │ CallCredentials removed per ADR-091 Am. 2026-07-17)
│ ├── alknet-tls TlsServerConfig + TlsClientConfig + FingerprintPinVerifier — shared TLS config across quinn + TCP+TLS + iroh (ADR-082/087; FingerprintPinVerifier moved from alknet-call per ADR-089 §5) │ ├── alknet-tls TlsServerConfig + TlsClientConfig + FingerprintPinVerifier — shared TLS config across quinn + TCP+TLS + iroh (ADR-082/087; FingerprintPinVerifier per ADR-089 §5)
│ ├── alknet-call CallAdapter on alknet/call, CallClient (spawn_dispatch only — connect removed per ADR-089 §5), OperationRegistry, adapters (no TLS/transport deps) │ ├── alknet-call CallAdapter on alknet/call, CallClient (spawn_dispatch only — connect removed per ADR-089 §5), OperationRegistry, adapters (no TLS/transport deps)
│ ├── 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