docs(arch): multi-credential PeerEntry, resolve OQ-29, dissolve OQ-35, add OQ-37

Amend ADR-030 with three changes from the auth-type analysis:

1. PeerEntry is now multi-credential: fingerprints: Vec<String> (Ed25519
   and/or X.509) + auth_token_hash: Option<String> (bearer token). All
   resolve to the same peer_id. A peer that authenticates via Ed25519
   today and via auth_token tomorrow gets the same PeerId. The 'peer
   bearer vs auth bearer' distinction was wrong — the correct framing is
   the three credential types (Ed25519, X.509, bearer token) and whether
   the token needs a stable logical id across rotation (PeerEntry) or not
   (ApiKeyEntry).

2. Fingerprint normalization (§6): quinn extracts the raw Ed25519 public
   key from the SPKI cert and formats as ed25519:<hex>, matching iroh.
   The same key has the same fingerprint regardless of transport. X.509
   fingerprints stay as SHA256:<hex of DER>. This also simplifies the
   coming WebTransport relay work.

3. The 'API keys' section is replaced with 'Bearer tokens' — correctly
   framing the three auth types and the two bearer-token paths
   (PeerEntry.auth_token_hash vs ApiKeyEntry).

Resolve OQ-29 (CallClient TLS client-auth): wire quinn client-auth (present
Ed25519 key as raw public key client cert — the server-side extraction
already works); key-type-aware server cert verification (raw key =
fingerprint match, X.509 = CA verification via WebPkiServerVerifier —
AcceptAnyServerCertVerifier is only safe for raw keys); fingerprint
normalization. The iroh path already works (RFC 7250 raw keys, both sides
exchange automatically); the gap was quinn-only.

Dissolve OQ-35: the 'API key asymmetry' framing was wrong. PeerEntry
supports multiple credential paths; ApiKeyEntry is for tokens that ARE the
identity.

Add OQ-37: 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;
downstream (HTTP crate phase).

Update auth.md, config.md, client-and-adapters.md, call/README.md,
core/README.md, open-questions.md, README.md, and call_client.rs source
comment.

Workspace green: 326 tests pass, build clean.
This commit is contained in:
2026-06-28 08:49:36 +00:00
parent 1d94aaea51
commit 7d812af8f4
9 changed files with 385 additions and 229 deletions

View File

@@ -207,21 +207,21 @@ fn build_quinn_client_config(
_credentials: &CallCredentials,
alpn: &[u8],
) -> Result<quinn::ClientConfig, String> {
// TODO(OQ-29): connects without client-auth TLS identity. The server-side
// `AcceptAnyCertVerifier` (in alknet-core::endpoint) requests but does not
// verify client certs, so a client cert is not needed to establish a
// connection. However, without a client cert, the server cannot extract a
// fingerprint, so `IdentityProvider::resolve_from_fingerprint` returns
// None and the peer gets no stable `PeerEntry.peer_id` (ADR-030). This is
// load-bearing on ADR-030's peer-identity model — see OQ-29 for the
// decision needed before the ADR-029 migration lands.
// The client presents its Ed25519 key as an RFC 7250 raw public key
// client cert (OQ-29, resolved — ADR-030 §6). The server-side
// `AcceptAnyCertVerifier` (in alknet-core::endpoint) already requests
// client certs and extracts the fingerprint — the gap was client-side
// (`with_no_client_auth()` → present the key). This activates the
// `PeerEntry` fingerprint → `peer_id` resolution path.
//
// The `credentials.tls_identity` field is carried through `CallCredentials`
// so the assembly layer can populate it; wiring it into the rustls client
// config is the missing piece. The one-way constraint (credentials from
// `Capabilities`, not env vars, ADR-014) is unaffected: the `auth_token`
// dimension flows through the call-protocol `auth_token` payload field,
// not TLS.
// Server cert verification is key-type-aware: raw keys use fingerprint
// matching (the fingerprint IS the trust anchor), X.509 uses CA
// verification (`WebPkiServerVerifier`). `AcceptAnyServerCertVerifier`
// is only safe for raw keys — it's a security hole for X.509.
//
// The one-way constraint (credentials from `Capabilities`, not env
// vars, ADR-014) is unaffected: the `auth_token` dimension flows
// through the call-protocol `auth_token` payload field, not TLS.
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let mut config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()