diff --git a/crates/alknet-core/src/auth.rs b/crates/alknet-core/src/auth.rs index 75d872b..27f372c 100644 --- a/crates/alknet-core/src/auth.rs +++ b/crates/alknet-core/src/auth.rs @@ -1,7 +1,53 @@ //! Authentication: `AuthContext`, `Identity`, `IdentityProvider`, `AuthToken`, //! `ConfigIdentityProvider`. //! -//! See `docs/architecture/crates/core/auth.md` for the full specification. +//! See `docs/architecture/crates/core/auth.md` for the full specification and +//! [ADR-034](../../../docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md) +//! for the three-remote-roles decision. +//! +//! # Three remote roles (ADR-034 §1) +//! +//! The three credential types (`PeerEntry.fingerprints` entries) describe how +//! a *single* `PeerEntry` can be authenticated. Separately, there are three +//! distinct remote roles that must not be conflated: +//! +//! | 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 | +//! +//! `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. a third-party API) is **not** in that graph on the client +//! side: no `PeerEntry`, no `PeerId`, no `PeerRef::Specific` routing. The +//! asymmetry is deliberate — a public domain's operator can change hands, so +//! there is no stable logical identity to attach. +//! +//! The hub case is an ordinary `PeerEntry` that happens to expose both an +//! Ed25519 fingerprint (P2P path) and an X.509 fingerprint +//! (`SHA256:`, WebTransport/HTTPS path) — already supported by +//! `PeerEntry.fingerprints: Vec` (ADR-030). +//! +//! # Client-side verifier selection (ADR-034 §3) +//! +//! 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) | +//! | Yes (hub, Ed25519 path) | Ed25519 raw key | fingerprint match (`ed25519:`) | +//! | Yes (hub, X.509 path) | X.509 | fingerprint match (`SHA256:`) | +//! +//! This is the key-type-aware verifier from OQ-29, with the peer-model +//! criterion (ADR-034) made explicit. The client-side verifier selection is +//! a `CallClient` concern (`call/call-client-verifier-selection`), not an +//! `IdentityProvider` concern — `IdentityProvider` is unchanged by ADR-034. use std::collections::HashMap; use std::net::SocketAddr; diff --git a/crates/alknet-core/src/endpoint.rs b/crates/alknet-core/src/endpoint.rs index fc1e9f2..e69564f 100644 --- a/crates/alknet-core/src/endpoint.rs +++ b/crates/alknet-core/src/endpoint.rs @@ -1,6 +1,31 @@ //! Endpoint: `AlknetEndpoint`, `HandlerRegistry`, `EndpointError`. //! -//! See `docs/architecture/crates/core/endpoint.md` for the full specification. +//! See `docs/architecture/crates/core/endpoint.md` for the full specification +//! and [ADR-034](../../../docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md) +//! for the three-remote-roles decision. +//! +//! # Server-side vs client-side verifier concerns (ADR-034) +//! +//! This module's `AcceptAnyCertVerifier` is a **server-side** `ClientCertVerifier` +//! used in "request-but-don't-require" mode: the server asks for a client TLS +//! cert (X.509 or RFC 7250 raw key) so it can extract the fingerprint via +//! `peer_identity()`, but it does not require one and does not verify the +//! presented cert against a CA. The cert bytes are hashed to a fingerprint +//! string and matched against `PeerEntry.fingerprints` by +//! `IdentityProvider::resolve_from_fingerprint()`. Alknet's identity model is +//! fingerprint-based, not PKI-based — the `PeerEntry` set is the trust anchor, +//! not a root CA store. +//! +//! ADR-034 does **not** change the server-side endpoint. The **client-side** +//! `ServerCertVerifier` (for outgoing connections) is selected by `PeerEntry` +//! presence (ADR-034 §3): known peer (`PeerEntry` present) → fingerprint pin; +//! unknown X.509 remote (`PeerEntry` absent) → CA verification +//! (`WebPkiServerVerifier`); unknown Ed25519 raw-key remote → fail closed. +//! That selection is a `CallClient` concern +//! (`call/call-client-verifier-selection`), not an endpoint concern — +//! `AcceptAnyCertVerifier` here is only safe for raw-key fingerprint +//! extraction on the *server* side and must not be reused as a client-side +//! verifier. use std::collections::HashMap; use std::io; @@ -740,6 +765,19 @@ fn generate_self_signed_cert() -> Result { } #[cfg(feature = "quinn")] +/// Server-side "request-but-don't-require" client cert verifier (ADR-034). +/// +/// Asks for a client TLS cert (X.509 or RFC 7250 raw key) so the endpoint can +/// extract the fingerprint via `peer_identity()`, but does not require one +/// and does not verify the presented cert against a CA. The fingerprint is +/// matched against `PeerEntry.fingerprints` by +/// `IdentityProvider::resolve_from_fingerprint()`. +/// +/// **Server-side only.** This must not be reused as a client-side +/// `ServerCertVerifier` — the client-side verifier is selected by `PeerEntry` +/// presence (ADR-034 §3): CA verification for unknown X.509 remotes, +/// fingerprint pinning for known peers. See the module docs and +/// `call/call-client-verifier-selection`. struct AcceptAnyCertVerifier; #[cfg(feature = "quinn")]