feat: add post-quantum hybrid KEM + SQLCipher persistence
Feature 1 — Post-Quantum Hybrid KEM (X25519 + ML-KEM-768): - Create hybrid_kem.rs with keygen, encrypt, decrypt + 11 unit tests - Wire format: version(1) | x25519_eph_pk(32) | mlkem_ct(1088) | nonce(12) | ct - Add uploadHybridKey/fetchHybridKey RPCs to node.capnp schema - Server: hybrid key storage in FileBackedStore + RPC handlers - Client: hybrid keypair in StoredState, auto-wrap/unwrap in send/recv/invite/join - demo-group runs full hybrid PQ envelope round-trip Feature 2 — SQLCipher Persistence: - Extract Store trait from FileBackedStore API - Create SqlStore (rusqlite + bundled-sqlcipher) with encrypted-at-rest SQLite - Schema: key_packages, deliveries, hybrid_keys tables with indexes - Server CLI: --store-backend=sql, --db-path, --db-key flags - 5 unit tests for SqlStore (FIFO, round-trip, upsert, channel isolation) Also includes: client lib.rs refactor, auth config, TOML config file support, mdBook documentation, and various cleanups by user. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
279
docs/src/internals/authentication-service.md
Normal file
279
docs/src/internals/authentication-service.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# 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<HashMap<Vec<u8>, VecDeque<Vec<u8>>>>
|
||||
^ ^
|
||||
| |
|
||||
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<Vec<u8>>, // 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<u8>, Vec<u8>>` (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<FileBackedStore>, // shared across connections
|
||||
waiters: Arc<DashMap<Vec<u8>, Arc<Notify>>>, // long-poll notification
|
||||
auth_cfg: Arc<AuthConfig>, // auth policy
|
||||
}
|
||||
```
|
||||
|
||||
All connections share the same `store` and `waiters` via `Arc`. The
|
||||
`DashMap<Vec<u8>, Arc<Notify>>` 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<RefCell<>>` 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
|
||||
Reference in New Issue
Block a user