Full-surface integration suite (tests/full_surface.rs, mcp feature): - one HttpAdapter over real TCP (ProtocolHandler::handle path) serving gateway endpoints, /openapi.json, /mcp, and the WS channels session - gateway: search/schema/call/subscribe/batch/publish presence, envelope shapes, error fidelity end-to-end - from_openapi import -> Internal-by-default invisible from the wire -> External facade composes it via env.invoke -> upstream HTTP API called end-to-end (ADR-015 composition model exercised) - to_openapi 6-path doc validated against openapiv3 over the wire - to_mcp: MCP client connects to /mcp on the served adapter, lists the 4 gateway tools, search returns ACL-filtered ops (Sub excluded) Production fix: the WS upgrade route was reserved but never wired into HttpAdapter's router (the ws-upgrade-session tests built their own router). Now wired with ws_bearer_auth (401 without a resolvable token) around ws_upgrade_handler. Docs sync: all 28 'Port notes' sections/blockquotes stripped from ported ADRs/specs; OQ-01/OQ-02 statuses corrected to resolved in overview.md, websocket.md, and the README table (open-questions.md was already current). Publish prep: cargo publish --dry-run --allow-dirty succeeds; cargo doc --no-deps warning-free (ADR link targets fixed); feature combinations (default / test-support / mcp / wss / all) compile warning-free under clippy -D warnings. Verified: cargo test (182 lib default), --all-features (227 lib + 29 integration), clippy -D warnings x3 feature sets, fmt, doc, publish --dry-run.
493 lines
27 KiB
Markdown
493 lines
27 KiB
Markdown
# ADR-034: Outgoing-Only X.509 and the Three Peer Roles
|
||
|
||
*Ported from alknet ADR-034 (Outgoing-Only X.509 and the Three Peer Roles); re-targeted to alkhttp.*
|
||
|
||
## Status
|
||
|
||
Accepted (resolves OQ-37)
|
||
|
||
**Relevance for this crate:** §4 — browsers are not peers — governs
|
||
alkhttp's browser-facing surface (the WebSocket path, alkhttp
|
||
ADR-044/ADR-048). The X.509/TLS machinery this ADR discusses (server
|
||
cert verifiers, `TlsIdentity`, WebPKI verification, fingerprint
|
||
pinning) is **an alknet concern**: alkhttp is transport-coupling-free
|
||
and owns no TLS configuration or verifier
|
||
> selection. The X.509-relevant sections (§2, §3, §5) are retained for
|
||
> provenance and marked accordingly; what alkhttp consumes is the peer-
|
||
> model closure of §4 and the three-role vocabulary of §1.
|
||
|
||
## Context
|
||
|
||
OQ-37 framed the open question as: "the three credential types (Ed25519,
|
||
X.509, bearer token) and how X.509 server identity fits the peer model."
|
||
During resolution, it became clear that **three distinct remote roles**
|
||
had been conflated under the single label "X.509 endpoint," and that the
|
||
conflation was the actual source of the confusion — not the TLS
|
||
mechanics, which alknet ADR-027 and alknet ADR-030 had already settled.
|
||
|
||
The three roles are real and structurally different:
|
||
|
||
1. **Public X.509 endpoint** — a remote HTTPS or `alk/call`-over-TLS
|
||
server reachable by domain name, authenticated by a CA-issued X.509
|
||
cert. The local node is a *client* of it. Examples: a
|
||
third-party API (`vast.ai`, `api.openai.com`), a public hub
|
||
that the local node dials over the open internet, an `alk/call`
|
||
peer that has chosen to expose a domain + X.509 instead of (or in
|
||
addition to) an Ed25519 raw key. The client authenticates to the
|
||
server by **bearer token** (browsers and most HTTP clients cannot do
|
||
TLS client-auth); the server authenticates to the client by **CA
|
||
verification** (WebPKI), not by fingerprint pinning.
|
||
|
||
2. **Transport relay** — iroh's DERP-equivalent (`iroh-relay`). A
|
||
connectivity-assistance node that forwards encrypted datagrams
|
||
between peers who cannot directly connect (NAT traversal). It is
|
||
*infrastructure*, not an application peer: it does not
|
||
register operations, does not participate in the call protocol's
|
||
peer graph, and has no `PeerEntry` / `PeerId` in the auth
|
||
model. Nodes inherit it for free when the `iroh` feature is on; the
|
||
relay's own identity (an Ed25519 `NodeId`) is iroh's concern, not
|
||
the protocol stack's. *(Transport-relay mechanics are an alknet
|
||
concern; alkhttp has no iroh dependency and no relay role — the
|
||
relay is named here only to keep the three-role vocabulary intact.)*
|
||
|
||
3. **Hub / hosting node** — an application peer that acts as a
|
||
hub in a hub-and-spoke (head/worker) topology. It is an ordinary
|
||
`PeerEntry` that *happens* to also expose a public domain + X.509
|
||
(so browsers / external HTTPS clients can reach it) *and* an Ed25519
|
||
identity (so other nodes can reach it P2P via iroh or direct
|
||
quinn). The git-hosting-relay-with-gossip-sync use case is this role:
|
||
the hub is a full peer that additionally serves browsers. *(The hub's
|
||
browser-facing surface is what alkhttp serves — the gateway endpoints
|
||
and the WebSocket session.)*
|
||
|
||
The pre-ADR-034 framing asked whether `PeerEntry` should be made
|
||
**symmetric** — i.e., whether the local node should hold a `PeerEntry`
|
||
for *every* remote it might dial, including pure-public-API servers it
|
||
has no P2P relationship with. This ADR answers **no**: the asymmetry is
|
||
correct and reflects a real difference in trust model. `PeerEntry` (and
|
||
the `PeerId` it produces) is the model for **peers in the call-protocol
|
||
peer graph** (alkcall ADR-024) — 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 HTTPS API is
|
||
not that.
|
||
|
||
This distinction matters because forcing a stable logical `peer_id`
|
||
onto "the operator of `api.example.com`" is wrong: a public domain's
|
||
operator can change hands, the cert can be reissued, and the local node
|
||
has no stable logical identity to attach — only "domain X verified by
|
||
CA Y today." That is a different trust model from "this Ed25519 key is
|
||
`worker-a`, and key rotation updates the fingerprint but not the
|
||
identity" (alkcall ADR-025).
|
||
|
||
## Decision
|
||
|
||
### 1. Name the three roles; stop using "relay" ambiguously
|
||
|
||
The architecture documents use three distinct terms:
|
||
|
||
| Role | Identity | Transport | Peer? | Example |
|
||
|------|----------|-----------|--------------|---------|
|
||
| **Public X.509 endpoint** | Domain + CA-issued X.509 | HTTPS / `alk/call`-over-TLS | No (client only, unless also role 3) | `api.alk.dev`, `vast.ai` |
|
||
| **Transport relay** | iroh `NodeId` (Ed25519) | iroh's DERP-like protocol | No (infrastructure) | `relay.iroh.network` |
|
||
| **Hub / hosting node** | Ed25519 raw key **and/or** X.509 | iroh / direct quinn / HTTPS | Yes (full `PeerEntry`) | git-hosting hub, head node |
|
||
|
||
Existing specs that say "relay" when they mean "domain-hosted service"
|
||
or "hub" are amended by reference to this table. alknet ADR-027's "domain-
|
||
hosted services" and alkcall ADR-025's "X.509 cert" credential path refer
|
||
to the **public X.509 endpoint** role and the **hub** role; iroh's
|
||
transport relay is a separate, inherited component referenced only in
|
||
the iroh transport path *(alknet concern)*.
|
||
|
||
### 2. Outgoing-only X.509 is not a `PeerEntry` on the client side *(TLS mechanics: alknet/alkcall concern; the peer-model rule: alkhttp-relevant)*
|
||
|
||
When a `CallClient` (or the alkhttp `from_openapi` / `from_mcp` adapters)
|
||
dials a remote that is a **public X.509 endpoint** and the local node has
|
||
no P2P relationship with it (no `PeerEntry` for the remote):
|
||
|
||
- The server is authenticated by **CA verification**
|
||
(`rustls::WebPkiServerVerifier` with the platform root store or a
|
||
configured CA bundle) *(the verifier itself is alknet/alkcall dial-layer
|
||
machinery — alkhttp consumes its outcome, it does not build
|
||
verifiers)*. There is no fingerprint to pin — pinning a
|
||
`SHA256:<hex of DER>` fingerprint against an external CA-issued cert
|
||
is brittle (cert renewal changes the fingerprint) and is not the
|
||
WebPKI trust model. The trigger for CA verification is **the absence
|
||
of a `PeerEntry` for the remote combined with an X.509 transport**;
|
||
the verifier selection rule is stated in full in §3 below. The
|
||
`ConnectionCredentials.remote_identity: Option<RemoteIdentity>` field
|
||
(alkcall ADR-012, extending alkcall ADR-022 §7) carries an expected
|
||
fingerprint/cert when the caller has one to pin (`Some`); for a
|
||
pure-client X.509 dial with no `PeerEntry`, `remote_identity` is
|
||
`None` and the CA path applies. The `Option` is load-bearing — `None`
|
||
is the public-X.509-endpoint state, not a missing field: an
|
||
implementer must not default it to a placeholder, and must not treat
|
||
`None` as "skip verification" (`None` + X.509 = CA verification;
|
||
`None` + Ed25519 raw key = fail closed). (alkcall ADR-022 §7 specified
|
||
`remote_identity` as "expected fingerprint or cert"; this ADR extends
|
||
its semantics so that `remote_identity: None` + no `PeerEntry` +
|
||
X.509 transport selects CA verification, and `remote_identity: None`
|
||
+ Ed25519 raw-key transport fails closed.)
|
||
- The client authenticates to the server by **bearer token**
|
||
(`ConnectionCredentials.auth_token`), carried in the call-protocol
|
||
`auth_token` payload field (or the HTTP `Authorization` header for
|
||
alkhttp's `from_openapi` / `from_mcp`). What the *server* does with
|
||
that token depends on which kind of public X.509 endpoint it is:
|
||
- **Third-party API** (`api.openai.com`, `vast.ai` — not an alknet
|
||
node): the server applies its own auth scheme (its own API-key
|
||
validation, its own ACL). The protocol's `PeerEntry` / `ApiKeyEntry`
|
||
types do not apply on the far side; the client just carries the
|
||
token in the shape the remote expects (an HTTP header, a
|
||
call-protocol `auth_token` payload) and treats the remote's
|
||
response as authoritative.
|
||
- **Hub reached over its public X.509 path** (a role-3 hub
|
||
dialed over the domain instead of P2P): the hub resolves the
|
||
client's token via its own `PeerEntry.auth_token_hash` or
|
||
`ApiKeyEntry` — the *server's* bookkeeping, not the client's. The
|
||
client still holds no `PeerEntry` for the hub on its own side
|
||
unless it also has a P2P trust relationship with that hub (in which
|
||
case the §3 mixed-fingerprint path applies, not this one).
|
||
- The client may still present its TLS client cert (Ed25519 raw public
|
||
key, per alknet OQ-29) when one is configured; bearer token is the
|
||
*authorization* credential, and TLS client-auth (when presented) is
|
||
*additional* identity material the server may use. For a third-party
|
||
API the cert is ignored; for a hub it may be extracted as a
|
||
fingerprint. Presenting or omitting the client cert is the caller's
|
||
choice via `ConnectionCredentials`; this ADR does not require
|
||
disabling client-auth on this path. *(TLS client-cert presentation is
|
||
dial-layer machinery — an alknet/alkcall concern, not alkhttp's.)*
|
||
- The connection does **not** get a `PeerId` on the client side. It is
|
||
not added to `PeerCompositeEnv` (alkcall ADR-024). There is no
|
||
`PeerRef::Specific` routing to it. The connection is a live
|
||
`CallConnection` (or, for alkhttp's reqwest-backed client host, an
|
||
HTTP client session) the caller holds directly; ops discovered via
|
||
the `from_call`-pattern import (alkcall ADR-028) or via alkhttp's
|
||
`from_openapi` / `from_mcp` land in that connection's Layer 2 overlay
|
||
(alkcall ADR-019) and are invoked through the connection handle, not
|
||
through the peer-keyed routing layer.
|
||
|
||
This is the **asymmetry** OQ-37 worried about, stated as a deliberate
|
||
design property: `PeerEntry` is for peers in the call-protocol peer
|
||
graph. Pure-client connections to public X.509 endpoints are not in
|
||
that graph on the client side. The server may have a `PeerEntry` for
|
||
*us* (resolving our bearer token, in the hub sub-case); we
|
||
don't need one for *it*.
|
||
|
||
### 3. The hub case is already covered by ADR-030's mixed-fingerprint `PeerEntry` *(alknet/alkcall concern)*
|
||
|
||
A **hub / hosting node** that is reachable both P2P (Ed25519 raw key
|
||
via iroh or direct quinn) and via a public domain (X.509 for browsers)
|
||
is a single `PeerEntry` with mixed fingerprints:
|
||
|
||
```rust
|
||
PeerEntry {
|
||
peer_id: "hub-a".into(),
|
||
fingerprints: vec![
|
||
"ed25519:<hex of hub's Ed25519 pub key>", // P2P path
|
||
"SHA256:<hex of hub's X.509 cert DER>", // HTTPS / browser-facing path
|
||
],
|
||
auth_token_hash: Some("<sha256 of peer's bearer token>"),
|
||
scopes: vec![...],
|
||
resources: {...},
|
||
...
|
||
}
|
||
```
|
||
|
||
*(The `PeerEntry` struct with mixed fingerprints is an alkcall type —
|
||
alkcall ADR-025. Fingerprint normalization across quinn/iroh —
|
||
alkcall ADR-025 §6 — is an alknet/alkcall dial-layer concern.)*
|
||
|
||
When a node dials this hub P2P, the Ed25519 fingerprint
|
||
matches; when it dials over the public X.509 path (e.g., because P2P
|
||
connectivity failed), the X.509 fingerprint matches — both resolve to
|
||
the same `peer_id` (`"hub-a"`). The X.509 path here uses
|
||
**fingerprint pinning** (the `SHA256:<hex>` is in `PeerEntry`), *not*
|
||
CA verification, because the local node has a prior P2P trust
|
||
relationship with this specific hub and has recorded its cert's
|
||
fingerprint. This is the one case where X.509 fingerprint pinning is
|
||
correct: the peer is a known peer, not an arbitrary public API.
|
||
|
||
The choice between **CA verification** (role 1) and **fingerprint
|
||
pinning** (role 3, X.509 path) is driven by whether the local node has
|
||
a `PeerEntry` for the remote — this is the authoritative verifier
|
||
selection rule, referenced from §2:
|
||
|
||
| 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; fingerprint IS identity) |
|
||
| 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 alknet OQ-29, with the
|
||
*peer-model* criterion made explicit: the verifier choice is determined
|
||
by whether the remote is a known peer (`PeerEntry` present → pin) or an
|
||
external server (`PeerEntry` absent → CA, or fail closed for raw keys).
|
||
*(The verifier construction itself is dial-layer machinery — an
|
||
alknet/alkcall concern; alkhttp's `from_openapi`/`from_mcp`/client host
|
||
consume connections established under this rule but do not select
|
||
verifiers.)*
|
||
|
||
### 4. Browsers connecting to a hub are not peers
|
||
|
||
A browser reaching a hub over WebTransport (or HTTPS — and, per alkhttp
|
||
ADR-044, over WebSocket) is served by the hub's HTTP handler — in the
|
||
extracted crate tree, **alkhttp**. The browser authenticates by **bearer
|
||
token** (HTTP `Authorization`), resolved by the hub's
|
||
`IdentityProvider::resolve_from_token` against the hub's
|
||
`PeerEntry.auth_token_hash` or `ApiKeyEntry`. The browser is **not a
|
||
peer on the hub's side either** — it does not get a `PeerId`, does
|
||
not enter `PeerCompositeEnv`, and its "ops" are HTTP routes / browser
|
||
streams served by alkhttp, not entries in the call-protocol
|
||
peer-keyed overlay. The hub's `PeerEntry` for the browser (if any) is
|
||
about authorizing the bearer token, not about peer-graph membership.
|
||
|
||
This keeps the peer graph populated only by full nodes (role 3
|
||
hubs and role-3-style spoke nodes), never by browsers or pure HTTP
|
||
clients.
|
||
|
||
> **Amendment (rationale added by alknet ADR-044 §5, ported to alkhttp
|
||
> ADR-044):** The closure above is correct but states the conclusion
|
||
> without the supporting argument. The distinction that makes it correct
|
||
> is: **"peer" means an addressable node in the call-protocol peer
|
||
> graph** — a stable `PeerId`, reachable via `PeerRef::Specific`, whose
|
||
> ops land in `PeerCompositeEnv`, whose identity is stable across
|
||
> reconnects. It does *not* mean "any endpoint that exchanges calls
|
||
> during a live session." A browser is the second thing but not the
|
||
> first, on three concrete grounds: (1) no stable cryptographic identity
|
||
> of its own (it presents a bearer token the hub issued; nothing to
|
||
> pin), (2) ephemeral (close the tab → connection dies → the
|
||
> connection-local overlay dies with it; a `PeerEntry` keyed to a
|
||
> browser would be dead within seconds), (3) not addressable from other
|
||
> nodes (another node has no way to reach "the browser currently
|
||
> connected to hub-A"; the hub holds it as a live `CallConnection`
|
||
> handle, not a peer-graph entry). The connection-local Layer 2 overlay
|
||
> (alkcall ADR-019; the inbound mirror of §2 above) is what gives the
|
||
> browser bidirectional-call capability *without* peer-graph membership.
|
||
> This rationale is transport-agnostic — it applies to WebSocket (the
|
||
> browser path in alkhttp, alkhttp ADR-044) and to WebTransport
|
||
> (an alknet transport; not in alkhttp scope — see alkhttp ADR-069)
|
||
> equally. See alkhttp ADR-044 §5 for the full statement.
|
||
|
||
### 5. WebTransport relay-as-proxy is a transport-only feature, scoped separately *(alknet concern)*
|
||
|
||
**WebTransport is not in alkhttp scope at all** — per alkhttp ADR-069
|
||
it was removed from this crate entirely (it is an alknet concern). This
|
||
section is retained for the *auth-model* point (the proxy is
|
||
transport-only and does not change identity resolution), which remains
|
||
true regardless of where the proxy lives.
|
||
|
||
A **WebTransport proxy** that terminates the browser's WebTransport
|
||
connection and proxies encrypted traffic to a hub's P2P endpoint
|
||
(avoiding the need for the hub itself to expose a public X.509 endpoint)
|
||
is a real feature, especially for the browser-to-P2P-peer case. It is
|
||
**not** load-bearing on the auth model resolved here:
|
||
|
||
- The proxy does not change how identities resolve. The browser still
|
||
authenticates by bearer token; the hub still resolves it via
|
||
`PeerEntry.auth_token_hash`. The proxy is transport-only.
|
||
- The fingerprint normalization committed in alkcall ADR-025 §6
|
||
(`ed25519:<hex>` for raw keys across quinn and iroh) was already
|
||
designed to keep the proxied path clean: a proxied connection's
|
||
Ed25519 identity is the same `ed25519:<hex>` whether the client
|
||
connected directly or through the proxy.
|
||
|
||
> **Amendment (wording only — the decision stands):** An earlier draft
|
||
> of this section framed the relay-as-proxy as belonging to an
|
||
> "h3/WebTransport deferral bucket" and "lands when `h3` /
|
||
> WebTransport lands." That framing was a residual of the "two-way door
|
||
> as deferral" anti-pattern (alkcall ADR-032 §"What this framework is
|
||
> NOT") that alknet ADR-038 was later written to reject. alknet ADR-038
|
||
> has since been **superseded by alknet ADR-044**, which re-defers
|
||
> `h3`/WebTransport as a genuine scope decision (the browser
|
||
> bidirectional path uses WebSocket). In the alkhttp crate, the scope
|
||
> question is closed differently: **WebTransport is removed from alkhttp
|
||
> entirely** (alkhttp ADR-069) — there is no "revives later" posture in
|
||
> this crate. The *auth-model* decision in this §5 (the proxy is
|
||
> transport-only; it does not change identity resolution) is unchanged
|
||
> by any of these ADRs. The *scope* question (which crate hosts the
|
||
> proxy, if it is ever built) remains an alknet concern — the alknet
|
||
> OQ-38 tracking is not carried into alkhttp.
|
||
|
||
### 6. On-chain / smart-contract peer discovery fits the OQ-36 adapter pattern *(alknet/alkcall concern)*
|
||
|
||
The downstream use case — storing relay/repo info and org/user ACL on a
|
||
smart-contract platform, with relays (hubs) syncing git repos via
|
||
iroh's gossip protocol — is a **discovery and ACL-source** concern, not
|
||
an auth-model concern. It does not change any of decisions 1–4:
|
||
|
||
- The hubs are role-3 `PeerEntry` peers (mixed fingerprints, full peer-
|
||
graph membership, gossip-synced).
|
||
- The smart contract is a **source of `PeerEntry` records**. It maps
|
||
cleanly onto the repo/adapter pattern (alknet ADR-033): a future
|
||
on-chain peer-store adapter implementing `IdentityProvider`
|
||
against a smart contract is additive, exactly like a SQLite peer
|
||
store (alknet ADR-035). The auth model (`PeerEntry`, `PeerId`,
|
||
`Identity`) is unchanged; only the *source* of the records changes.
|
||
- The repo/ACL data on-chain is consumed by the hub's authorization
|
||
layer (`AccessControl::check` against scopes/resources populated from
|
||
the on-chain `PeerEntry`), not by the TLS / fingerprint path.
|
||
|
||
Designing that adapter now would be premature — it is downstream of
|
||
both the repo/adapter exploration (alknet OQ-36) and the git crate
|
||
(alknet OQ-10). It is noted here only to confirm it does not reopen
|
||
OQ-37. *(This is recorded for provenance; the peer-store adapters are
|
||
alknet/alkcall assembly-layer components, not alkhttp surface.)*
|
||
|
||
## What this does NOT change
|
||
|
||
- **`PeerEntry` struct shape** (alkcall ADR-025) — unchanged. Mixed
|
||
fingerprints (Ed25519 + X.509) were already supported.
|
||
- **`Identity` / `IdentityProvider` trait** — unchanged (vendored in the
|
||
alkcall crate). The verifier choice is a `CallClient` /
|
||
`from_openapi` / `from_mcp` dial-layer concern, not an
|
||
`IdentityProvider` concern.
|
||
- **`ConnectionCredentials` struct** — unchanged (alkcall ADR-012).
|
||
`remote_identity` already carries the expected key type; this ADR
|
||
specifies how the verifier is chosen from it (CA for unknown X.509
|
||
remotes, fingerprint match for known peers).
|
||
- **`PeerCompositeEnv` / `PeerRef`** (alkcall ADR-024) — unchanged.
|
||
Pure-client X.509 connections simply do not enter the peer-keyed
|
||
overlay.
|
||
- **`TlsIdentity`** (alknet ADR-027) — unchanged. The server-side X.509 / ACME
|
||
/ RawKey modes are unaffected; this ADR is about the *client-side*
|
||
verifier choice for outgoing connections. *(And `TlsIdentity` itself
|
||
is an alknet concern — not present in alkhttp.)*
|
||
- **The no-env-vars invariant** — unaffected. The bearer token for the
|
||
outgoing X.509 case still comes from `Capabilities` (alkcall ADR-010
|
||
secret-material flow), not env vars.
|
||
|
||
## Consequences
|
||
|
||
**Positive:**
|
||
- OQ-37 is resolved. The "make `PeerEntry` symmetric" instinct is
|
||
rejected with a clear criterion: `PeerEntry` is for peers in the
|
||
call-protocol peer graph; pure-client connections to public X.509
|
||
endpoints are not in that graph on the client side.
|
||
- The three remote roles are named, so future specs and conversations
|
||
can distinguish "public X.509 endpoint," "transport relay," and
|
||
"hub / hosting node" instead of overloading "relay."
|
||
- The client-side verifier choice has a single rule: known peer
|
||
(`PeerEntry` present) → fingerprint pin; unknown X.509 remote
|
||
(`PeerEntry` absent) → CA verification. This closes the
|
||
`AcceptAnyServerCertVerifier` security hole for X.509 that alknet
|
||
OQ-29 flagged, with the peer-model criterion made explicit.
|
||
- The hub case (mixed Ed25519 + X.509 fingerprints, browser access via
|
||
HTTPS and the browser transports) is confirmed to need no new types —
|
||
alkcall ADR-025's `fingerprints: Vec<String>` already covers it.
|
||
- The relay-as-proxy and on-chain-discovery use cases are
|
||
recorded with clear homes (the relay-as-proxy is a transport-only
|
||
feature whose scope is an alknet concern — not tracked in alkhttp;
|
||
the on-chain discovery follows the alknet OQ-36 adapter pattern) so
|
||
they don't get lost and don't reopen the auth model.
|
||
|
||
**Negative:**
|
||
- The dial-layer client paths (alkcall's `CallClient`; alkhttp's
|
||
`from_openapi` / `from_mcp` outbound adapters via the reqwest-backed
|
||
client host) must respect the "is this remote a known `PeerEntry`?"
|
||
distinction when a TLS client config is built for an outgoing
|
||
connection. In alkhttp the credential injection point is the adapter
|
||
layer (alkhttp ADR-014 secret-material flow): `ConnectionCredentials`
|
||
— including `remote_identity` — are supplied by the consumer's
|
||
assembly layer, and the verifier itself is built in the dial layer
|
||
(alknet/alkcall), not in this crate. This is a small implementation
|
||
cost and is local to connection establishment; it is not a structural
|
||
change.
|
||
- Operators must understand the distinction between "I have a
|
||
`PeerEntry` for this remote (pin its fingerprint)" and "I'm calling a
|
||
public API (trust the CA)." In practice this is intuitive (it's the
|
||
difference between `~/.ssh/known_hosts` and a browser's CA trust
|
||
store), but the docs must state it clearly, which this ADR and the
|
||
spec amendments do.
|
||
- Pure-client X.509 connections have no `PeerId` on the client side, so
|
||
any future feature that wants to route to "the connection I opened to
|
||
`api.alk.dev`" must hold the `CallConnection` handle directly rather
|
||
than using `PeerRef::Specific`. This is the correct constraint —
|
||
`PeerRef::Specific` is for known peers, not for arbitrary dials — but
|
||
it is a constraint downstream code must respect.
|
||
|
||
## Assumptions
|
||
|
||
1. **A remote reachable by Ed25519 raw key is always a known peer.**
|
||
Raw-key remotes have no CA; the fingerprint IS the trust anchor. An
|
||
unknown Ed25519 remote cannot be verified at all (there is no CA to
|
||
fall back to), so the connection fails closed. This means the
|
||
"public X.509 endpoint" role is the *only* role where the local node
|
||
dials a remote it has no `PeerEntry` for. This is correct and
|
||
intended — it is the same model iroh uses. *(The iroh transport is an
|
||
alknet concern; the fail-closed rule is stated here because it is a
|
||
property of the credential model, not of any transport.)*
|
||
|
||
2. **Browsers never enter the peer-keyed overlay.** A browser is
|
||
served by alkhttp (gateway routes and, per alkhttp ADR-044/ADR-048,
|
||
the WebSocket session) and authenticates by bearer token. The hub may
|
||
have a `PeerEntry` for the browser's token (to authorize it), but the
|
||
browser is not a `PeerId`-bearing peer. This is the explicit closure
|
||
of the "browser as peer" path — browsers are clients, not peers.
|
||
**The rationale** (addressability vs. bidirectionality — a browser
|
||
has no stable identity of its own, is ephemeral, and is not
|
||
addressable from other nodes) is stated in alkhttp ADR-044 §5, which
|
||
amends §4 above by reference. The closure applies transport-
|
||
agnostically.
|
||
|
||
3. **X.509 fingerprint pinning is only for known hubs.** Pinning an
|
||
X.509 fingerprint for an arbitrary public API is brittle (cert
|
||
renewal) and is not done. The `PeerEntry.fingerprints` X.509 entry
|
||
is for the hub case where the local node has a P2P trust
|
||
relationship and wants to also recognize the hub's domain-facing
|
||
cert.
|
||
|
||
4. **The on-chain / smart-contract discovery use case does not change
|
||
the auth model.** It is a source of `PeerEntry` records, implemented
|
||
as an additive `IdentityProvider` adapter (alknet ADR-033 / alknet
|
||
OQ-36). The hub-and-gossip topology it implies is built from role-3
|
||
hubs, which this ADR confirms are ordinary `PeerEntry` peers.
|
||
|
||
## References
|
||
|
||
- OQ-37 (resolved by this ADR) — the three auth types and how X.509
|
||
server identity fits the peer model
|
||
- alknet [ADR-027](027-tls-identity-redesign-acme-rawkey-decoupling.md) —
|
||
`TlsIdentity` (RawKey / X509 / Acme), the browser limitation (no RFC
|
||
7250), WebTransport requires X.509 *(ported to alkhttp — same file
|
||
name in this directory; the TLS machinery itself is an alknet
|
||
concern)*
|
||
- alkcall ADR-024 — the peer-keyed overlay model that `PeerEntry` /
|
||
`PeerId` feed into; pure-client connections are not in this graph
|
||
- alkcall ADR-025 — `PeerEntry` with mixed fingerprints; fingerprint
|
||
normalization (`ed25519:` across quinn/iroh); the `SHA256:<hex>` X.509
|
||
fingerprint format
|
||
- alknet ADR-033 / alknet ADR-035 — the repo/adapter pattern that an
|
||
on-chain `IdentityProvider` adapter follows and the concrete SQLite
|
||
adapter shape (the on-chain adapter would follow the same trait +
|
||
separate-crate pattern) *(alknet decisions, not ported to alkhttp)*
|
||
- alkcall ADR-022 §7 — `CallCredentials` (now `ConnectionCredentials`,
|
||
alkcall ADR-012) with `remote_identity`; alkcall ADR-022 specified
|
||
"expected fingerprint or cert", this ADR §2 extends its semantics so
|
||
that `remote_identity: None` + no `PeerEntry` + X.509 transport
|
||
selects CA verification
|
||
- alkcall ADR-019 — the Layer 2 per-connection overlay where ops
|
||
discovered via the `from_call` pattern (alkcall ADR-028) or via
|
||
alkhttp's `from_openapi` / `from_mcp` on a pure-client X.509
|
||
connection land
|
||
- alknet OQ-29 (resolved) — key-type-aware server cert verification;
|
||
this ADR adds the peer-model criterion (known peer vs. public X.509
|
||
endpoint) that selects the verifier
|
||
- alknet OQ-10 (deferred) — git adapter scope; the on-chain /
|
||
gossip-synced git-hosting hub use case in §6 is downstream of the git
|
||
crate *(alknet OQ)*
|
||
- alknet OQ-36 (resolved by alknet ADR-035) — concrete persistence
|
||
adapter shapes; the on-chain `IdentityProvider` adapter in §6 follows
|
||
the same repo/adapter pattern *(alknet OQ)*
|
||
- alknet ADR-038 — **superseded by alknet ADR-044**; not ported to
|
||
alkhttp. See alknet ADR-034 §5's amendment history and alkhttp
|
||
ADR-069 (WebTransport removed from alkhttp scope).
|
||
- alkhttp ADR-044 — the WebSocket browser path; §5 states the
|
||
"browser is not a peer" rationale that amends this ADR's §4
|
||
- alkhttp ADR-048 — the WebSocket session shape; the browser bidirectional
|
||
path this ADR's §4 governs
|
||
- iroh transport relay (`iroh-relay`) — referenced to distinguish it
|
||
from the hub role *(iroh docs; an alknet transport concern)*
|
||
- alkcall crate docs — `CallClient`/`ConnectionCredentials` dial path,
|
||
verifier selection by `PeerEntry` presence; see the alkcall crate's
|
||
own documentation for the client-and-adapters spec
|