docs(arch): ADR-034 — outgoing-only X.509 and three peer roles, resolve OQ-37
Untangles the conflation of three distinct remote roles under 'X.509 endpoint': (1) public X.509 endpoint — a remote HTTPS/call-over-TLS server the local node is a client of (no PeerEntry, no PeerId, not in the peer graph; CA verification + bearer token); (2) transport relay — iroh's DERP-equivalent, infrastructure, not an alknet peer; (3) hub / hosting node — an alknet peer that also exposes a public domain + X.509 for browsers (mixed-fingerprint PeerEntry, already supported by ADR-030). The load-bearing one-way door is the client-side verifier selection rule: known peer (PeerEntry present) → fingerprint pin; unknown X.509 remote → CA verification (WebPkiServerVerifier); unknown Ed25519 remote → fails closed. This closes the AcceptAnyServerCertVerifier security hole OQ-29 flagged, with the peer-model criterion (PeerEntry presence) made explicit. The 'make PeerEntry symmetric' instinct is rejected — pure-client connections to public APIs have no stable logical identity to pin. Documents that CallCredentials.remote_identity: None is load-bearing (None = public X.509 endpoint → CA path, not a missing field; Some = known peer → fingerprint pin), closing a subtle gap where an implementer could have defaulted to a placeholder or treated None as skip-verify. Records WebTransport relay-as-proxy (deferred with h3/WebTransport, new OQ-HTTP-07) and on-chain/smart-contract peer discovery (fits the OQ-36 repo/adapter pattern, no auth-model change) so they aren't lost. Amends auth.md and client-and-adapters.md with the three-role naming, the verifier selection rule, and the Option semantics; updates OQ-37 to resolved in open-questions.md, README.md, and both crate READMEs.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-27
|
||||
last_updated: 2026-06-28
|
||||
---
|
||||
|
||||
# Authentication
|
||||
@@ -132,6 +132,64 @@ Bearer tokens have two paths:
|
||||
|
||||
The distinction is whether the token needs a stable logical id across rotation (`PeerEntry`) or not (`ApiKeyEntry`). See ADR-030 §"Bearer tokens."
|
||||
|
||||
## Three Remote Roles (ADR-034)
|
||||
|
||||
The three credential types above describe how a *single* `PeerEntry` can
|
||||
be authenticated. Separately, there are **three distinct remote roles**
|
||||
that the architecture must not conflate (see [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md)):
|
||||
|
||||
| Role | Identity | alknet peer? | `PeerEntry` on local side? |
|
||||
|------|----------|--------------|----------------------------|
|
||||
| **Public X.509 endpoint** | Domain + CA-issued X.509 | No (local node is a client) | No |
|
||||
| **Transport relay** (iroh's DERP-equivalent) | iroh `NodeId` (Ed25519) | No (infrastructure) | No |
|
||||
| **Hub / hosting node** | Ed25519 raw key **and/or** X.509 | Yes (full peer) | Yes |
|
||||
|
||||
(Transport path and examples per role are in ADR-034; this table is
|
||||
auth-focused — identity, peer-graph membership, and `PeerEntry`
|
||||
presence on the local side.)
|
||||
|
||||
`PeerEntry` (and the `PeerId` it resolves to) is the model for peers in
|
||||
the call-protocol peer graph (ADR-029) — peers that get a stable logical
|
||||
identity, are addressable via `PeerRef::Specific`, and whose ops land in
|
||||
the peer-keyed overlay. A pure-client connection to a public X.509
|
||||
endpoint (e.g., `api.alk.dev`, a third-party API) is **not** in that
|
||||
graph on the client side: the local node holds no `PeerEntry` for it,
|
||||
the connection gets no `PeerId`, and ops discovered via
|
||||
`from_call`/`from_openapi`/`from_mcp` are invoked through the
|
||||
connection handle directly (Layer 2 overlay, ADR-024), not through
|
||||
peer-keyed routing. The asymmetry is deliberate — a public domain's
|
||||
operator can change hands, so there is no stable logical identity to
|
||||
attach; the local node trusts the CA today and holds the connection
|
||||
handle.
|
||||
|
||||
The **hub** case is an ordinary `PeerEntry` that happens to expose both
|
||||
an Ed25519 fingerprint (P2P path) and an X.509 fingerprint
|
||||
(`SHA256:<hex>`, WebTransport/HTTPS path) — already supported by
|
||||
`PeerEntry.fingerprints: Vec<String>` (ADR-030). Browsers connecting to
|
||||
a hub over WebTransport/HTTPS are *not* alknet peers on the hub's side
|
||||
either — they're served by `alknet-http`, authenticate by bearer token,
|
||||
and get no `PeerId`.
|
||||
|
||||
### Client-side verifier selection (outgoing connections)
|
||||
|
||||
The `CallClient` / `from_openapi` / `from_mcp` client-side
|
||||
`ServerCertVerifier` is selected by **whether the local node has a
|
||||
`PeerEntry` for the remote**, not by key type alone:
|
||||
|
||||
| Local has `PeerEntry` for remote? | Remote cert type | Client verifier |
|
||||
|----------------------------------|------------------|-----------------|
|
||||
| No (public X.509 endpoint) | X.509 | `WebPkiServerVerifier` (CA verification) |
|
||||
| No | Ed25519 raw key | fails closed (no CA to fall back to — raw-key remotes are always known peers) |
|
||||
| Yes (hub, Ed25519 path) | Ed25519 raw key | fingerprint match (`ed25519:<hex>`) |
|
||||
| Yes (hub, X.509 path) | X.509 | fingerprint match (`SHA256:<hex>`) |
|
||||
|
||||
This is the key-type-aware verifier from OQ-29, with the peer-model
|
||||
criterion (ADR-034) made explicit. `AcceptAnyServerCertVerifier` is a
|
||||
security hole for X.509 and is only safe for raw-key fingerprint
|
||||
extraction on the *server* side; the *client* side must use CA
|
||||
verification for unknown X.509 remotes and fingerprint pinning for
|
||||
known peers.
|
||||
|
||||
## AuthToken
|
||||
|
||||
Opaque authentication token carried in protocol frames.
|
||||
@@ -230,7 +288,7 @@ The verifier accepts any presented cert without CA verification because
|
||||
alknet's identity model is fingerprint-based, not PKI-based — the
|
||||
`AuthPolicy::peers` set is the trust anchor, not a root CA store. The
|
||||
cert bytes are extracted at the TLS layer and hashed to a fingerprint
|
||||
string; the fingerprint is then matched against the configured `PeerEntry.fingerprint`
|
||||
string; the fingerprint is then matched against the configured `PeerEntry.fingerprints`
|
||||
fields by `IdentityProvider::resolve_from_fingerprint()`.
|
||||
|
||||
## Resolution Flow
|
||||
@@ -328,12 +386,13 @@ The endpoint's `AlknetEndpoint` also holds `Arc<dyn IdentityProvider>` for endpo
|
||||
| PeerEntry and Identity.id decoupling | [ADR-030](../../decisions/030-peerentry-and-identity-id-decoupling.md) | `authorized_fingerprints` → `peers: Vec<PeerEntry>`; `Identity.id` = `peer_id` (stable), not fingerprint; key rotation changes fingerprint, not identity |
|
||||
| CredentialStore repo trait | [ADR-031](../../decisions/031-credentialstore-repo-trait.md) | Second repo trait in core (alongside `IdentityProvider`); `InMemoryCredentialStore` default adapter |
|
||||
| Storage boundary and repo/adapter pattern | [ADR-033](../../decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Core defines traits + in-memory defaults; persistence adapters are separate crates |
|
||||
| Three remote roles and outgoing-only X.509 | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Public X.509 endpoint / transport relay / hub; `PeerEntry` asymmetry (pure-client X.509 is not a peer); client-side verifier by `PeerEntry` presence |
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-29** (resolved): `CallClient` TLS client-auth — wire quinn client-auth (present Ed25519 key as raw public key client cert); key-type-aware server cert verification (raw key = fingerprint match, X.509 = CA verification); fingerprint normalization (`ed25519:` across quinn/iroh). See OQ-29 in open-questions.md.
|
||||
- **OQ-35** (dissolved): the "API key asymmetry" framing was wrong; `PeerEntry` supports multiple credential paths (fingerprints + auth_token_hash), `ApiKeyEntry` is for tokens that ARE the identity. See OQ-35 in open-questions.md.
|
||||
- **OQ-37** (open): X.509 outgoing-only case — the three auth types and how X.509 server identity fits the peer model. Not blocking the ADR-029 migration. See OQ-37 in open-questions.md.
|
||||
- **OQ-37** (resolved): X.509 outgoing-only case — three remote roles named (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry is correct (pure-client X.509 connections are not in the peer graph on the client side); client-side verifier selection by `PeerEntry` presence (CA verification for unknown X.509, fingerprint pin for known peers). See ADR-034 and OQ-37 in open-questions.md.
|
||||
|
||||
## Security Constraints
|
||||
|
||||
|
||||
Reference in New Issue
Block a user