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
|
||||
---
|
||||
|
||||
# Alknet Architecture
|
||||
@@ -78,6 +78,7 @@ The alknet-call crate is **implemented and reviewed** — both the server-side c
|
||||
| [031](decisions/031-credentialstore-repo-trait.md) | CredentialStore Repo Trait | Accepted |
|
||||
| [032](decisions/032-forwarded-for-identity.md) | Forwarded-For Identity (Metadata, Not Authority) | Accepted |
|
||||
| [033](decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Storage Boundary and Repo/Adapter Pattern | Accepted |
|
||||
| [034](decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Outgoing-Only X.509 and the Three Peer Roles | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -124,7 +125,7 @@ See [open-questions.md](open-questions.md) for the full tracker.
|
||||
**Open (feature extensions, not blocking):**
|
||||
- **OQ-32**: Multi-hop federation — the one-hop model is the architectural commitment; multi-hop is a feature extension that doesn't break downstream
|
||||
- **OQ-36**: Concrete persistence adapter shapes — the repo/adapter pattern is committed (ADR-033); in-memory adapters ship with core; persistence adapters (SQLite, etc.) are deferred for exploration
|
||||
- **OQ-37**: X.509 outgoing-only case — the three auth types (Ed25519, X.509, bearer token) and how X.509 server identity fits the peer model. Not blocking the ADR-029 migration; downstream (HTTP crate phase)
|
||||
- **OQ-37**: ~~X.509 outgoing-only case~~ — **resolved by ADR-034** (three remote roles named: public X.509 endpoint, transport relay, hub; `PeerEntry` asymmetry is correct; client-side verifier selection by `PeerEntry` presence)
|
||||
|
||||
**Deferred (not active):**
|
||||
- **OQ-09**: WASM target boundaries — design constraint, not deliverable
|
||||
|
||||
@@ -64,7 +64,7 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
|
||||
| OQ-33 | PeerId — crypto identity vs stable logical id | **resolved** (ADR-030) | `PeerId = Identity.id = PeerEntry.peer_id` (stable across key rotation) |
|
||||
| OQ-34 | Persistent peer registry | **resolved** (ADR-030+033) | Core trait + in-memory default; persistence adapters are separate crates |
|
||||
| OQ-35 | ~~API key asymmetry~~ | **dissolved** | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity |
|
||||
| OQ-37 | X.509 outgoing-only case | open | Three auth types; how X.509 server identity fits the peer model. Not blocking. |
|
||||
| OQ-37 | X.509 outgoing-only case | **resolved** (ADR-034) | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; verifier by `PeerEntry` presence |
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-27
|
||||
last_updated: 2026-06-28
|
||||
---
|
||||
|
||||
# alknet-call — Client and Adapters
|
||||
@@ -205,16 +205,28 @@ credential dimensions (ADR-017 §7):
|
||||
pub struct CallCredentials {
|
||||
pub tls_identity: Option<TlsIdentity>, // RFC 7250 raw key or X.509
|
||||
pub auth_token: Option<AuthToken>, // call-protocol-level token
|
||||
pub remote_identity: Option<RemoteIdentity>, // expected fingerprint/cert
|
||||
pub remote_identity: Option<RemoteIdentity>, // expected fingerprint/cert (None = CA path, see below)
|
||||
}
|
||||
|
||||
/// Expected identity of the remote node (ADR-017 §7). v1 carries a
|
||||
/// fingerprint string the assembly layer derives from `Capabilities`.
|
||||
/// Expected identity of the remote node (ADR-017 §7, extended by
|
||||
/// 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 }
|
||||
|
||||
/// Errors produced by `CallClient::connect`.
|
||||
#[non_exhaustive]
|
||||
pub enum ClientError { Transport { .. }, TlsSetup { .. }, ConnectionClosed }
|
||||
```
|
||||
|
||||
- **TLS identity** — the local node's Ed25519 raw key (RFC 7250) or X.509 cert,
|
||||
@@ -222,7 +234,10 @@ pub enum ClientError { Transport { .. }, TlsSetup { .. }, ConnectionClosed }
|
||||
- **Auth token** — an opaque call-protocol-level token, decrypted from the
|
||||
vault or derived from a shared secret.
|
||||
- **Remote identity verification** — the expected fingerprint/cert of the
|
||||
remote node, stored as a capability.
|
||||
remote node, stored as a capability. `Some` → fingerprint pin (known
|
||||
peer with a `PeerEntry`); `None` → CA verification for X.509 remotes,
|
||||
fail-closed for Ed25519 raw-key remotes (ADR-034 §2/§3). The `None`
|
||||
case is the public-X.509-endpoint path, not a missing field.
|
||||
|
||||
These are populated by the assembly layer at `CallClient` construction time
|
||||
from vault-derived `Capabilities`. The credential path is the no-env-vars
|
||||
@@ -242,6 +257,22 @@ vars, ADR-014) is unaffected — the `auth_token` dimension flows through the
|
||||
call-protocol `auth_token` payload field, not TLS, so the no-env-vars
|
||||
invariant holds independently of this gap.
|
||||
|
||||
**Outgoing X.509 and the peer model** (ADR-034): the client-side
|
||||
`ServerCertVerifier` is selected by whether the local node has a
|
||||
`PeerEntry` for the remote, not by key type alone. A pure-client
|
||||
connection to a **public X.509 endpoint** (no `PeerEntry` on the local
|
||||
side — e.g., dialing `api.alk.dev` or a third-party API) uses
|
||||
`WebPkiServerVerifier` (CA verification), gets **no `PeerId`** on the
|
||||
client side, and is **not added to `PeerCompositeEnv`** — it is not in
|
||||
the call-protocol peer graph (ADR-029). Ops discovered via `from_call`
|
||||
on such a connection land in the connection's Layer 2 overlay
|
||||
(ADR-024) and are invoked through the `CallConnection` handle directly,
|
||||
not via `PeerRef::Specific`. A connection to a **hub** (a `PeerEntry`
|
||||
with mixed Ed25519 + X.509 fingerprints) uses fingerprint pinning on
|
||||
both cert paths and does enter the peer graph. See
|
||||
[ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md)
|
||||
for the verifier selection rule and the three-role naming.
|
||||
|
||||
### from_call
|
||||
|
||||
`from_call` discovers the remote peer's `External` operations and registers
|
||||
@@ -591,6 +622,22 @@ Based on the gap analysis and the downstream unblock chain:
|
||||
- **MCP stdio transport is not built.** Streamable HTTP is the only supported
|
||||
MCP transport in alknet. stdio = spawn arbitrary executable = built-in RCE.
|
||||
Recorded as an explicit security position, not a feature gap.
|
||||
- **Pure-client X.509 connections are not in the peer graph on the client
|
||||
side.** A `CallClient` connection to a public X.509 endpoint with no
|
||||
local `PeerEntry` for the remote gets no `PeerId`, is not added to
|
||||
`PeerCompositeEnv`, and is not addressable via `PeerRef::Specific`.
|
||||
Ops discovered on it live in the connection's Layer 2 overlay and are
|
||||
invoked through the `CallConnection` handle. The client-side
|
||||
`ServerCertVerifier` uses CA verification (`WebPkiServerVerifier`) for
|
||||
such remotes; known peers (hub with `PeerEntry`) use fingerprint
|
||||
pinning. See [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md).
|
||||
- **`CallCredentials.remote_identity: None` is load-bearing.** `None`
|
||||
means "no `PeerEntry` for this remote → use CA verification (X.509)
|
||||
or fail closed (Ed25519 raw key)" per the ADR-034 §3 verifier rule.
|
||||
The implementation must not default `remote_identity` to a placeholder
|
||||
to satisfy the field, and must not treat `None` as "skip verification"
|
||||
— `None` + X.509 is CA verification, `None` + raw key is a hard
|
||||
failure. `Some(fingerprint)` is the known-peer pin path.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
@@ -609,6 +656,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` |
|
||||
| Operation error schemas | [ADR-023](../../decisions/023-operation-error-schemas.md) | `error_schemas` mirrored by `from_call` from remote op's spec |
|
||||
| TLS identity redesign | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | RFC 7250 raw key / X.509 cert dimensions of `CallCredentials` |
|
||||
| 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 |
|
||||
| Vault key model | [ADR-026](../../decisions/026-vault-key-model-hd-derivation.md) | Vault-derived TLS identity material |
|
||||
| Vault local-only dispatch | [ADR-025](../../decisions/025-vault-local-only-dispatch.md) | Vault access at assembly layer only; the credential injection path's first hop |
|
||||
@@ -662,9 +710,17 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
shapes — the repo/adapter pattern is committed (ADR-033); the in-memory
|
||||
adapters ship with core; the persistence adapter shapes (SQLite, etc.)
|
||||
are deferred for exploration. See OQ-36 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 by ADR-034): X.509 outgoing-only case — three
|
||||
remote roles named (public X.509 endpoint, transport relay, hub).
|
||||
`PeerEntry` asymmetry is correct: a pure-client connection to a public
|
||||
X.509 endpoint is **not** in the call-protocol peer graph on the
|
||||
client side — no `PeerEntry`, no `PeerId`, no `PeerRef::Specific`
|
||||
routing. Ops discovered via `from_call`/`from_openapi`/`from_mcp`
|
||||
land in the connection's Layer 2 overlay and are invoked through the
|
||||
connection handle. The client-side `ServerCertVerifier` is selected
|
||||
by `PeerEntry` presence: known peer → fingerprint pin; unknown X.509
|
||||
remote → CA verification (`WebPkiServerVerifier`). See ADR-034 and
|
||||
OQ-37 in open-questions.md.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ Core library for ALPN-based protocol dispatch. Every handler crate depends on al
|
||||
| OQ-34 | Persistent peer registry (storage boundary) | resolved by ADR-030+031+033 | Core defines repo traits + in-memory defaults; persistence adapters are separate crates |
|
||||
| OQ-35 | ~~API key asymmetry~~ | dissolved | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity |
|
||||
| OQ-36 | Concrete persistence adapter shapes | open (deferred for exploration) | The repo/adapter pattern is committed (ADR-033); in-memory adapters ship with core; persistence adapters deferred |
|
||||
| OQ-37 | X.509 outgoing-only case | open | Three auth types; how X.509 server identity fits the peer model. Not blocking. |
|
||||
| OQ-37 | X.509 outgoing-only case | resolved by ADR-034 | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; client-side verifier by `PeerEntry` presence (CA vs fingerprint pin) |
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
# ADR-034: Outgoing-Only X.509 and the Three Peer Roles
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (resolves OQ-37)
|
||||
|
||||
## 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 ADR-027 and ADR-030 had already settled.
|
||||
|
||||
The three roles are real and structurally different:
|
||||
|
||||
1. **Public X.509 endpoint** — a remote HTTPS or `alknet/call`-over-TLS
|
||||
server reachable by domain name, authenticated by a CA-issued X.509
|
||||
cert. The local alknet node is a *client* of it. Examples: a
|
||||
third-party API (`vast.ai`, `api.openai.com`), a public alknet hub
|
||||
that the local node dials over the open internet, an `alknet/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 alknet application peer: it does not
|
||||
register operations, does not participate in the call protocol's
|
||||
peer graph, and has no `PeerEntry` / `PeerId` in alknet's auth
|
||||
model. Alknet inherits it for free when the `iroh` feature is on; the
|
||||
relay's own identity (an Ed25519 `NodeId`) is iroh's concern, not
|
||||
alknet's.
|
||||
|
||||
3. **Hub / hosting node** — an alknet 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 alknet 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 alknet peer that additionally serves browsers.
|
||||
|
||||
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** (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 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" (ADR-030).
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Name the three roles; stop using "relay" ambiguously
|
||||
|
||||
The architecture documents use three distinct terms:
|
||||
|
||||
| Role | Identity | Transport | alknet peer? | Example |
|
||||
|------|----------|-----------|--------------|---------|
|
||||
| **Public X.509 endpoint** | Domain + CA-issued X.509 | HTTPS / `alknet/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. ADR-027's "domain-
|
||||
hosted services" and ADR-030'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.
|
||||
|
||||
### 2. Outgoing-only X.509 is not a `PeerEntry` on the client side
|
||||
|
||||
When a `CallClient` (or `from_openapi` / `from_mcp`) 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). 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
|
||||
`CallCredentials.remote_identity: Option<RemoteIdentity>` field
|
||||
(ADR-017 §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).
|
||||
(ADR-017 §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**
|
||||
(`CallCredentials.auth_token`), carried in the call-protocol
|
||||
`auth_token` payload field (or the HTTP `Authorization` header for
|
||||
`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). Alknet's `PeerEntry` / `ApiKeyEntry` types
|
||||
do not apply on the far side; the alknet 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.
|
||||
- **Alknet 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 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 an alknet hub it may be extracted as a
|
||||
fingerprint. Presenting or omitting the client cert is the caller's
|
||||
choice via `CallCredentials`; this ADR does not require disabling
|
||||
client-auth on this path.
|
||||
- The connection does **not** get a `PeerId` on the client side. It is
|
||||
not added to `PeerCompositeEnv` (ADR-029). There is no
|
||||
`PeerRef::Specific` routing to it. The connection is a live
|
||||
`CallConnection` (or HTTP client session) the caller holds directly;
|
||||
ops discovered via `from_call` / `from_openapi` / `from_mcp` land in
|
||||
that connection's Layer 2 overlay (ADR-024) 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 alknet-hub sub-case); we
|
||||
don't need one for *it*.
|
||||
|
||||
### 3. The hub case is already covered by ADR-030's mixed-fingerprint `PeerEntry`
|
||||
|
||||
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>", // WebTransport / HTTPS path
|
||||
],
|
||||
auth_token_hash: Some("<sha256 of peer's bearer token>"),
|
||||
scopes: vec![...],
|
||||
resources: {...},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
When an alknet 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 alknet 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 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).
|
||||
|
||||
### 4. Browsers connecting to a hub are not alknet peers
|
||||
|
||||
A browser reaching a hub over WebTransport (or HTTPS) is served by the
|
||||
hub's `alknet-http` handler. 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** an
|
||||
alknet peer on the hub's side either — it does not get a `PeerId`, does
|
||||
not enter `PeerCompositeEnv`, and its "ops" are HTTP routes / WebTransport
|
||||
streams served by `alknet-http`, 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 alknet nodes (role 3
|
||||
hubs and role-3-style spoke nodes), never by browsers or pure HTTP
|
||||
clients.
|
||||
|
||||
### 5. WebTransport relay-as-proxy is deferred with h3 / WebTransport
|
||||
|
||||
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 ADR-030 §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.
|
||||
|
||||
WebTransport support is already deferred past v1 in the alknet-http
|
||||
Phase 0 findings (decision point DH-2, "h3/WebTransport — in v1 or
|
||||
deferred?"). The WebTransport-relay-as-proxy feature
|
||||
belongs in that same deferral bucket — it lands when `h3` /
|
||||
WebTransport lands, and it does not require any change to the auth
|
||||
model in this ADR. It is recorded here so it is not lost; it is not an
|
||||
open question for the auth model.
|
||||
|
||||
### 6. On-chain / smart-contract peer discovery fits the OQ-36 adapter pattern
|
||||
|
||||
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 (ADR-033): a future
|
||||
`alknet-peer-store-onchain` adapter implementing `IdentityProvider`
|
||||
against a smart contract is additive, exactly like
|
||||
`alknet-peer-store-sqlite`. 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 (OQ-36) and the git crate (OQ-10).
|
||||
It is noted here only to confirm it does not reopen OQ-37.
|
||||
|
||||
## What this does NOT change
|
||||
|
||||
- **`PeerEntry` struct shape** (ADR-030) — unchanged. Mixed
|
||||
fingerprints (Ed25519 + X.509) were already supported.
|
||||
- **`Identity` / `IdentityProvider` trait** — unchanged. The verifier
|
||||
choice is a `CallClient` / `from_openapi` / `from_mcp` concern, not
|
||||
an `IdentityProvider` concern.
|
||||
- **`CallCredentials` struct** — unchanged. `remote_identity` already
|
||||
carries the expected key type (OQ-29); this ADR specifies how the
|
||||
verifier is chosen from it (CA for unknown X.509 remotes, fingerprint
|
||||
match for known peers).
|
||||
- **`PeerCompositeEnv` / `PeerRef`** (ADR-029) — unchanged. Pure-client
|
||||
X.509 connections simply do not enter the peer-keyed overlay.
|
||||
- **`TlsIdentity`** (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.
|
||||
- **The no-env-vars invariant** — unaffected. The bearer token for the
|
||||
outgoing X.509 case still comes from `Capabilities`, 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 OQ-29
|
||||
flagged, with the peer-model criterion made explicit.
|
||||
- The hub case (mixed Ed25519 + X.509 fingerprints, browser access via
|
||||
WebTransport/HTTPS) is confirmed to need no new types — ADR-030's
|
||||
`fingerprints: Vec<String>` already covers it.
|
||||
- The WebTransport-relay-as-proxy and on-chain-discovery use cases are
|
||||
recorded with clear homes (h3/WebTransport deferral bucket; OQ-36
|
||||
adapter pattern) so they don't get lost and don't reopen the auth
|
||||
model.
|
||||
|
||||
**Negative:**
|
||||
- The `alknet-http` and `alknet-call` client paths must branch on
|
||||
"is this remote a known `PeerEntry`?" when selecting a
|
||||
`ServerCertVerifier`. This is a small implementation cost and is
|
||||
local to the client connection-establishment code; 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.
|
||||
|
||||
2. **Browsers never enter the peer-keyed overlay.** A browser is
|
||||
served by `alknet-http` (HTTP routes / WebTransport streams) 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.
|
||||
|
||||
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 (ADR-033 / 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
|
||||
- [ADR-027](027-tls-identity-redesign-acme-rawkey-decoupling.md) —
|
||||
`TlsIdentity` (RawKey / X509 / Acme), the browser limitation (no RFC
|
||||
7250), WebTransport requires X.509
|
||||
- [ADR-029](029-peer-graph-routing-model.md) — the peer-keyed overlay
|
||||
model that `PeerEntry` / `PeerId` feed into; pure-client connections
|
||||
are not in this graph
|
||||
- [ADR-030](030-peerentry-and-identity-id-decoupling.md) — `PeerEntry`
|
||||
with mixed fingerprints; fingerprint normalization (`ed25519:` across
|
||||
quinn/iroh); the `SHA256:<hex>` X.509 fingerprint format
|
||||
- [ADR-033](033-storage-boundary-and-repo-adapter-pattern.md) — the
|
||||
repo/adapter pattern that an on-chain `IdentityProvider` adapter
|
||||
follows; OQ-36 (concrete adapter shapes deferred for exploration)
|
||||
- [ADR-017](017-call-protocol-client-and-adapter-contract.md) §7 —
|
||||
`CallCredentials.remote_identity` (ADR-017 specified "expected
|
||||
fingerprint or cert"; this ADR §2 extends its semantics so that
|
||||
`remote_identity: None` + no `PeerEntry` + X.509 transport selects
|
||||
CA verification)
|
||||
- [ADR-024](024-operation-registry-layering.md) — the Layer 2
|
||||
per-connection overlay where ops discovered via `from_call` /
|
||||
`from_openapi` / `from_mcp` on a pure-client X.509 connection land
|
||||
- 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
|
||||
- OQ-10 (deferred) — git adapter scope; the on-chain / gossip-synced
|
||||
git-hosting hub use case in §6 is downstream of the git crate
|
||||
- OQ-36 (open, deferred for exploration) — concrete persistence adapter
|
||||
shapes; the on-chain `IdentityProvider` adapter in §6 follows this
|
||||
pattern
|
||||
- `docs/research/alknet-http/phase-0-findings.md` — DH-2 (h3 /
|
||||
WebTransport deferred past v1); the WebTransport-relay-as-proxy
|
||||
feature noted in this ADR's §5 belongs in that deferral bucket
|
||||
- `docs/research/references/iroh/iroh/04-sub-crates.md` — iroh's
|
||||
transport relay (`iroh-relay`), referenced to distinguish it from
|
||||
alknet's hub role
|
||||
- `docs/architecture/crates/core/auth.md` — amended: three-role
|
||||
naming, the outgoing X.509 verifier selection rule
|
||||
- `docs/architecture/crates/call/client-and-adapters.md` — amended:
|
||||
outgoing X.509 connection has no client-side `PeerId`; verifier
|
||||
selection by `PeerEntry` presence
|
||||
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-25
|
||||
last_updated: 2026-06-28
|
||||
---
|
||||
|
||||
# alknet-http — Phase 0 Research Findings
|
||||
@@ -166,6 +166,19 @@ HTTP/2 is sufficient. WebTransport is the browser path for the agent service
|
||||
lands as a fast-follow when the agent service needs browser streaming. This
|
||||
keeps v1 focused on the adapter + REST surface. Two-way door.
|
||||
|
||||
**WebTransport relay-as-proxy (recorded via ADR-034, not a v1 item):** a
|
||||
distinct WebTransport feature — a proxy that terminates the browser's
|
||||
WebTransport connection and forwards encrypted traffic to a P2P hub's
|
||||
Ed25519 endpoint (so the hub need not expose its own public X.509 cert)
|
||||
— belongs in this same deferral bucket. It does not change the auth
|
||||
model: the browser still authenticates by bearer token, the hub still
|
||||
resolves it via `PeerEntry.auth_token_hash`, and the proxy is
|
||||
transport-only. ADR-030 §6's fingerprint normalization
|
||||
(`ed25519:<hex>` across quinn/iroh) was already designed to keep the
|
||||
proxied path clean. See
|
||||
[ADR-034](../../architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md)
|
||||
§5 for the recording.
|
||||
|
||||
### DH-3: How does HTTP map to call-protocol operations?
|
||||
*(One-way door — needs an ADR)*
|
||||
|
||||
@@ -335,6 +348,12 @@ implementation detail; the credential (API key/token) comes from
|
||||
strategy for generated OpenAPI specs (tied to the registry's External
|
||||
operation set version) needs specifying. One-way door after first
|
||||
publication.
|
||||
- **OQ-HTTP-07 (WebTransport relay-as-proxy)**: a WebTransport proxy that
|
||||
fronts a P2P hub for browsers (so the hub need not expose public X.509)
|
||||
is a real feature for the browser-to-P2P-peer case. Deferred with h3 /
|
||||
WebTransport (DH-2); recorded in ADR-034 §5 so it is not lost. Does not
|
||||
change the auth model (bearer token + `PeerEntry.auth_token_hash`;
|
||||
proxy is transport-only). Two-way door; lands with the `h3` fast-follow.
|
||||
|
||||
## Next Steps (Phase 0 → Phase 1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user