# Authentication Service Internals The Authentication Service (AS) stores and distributes single-use MLS KeyPackages. It is one of the two logical services exposed through the unified `NodeService` RPC interface. The AS also stores hybrid (X25519 + ML-KEM-768) public keys for post-quantum envelope encryption. This page covers the server-side implementation of KeyPackage storage, the `Auth` struct validation logic, and the hybrid key endpoints. **Sources:** - `crates/quicnprotochat-server/src/main.rs` (RPC handlers, auth validation) - `crates/quicnprotochat-server/src/storage.rs` (FileBackedStore) - `schemas/node.capnp` (wire schema) --- ## KeyPackage Storage ### Data Model KeyPackages are stored in a `FileBackedStore` using a `Mutex`-protected `HashMap`: ```text key_packages: Mutex, VecDeque>>> ^ ^ | | identity_key FIFO queue of (32-byte Ed25519 TLS-encoded public key) KeyPackage bytes ``` Each identity can have multiple KeyPackages queued. This is essential because KeyPackages are single-use (per RFC 9420): once fetched by a peer, they are permanently removed. Clients should upload several KeyPackages to handle concurrent group invitations. The map is persisted to `data/keypackages.bin` using bincode serialization, wrapped in the `QueueMapV1` struct. See [Storage Backend](storage-backend.md) for persistence details. ### uploadKeyPackage ```capnp uploadKeyPackage @0 (identityKey :Data, package :Data, auth :Auth) -> (fingerprint :Data); ``` **Handler logic:** 1. **Parse parameters.** Extract `identityKey`, `package`, and `auth`. 2. **Validate auth.** Call `validate_auth()` (see [Auth Validation](#auth-validation) below). 3. **Validate inputs:** | Check | Constraint | Error Message | |-------|------------|---------------| | Identity key length | Exactly 32 bytes | `"identityKey must be exactly 32 bytes, got {n}"` | | Package non-empty | `package.len() > 0` | `"package must not be empty"` | | Package size cap | `package.len() <= 1,048,576` | `"package exceeds max size (1048576 bytes)"` | 4. **Compute fingerprint.** `SHA-256(package_bytes)` produces a 32-byte digest. 5. **Store.** `FileBackedStore::upload_key_package(identity_key, package)` pushes the package to the back of the identity's `VecDeque` and flushes to disk. 6. **Return fingerprint.** The SHA-256 hash is set in the response. The fingerprint allows the uploading client to verify that the server stored the exact bytes it sent. See [KeyPackage Exchange Flow](keypackage-exchange.md) for the client-side verification logic. ### fetchKeyPackage ```capnp fetchKeyPackage @1 (identityKey :Data, auth :Auth) -> (package :Data); ``` **Handler logic:** 1. **Parse and validate** `identityKey` (32 bytes) and `auth`. 2. **Pop from queue.** `FileBackedStore::fetch_key_package(identity_key)` calls `VecDeque::pop_front()` on the identity's queue, removing and returning the oldest KeyPackage. The updated map is flushed to disk. 3. **Return.** If a KeyPackage was available, set it in the response. If the queue was empty (or the identity has no entry), return empty `Data`. **Single-use semantics:** The `pop_front()` operation ensures each KeyPackage is returned exactly once. This is critical for MLS security -- reusing a KeyPackage would allow conflicting group states. The removal is atomic with respect to the `Mutex` lock, so concurrent fetch requests will not receive the same package. **Empty response handling:** The client checks `package.is_empty()` to distinguish between "no packages available" and "package fetched." An empty response is not an error -- it means the target identity has exhausted their KeyPackage supply and needs to upload more. --- ## Auth Validation All `NodeService` RPC methods accept an `Auth` struct: ```capnp struct Auth { version @0 :UInt16; # 0 = legacy/none, 1 = token-based accessToken @1 :Data; # opaque bearer token deviceId @2 :Data; # optional UUID for auditing } ``` The server validates this struct through the `validate_auth` function: ```text validate_auth(cfg, auth) | +-- version == 0? | +-- cfg.allow_legacy_v0 == true? -> OK | +-- cfg.allow_legacy_v0 == false? -> ERROR "auth version 0 disabled" | +-- version == 1? | +-- accessToken empty? -> ERROR "requires non-empty accessToken" | +-- cfg.required_token is Some? | | +-- token matches? -> OK | | +-- token mismatch? -> ERROR "invalid accessToken" | +-- cfg.required_token is None? -> OK (any non-empty token accepted) | +-- version >= 2? -> ERROR "unsupported auth version" ``` ### AuthConfig The server's auth behavior is controlled by `AuthConfig`: ```rust struct AuthConfig { required_token: Option>, // None = accept any token allow_legacy_v0: bool, // true = accept version 0 (no auth) } ``` Configured via CLI flags / environment variables: | Flag / Env Var | Default | Purpose | |-----------------------------------|---------|---------| | `--auth-token` / `QUICNPROTOCHAT_AUTH_TOKEN` | None | Required bearer token. If unset, any non-empty token is accepted for version 1. | | `--allow-auth-v0` / `QUICNPROTOCHAT_ALLOW_AUTH_V0` | `true` | Whether to accept `auth.version=0` (legacy, unauthenticated) requests. | ### Version Semantics | Version | Meaning | Token Required? | |---------|---------|-----------------| | 0 | Legacy / unauthenticated | No. Token is ignored. Server must have `allow_legacy_v0 = true`. | | 1 | Token-based authentication | Yes. Must be non-empty. Must match `required_token` if configured. | | 2+ | Reserved for future use | Rejected. | ### Current Limitations The current auth implementation is intentionally minimal: - **No identity binding.** The access token is not tied to a specific Ed25519 identity. Any valid token can upload or fetch KeyPackages for any identity. - **No rate limiting.** There is no per-identity or per-IP rate limiting. - **No token rotation.** Tokens are static strings configured at server startup. - **No device management.** The `deviceId` field is accepted but not used for authorization decisions. The [Auth, Devices, and Tokens](../roadmap/authz-plan.md) roadmap item addresses these gaps with a proper token issuance and validation system. --- ## Hybrid Key Endpoints The AS also stores hybrid (X25519 + ML-KEM-768) public keys for post-quantum envelope encryption. Unlike KeyPackages, hybrid keys are **not single-use** -- they are stored persistently and can be fetched multiple times. ### uploadHybridKey ```capnp uploadHybridKey @6 (identityKey :Data, hybridPublicKey :Data) -> (); ``` **Handler logic:** 1. Validate `identityKey` (32 bytes) and `hybridPublicKey` (non-empty). 2. `FileBackedStore::upload_hybrid_key(identity_key, hybrid_pk)` stores the key, overwriting any previous value for this identity. 3. Flushes to `data/hybridkeys.bin`. The storage model is simpler than KeyPackages: a flat `HashMap, Vec>` (identity key to hybrid public key bytes). There is no queue -- each identity has at most one hybrid public key. ### fetchHybridKey ```capnp fetchHybridKey @7 (identityKey :Data) -> (hybridPublicKey :Data); ``` **Handler logic:** 1. Validate `identityKey` (32 bytes). 2. Look up the hybrid public key in the store. Unlike `fetchKeyPackage`, this does **not** remove the key -- it can be fetched repeatedly. 3. Return the key bytes, or empty `Data` if none is stored. See [Hybrid KEM](../protocol-layers/hybrid-kem.md) for how the client uses these keys to wrap MLS payloads in post-quantum envelopes. --- ## NodeServiceImpl Structure The server-side implementation struct: ```rust struct NodeServiceImpl { store: Arc, // shared across connections waiters: Arc, Arc>>, // long-poll notification auth_cfg: Arc, // auth policy } ``` All connections share the same `store` and `waiters` via `Arc`. The `DashMap, Arc>` is keyed by recipient key and provides the push-notification mechanism for `fetchWait`. See [Delivery Service Internals](delivery-service.md) for the long-polling implementation. --- ## Connection Model ```text QUIC endpoint (port 7000) +-- TLS 1.3 handshake (self-signed cert by default) +-- Accept bidirectional stream +-- capnp-rpc VatNetwork (Side::Server) +-- NodeServiceImpl { store, waiters, auth_cfg } ``` Each QUIC connection opens one bidirectional stream for Cap'n Proto RPC. The `capnp-rpc` crate uses `Rc>` internally, making it `!Send`. All RPC tasks run on a `tokio::task::LocalSet` to satisfy this constraint. The server generates a self-signed TLS certificate on first start if no certificate files exist. Certificate and key paths are configurable via `--tls-cert` and `--tls-key`. --- ## Health Endpoint ```capnp health @5 () -> (status :Text); ``` A simple readiness probe. Returns `"ok"` unconditionally. No auth validation is performed. Useful for infrastructure health checks and measuring QUIC round-trip time. --- ## Related Pages - [KeyPackage Exchange Flow](keypackage-exchange.md) -- end-to-end upload and fetch flow including client-side logic - [Delivery Service Internals](delivery-service.md) -- the DS half of NodeService - [Storage Backend](storage-backend.md) -- FileBackedStore persistence model - [GroupMember Lifecycle](group-member-lifecycle.md) -- how KeyPackages are generated and consumed - [Auth, Devices, and Tokens](../roadmap/authz-plan.md) -- planned auth improvements - [NodeService Schema](../wire-format/node-service-schema.md) -- Cap'n Proto schema reference - [Hybrid KEM](../protocol-layers/hybrid-kem.md) -- post-quantum envelope encryption