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:
2026-06-28 10:47:49 +00:00
parent 3f011cbb82
commit 6cc8715ccf
8 changed files with 602 additions and 65 deletions

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-06-27
last_updated: 2026-06-28
---
# Open Questions
@@ -666,58 +666,62 @@ is a feature extension, not an unmade architecture decision.
## Theme: TLS Identity
### OQ-37: X.509 Outgoing-Only Case (Three Auth Types)
### OQ-37: X.509 Outgoing-Only Case (Three Peer Roles)
- **Origin**: ADR-030 §"Bearer tokens" (the three credential types), the
discussion that X.509 is fundamentally different from Ed25519
- **Status**: open (lingering — the X.509 server-identity case needs design)
- **Status**: **resolved** (2026-06-28 by ADR-034)
- **Door type**: One-way (how X.509 server identity integrates with the
peer model)
- **Priority**: medium
- **Resolution**: The three credential types are: Ed25519 raw key (the
common case, normalized to `ed25519:<hex>` across quinn/iroh), X.509
(domain-facing endpoints, ACME, `SHA256:<hex>`), and bearer token
(`PeerEntry.auth_token_hash` or `ApiKeyEntry`).
- **Priority**: medium → resolved
- **Resolution**: **The pre-ADR-034 framing conflated three distinct
remote roles under "X.509 endpoint."** [ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md)
names them and resolves the peer-model question:
Ed25519 and bearer token are resolved (ADR-030 + OQ-29). The X.509 case
that remains open is **outgoing-only**: a client connects to a public
X.509 endpoint (e.g., `api.alk.dev`). The client must verify the server
cert against a CA (rustls's `WebPkiServerVerifier`) — the
`AcceptAnyServerCertVerifier` is a security hole for X.509. The server
may or may not require a client cert (most public X.509 endpoints
won't — browsers can't easily do TLS client-auth).
1. **Public X.509 endpoint** — a remote HTTPS / `alknet/call`-over-TLS
server reachable by domain, authenticated by CA verification
(`WebPkiServerVerifier`). The local node is a *client*; it
authenticates by bearer token. **Not a `PeerEntry` on the client
side** — it is not in the call-protocol peer graph (ADR-029), gets
no `PeerId`, and is not addressable via `PeerRef::Specific`. Ops
discovered via `from_call`/`from_openapi`/`from_mcp` land in the
connection's Layer 2 overlay and are invoked through the
connection handle.
2. **Transport relay** — iroh's DERP-equivalent (`iroh-relay`).
Infrastructure, not an alknet peer; no `PeerEntry` / `PeerId`.
Inherited with the `iroh` feature; its identity is iroh's concern.
3. **Hub / hosting node** — an alknet application peer (head/worker
hub, git-hosting hub) that *also* exposes a public domain + X.509
for browsers. A single `PeerEntry` with **mixed fingerprints**
(`ed25519:...` + `SHA256:...`), already supported by ADR-030.
Browsers connecting to it are *not* alknet peers — served by
`alknet-http`, bearer-token auth, no `PeerId`.
What's resolved:
- The `PeerEntry.fingerprints` field accepts X.509 fingerprints
(`SHA256:<hex of DER>`) alongside Ed25519 fingerprints.
- The client-side verifier is key-type-aware (OQ-29): raw keys use
fingerprint-matching, X.509 uses CA verification.
**The "make `PeerEntry` symmetric" instinct is rejected.** `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 asymmetry reflects a real trust-model difference: known peers have
stable logical identities (pin the fingerprint); public APIs don't
(trust the CA, hold the connection handle directly).
What's open:
- How does the outgoing X.509 case interact with `PeerEntry`? If a
client connects to `api.alk.dev` (X.509, no client-auth), the client
doesn't present a cert, so the server has no fingerprint to resolve.
The client authenticates via `auth_token` (the bearer-token path).
The server's `PeerEntry` for this client uses `auth_token_hash`, not
`fingerprints`. This works — but the server's `PeerEntry` might not
have a fingerprint at all for an HTTP-only client.
- Conversely, if the server requires X.509 client-auth (mutual TLS),
the client presents its X.509 cert, the server extracts the
`SHA256:<hex>` fingerprint, and `PeerEntry.fingerprints` matches it.
This works too.
- The open question is whether there are cases where X.509 server
identity needs to be part of the `PeerEntry` model (the server's
identity, not the client's) — e.g., for the client to know "I'm
connected to `api.alk.dev`, which is peer-id `api-server`." Currently
`PeerEntry` is about the *remote* peer's credentials, as seen by the
*local* node. For an outgoing connection, the local node is the
client, and `PeerEntry` describes the server. This may need a
design pass to make sure the model is symmetric.
**Client-side verifier selection rule (extends OQ-29):** known peer
(`PeerEntry` present) → fingerprint pin (Ed25519 `ed25519:<hex>` or
X.509 `SHA256:<hex>`); unknown X.509 remote (`PeerEntry` absent) → CA
verification. An unknown Ed25519 raw-key remote cannot be verified at
all (no CA fallback) and fails closed — same model as iroh.
**Downstream, not blocking, recorded so they don't get lost:**
WebTransport relay-as-proxy (browser → proxy → P2P hub) is deferred
with the rest of h3/WebTransport (alknet-http DH-2); ADR-030 §6's
fingerprint normalization already keeps the proxied path clean. On-
chain / smart-contract peer discovery (relays syncing git repos via
iroh gossip) is a *source* of `PeerEntry` records, fits the OQ-36
repo/adapter pattern (`alknet-peer-store-onchain` implementing
`IdentityProvider`), and does not change the auth model.
Not blocking the ADR-029 migration — the Ed25519 path is the primary
use case and it's resolved. The X.509 outgoing-only case is a real
question but it's downstream (the HTTP crate phase, when
`from_openapi`/`from_mcp` handlers connect to X.509 endpoints).
- **Cross-references**: ADR-027, ADR-029, ADR-030, OQ-29,
[client-and-adapters.md](crates/call/client-and-adapters.md),
use case and was already resolved; this ADR closes the X.509
outgoing-only remainder.
- **Cross-references**: ADR-027, ADR-029, ADR-030, ADR-033, ADR-034,
OQ-29, OQ-36, [client-and-adapters.md](crates/call/client-and-adapters.md),
[endpoint.md](crates/core/endpoint.md), [auth.md](crates/core/auth.md)