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.
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-27 |
alknet-core
Core library for ALPN-based protocol dispatch. Every handler crate depends on alknet-core.
Documents
| Document | Status | Description |
|---|---|---|
| core-types.md | draft | ProtocolHandler trait, HandlerError, Connection, BiStream, StreamError |
| endpoint.md | draft | ALPN router, HandlerRegistry, accept loop, graceful shutdown |
| auth.md | draft | AuthContext, Identity, IdentityProvider, AuthToken, resolution flow, PeerEntry, CredentialStore |
| config.md | draft | StaticConfig, DynamicConfig, ArcSwap, ConfigReloadHandle, AuthPolicy.peers |
Applicable ADRs
| ADR | Title | Relevance |
|---|---|---|
| 001 | ALPN-Based Protocol Dispatch | Core architectural model |
| 002 | ProtocolHandler Trait | The trait every handler implements |
| 003 | Crate Decomposition | alknet-core's position in the crate graph |
| 004 | Auth as Shared Core | IdentityProvider in core |
| 006 | ALPN String Convention | ALPN format, one-ALPN-per-connection |
| 007 | BiStream Type Definition | Connection, BiStream trait, SendStream, RecvStream |
| 009 | One-Way Door Framework | Decision classification |
| 010 | ALPN Router and Endpoint | Endpoint, HandlerRegistry, accept loop |
| 011 | AuthContext Structure | AuthContext fields and resolution flow |
| 015 | Privilege Model and Authority Context | Per-request identity on OperationContext; admin scope for config reload |
| 030 | PeerEntry and Identity.id Decoupling | authorized_fingerprints → peers: Vec<PeerEntry>; Identity.id = peer_id (stable) |
| 031 | CredentialStore Repo Trait | Second repo trait in core; InMemoryCredentialStore default adapter |
| 033 | Storage Boundary and Repo/Adapter Pattern | Core defines traits + in-memory defaults; persistence adapters are separate crates |
Relevant Open Questions
| OQ | Title | Status | Relevance |
|---|---|---|---|
| OQ-04 | Dynamic handler registration | resolved (start static) | HandlerRegistry is immutable at startup |
| OQ-05 | Multi-connectivity endpoint | resolved (quinn + iroh) | AlknetEndpoint supports both, both feature-gated |
| OQ-11 | Handler-level auth resolution observability | resolved | Handlers store resolved identity on Connection; two identity scopes (connection-level for observability, per-request for ACL) |
| OQ-33 | PeerId — logical id vs crypto identity | resolved by ADR-030 | PeerId = Identity.id = PeerEntry.peer_id (stable across key rotation) |
| 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 | 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. |
Key Design Principles
- One trait, one dispatch point:
ProtocolHandleris the only abstraction handlers implement. No StreamInterface/MessageInterface split. - ALPN does the routing: The endpoint dispatches by ALPN string. No byte-peeking, no ListenerConfig enum.
- Handlers own their wire format: Each handler manages its own protocol parsing. alknet-core provides the Connection, not the framing.
- Auth is hybrid: The endpoint provides what it can (TLS-level auth). Handlers complete what they need. AuthContext may be partial.
- WASM door preserved: BiStream is a trait, Connection is an opaque type. Core types don't assume tokio or quinn in public APIs.