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:
2026-02-22 08:07:48 +01:00
parent d1ddef4cea
commit f334ed3d43
81 changed files with 14502 additions and 2289 deletions

View File

@@ -0,0 +1,232 @@
# Crate Responsibilities
The quicnprotochat workspace is split into four crates with strict layering
rules. Each crate owns one concern and depends only on the crates below it.
This page documents what each crate provides, what it explicitly avoids, and
how the crates relate to one another.
---
## Dependency Flow Diagram
```text
┌──────────────────────────┐
│ quicnprotochat-client │
│ (CLI, QUIC client, │
│ GroupMember orchestr.) │
└─────────┬───────┬────────┘
│ │
┌───────┘ └────────┐
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ quicnprotochat-core │ │ quicnprotochat-server │
│ (crypto, Noise, │ │ (QUIC listener, │
│ MLS, hybrid KEM) │ │ NodeService RPC, │
│ │ │ storage) │
└──────────┬─────────────┘ └─────────┬──────────────┘
│ │
│ ┌───────────────────┘
▼ ▼
┌────────────────────────┐
│ quicnprotochat-proto │
│ (Cap'n Proto schemas, │
│ codegen, helpers) │
└────────────────────────┘
```
**Arrows point from dependant to dependency.** The proto crate sits at the base
of the dependency graph. The core crate depends on proto for envelope
serialisation. The server and client crates both depend on core and proto.
---
## quicnprotochat-core
**Role:** Pure cryptographic logic and transport primitives. No network I/O
(except for the Noise handshake helpers that take an existing `TcpStream`). No
async runtime dependency beyond what Noise transport needs.
### Modules
| Module | Public API | Description |
|---------------|-----------------------------------------------------------------------------|-------------|
| `keypair` | `NoiseKeypair` | Static X25519 keypair for Noise_XX. `StaticSecret` is `ZeroizeOnDrop`. `private_bytes()` returns `Zeroizing<[u8; 32]>`. |
| `identity` | `IdentityKeypair` | Ed25519 signing keypair for MLS credentials. Seed stored as `Zeroizing<[u8; 32]>`. Implements `openmls_traits::Signer`. |
| `noise` | `handshake_initiator`, `handshake_responder`, `NoiseTransport` | Noise_XX_25519_ChaChaPoly_BLAKE2s handshake over TCP. `NoiseTransport` provides `send_frame`/`recv_frame`, envelope helpers, and `into_capnp_io()` bridge. |
| `codec` | `LengthPrefixedCodec`, `NOISE_MAX_MSG` | Tokio `Encoder<Bytes>` + `Decoder`. 4-byte LE length prefix. Max frame 65,535 bytes. |
| `group` | `GroupMember` | MLS group state machine wrapping `openmls::MlsGroup`. Lifecycle: `new` -> `generate_key_package` -> `create_group` / `join_group` -> `send_message` / `receive_message`. |
| `keypackage` | `generate_key_package` | Standalone KeyPackage generation (returns TLS-encoded bytes + SHA-256 fingerprint). |
| `keystore` | `DiskKeyStore`, `StoreCrypto` | `OpenMlsKeyStore` implementation backed by an in-memory `HashMap` with optional bincode flush to disk. `StoreCrypto` couples `RustCrypto` + `DiskKeyStore` into an `OpenMlsCryptoProvider`. |
| `hybrid_kem` | `HybridKeypair`, `HybridPublicKey`, `hybrid_encrypt`, `hybrid_decrypt` | X25519 + ML-KEM-768 hybrid KEM. HKDF-SHA256 key derivation, ChaCha20-Poly1305 AEAD. Versioned envelope wire format. |
| `error` | `CoreError`, `CodecError`, `MAX_PLAINTEXT_LEN` | Unified error types. `CoreError` covers Noise, Codec, Cap'n Proto, MLS, and hybrid KEM failures. |
### What this crate does NOT do
- No network I/O beyond the Noise helpers (which take a pre-connected `TcpStream`).
- No QUIC or TLS -- that is the server and client crates' concern.
- No async runtime setup (it uses Tokio types internally but does not spawn or
manage a runtime).
- No CLI parsing.
### Key dependencies
`snow`, `x25519-dalek`, `ed25519-dalek`, `openmls`, `openmls_rust_crypto`,
`openmls_traits`, `tls_codec`, `ml-kem`, `chacha20poly1305`, `hkdf`, `sha2`,
`zeroize`, `capnp`, `quicnprotochat-proto`, `tokio`, `tokio-util`, `futures`,
`bytes`, `serde`, `bincode`, `serde_json`, `thiserror`.
---
## quicnprotochat-proto
**Role:** Cap'n Proto schema definitions, compile-time code generation, and
pure-synchronous serialisation helpers. This crate is the single source of truth
for the wire format.
### Contents
| Item | Description |
|---------------------------|-------------|
| `schemas/envelope.capnp` | `Envelope` struct and `MsgType` enum -- top-level wire message for Noise-channel traffic. |
| `schemas/auth.capnp` | `AuthenticationService` interface -- `uploadKeyPackage`, `fetchKeyPackage`. |
| `schemas/delivery.capnp` | `DeliveryService` interface -- `enqueue`, `fetch`. |
| `schemas/node.capnp` | `NodeService` interface (unified AS+DS) -- all RPC methods plus `Auth` struct. |
| `build.rs` | Invokes `capnpc` to generate Rust types from the four `.capnp` files. |
| `lib.rs` | `pub mod envelope_capnp`, `auth_capnp`, `delivery_capnp`, `node_capnp` -- re-exports generated modules. |
| `MsgType` | Re-exported enum from `envelope_capnp::envelope::MsgType`. |
| `ParsedEnvelope` | Owned, `Send + 'static` representation of a decoded `Envelope`. All byte fields are eagerly copied out of the Cap'n Proto reader. |
| `build_envelope` | Serialise a `ParsedEnvelope` to unpacked Cap'n Proto wire bytes. |
| `parse_envelope` | Deserialise wire bytes into a `ParsedEnvelope`. |
| `to_bytes` / `from_bytes` | Low-level Cap'n Proto message <-> byte conversions. |
### What this crate does NOT do
- **No crypto** -- key material never enters this crate.
- **No I/O** -- callers own the transport; this crate only converts bytes to
types and back.
- **No async** -- pure synchronous data-layer code.
### Key dependencies
`capnp` (runtime), `capnpc` (build-time only).
---
## quicnprotochat-server
**Role:** Network-facing server binary. Accepts QUIC + TLS 1.3 connections,
dispatches Cap'n Proto RPC calls to `NodeServiceImpl`, and persists state to
disk via `FileBackedStore`.
### Components
| Component | Description |
|----------------------|-------------|
| `NodeServiceImpl` | Implements `node_service::Server` (Cap'n Proto generated trait). Handles all eight RPC methods: `uploadKeyPackage`, `fetchKeyPackage`, `enqueue`, `fetch`, `fetchWait`, `health`, `uploadHybridKey`, `fetchHybridKey`. |
| `FileBackedStore` | Mutex-guarded `HashMap`s for KeyPackages (keyed by Ed25519 public key), delivery queues (keyed by `ChannelKey = (channelId, recipientKey)`), and hybrid public keys. Each mutation flushes the full map to a bincode file on disk. |
| `DashMap` waiters | `DashMap<Vec<u8>, Arc<Notify>>` -- per-recipient `tokio::sync::Notify` instances for `fetchWait` long-polling. `enqueue` calls `notify_waiters()` after appending. |
| TLS config | Self-signed certificate auto-generated on first run (`rcgen`). TLS 1.3 only, ALPN `capnp`. |
| CLI (`clap`) | `--listen` (default `0.0.0.0:7000`), `--data-dir`, `--tls-cert`, `--tls-key`. |
### Connection lifecycle
```text
QUIC accept
└─ TLS 1.3 handshake (self-signed cert, ALPN "capnp")
└─ accept_bi() -> bidirectional QUIC stream
└─ tokio_util::compat adapters (AsyncRead/AsyncWrite)
└─ capnp-rpc twoparty::VatNetwork (Side::Server)
└─ RpcSystem drives NodeServiceImpl
```
Because `capnp-rpc` uses `Rc<RefCell<>>` internally and is therefore `!Send`,
the entire RPC stack runs on a `tokio::task::LocalSet`. Each incoming connection
is handled by `spawn_local`.
### What this crate does NOT do
- No direct crypto operations (it delegates to `quicnprotochat-core` types
for fingerprinting and storage only).
- No MLS processing -- all payloads are opaque byte strings.
- No Noise transport (QUIC/TLS only).
### Key dependencies
`quicnprotochat-core`, `quicnprotochat-proto`, `quinn`, `quinn-proto`,
`rustls`, `rcgen`, `capnp`, `capnp-rpc`, `tokio`, `tokio-util`, `dashmap`,
`sha2`, `clap`, `tracing`, `anyhow`, `thiserror`, `bincode`, `serde`.
---
## quicnprotochat-client
**Role:** CLI client binary. Connects to the server over QUIC + TLS 1.3,
orchestrates MLS group operations via `GroupMember`, and persists identity and
group state to disk.
### Components
| Component | Description |
|-------------------------|-------------|
| `connect_node` | Establishes a QUIC/TLS connection, opens a bidirectional stream, and bootstraps a `capnp-rpc` `RpcSystem` to obtain a `node_service::Client`. |
| CLI subcommands (`clap`)| `ping`, `register`, `fetch-key`, `demo-group`, `register-state`, `create-group`, `invite`, `join`, `send`, `recv`. |
| `GroupMember` usage | The client creates a `GroupMember` (from `quicnprotochat-core`), calls `generate_key_package` / `create_group` / `add_member` / `join_group` / `send_message` / `receive_message`. |
| State persistence | `StoredState` holds `identity_seed` (32 bytes) and optional serialised `MlsGroup`. A companion `.ks` file stores the `DiskKeyStore` with HPKE init private keys. |
| Auth context | `ClientAuth` bundles an optional bearer token and device ID. Passed to every RPC via the `Auth` struct in `node.capnp`. |
### CLI subcommand summary
| Subcommand | What it does |
|-------------------|--------------|
| `ping` | Call `health()` and print RTT. |
| `register` | Generate a fresh identity + KeyPackage, upload to AS, print identity key. |
| `register-state` | Same as `register` but uses/creates persistent state file. |
| `fetch-key` | Fetch a peer's KeyPackage by hex identity key. |
| `create-group` | Create a new MLS group and save state. |
| `invite` | Fetch peer's KeyPackage, add to group, enqueue Welcome via DS. |
| `join` | Fetch Welcome from DS, join the MLS group. |
| `send` | Encrypt a message with MLS, enqueue via DS. |
| `recv` | Fetch pending payloads from DS, decrypt with MLS. Supports `--stream` for continuous long-polling. |
| `demo-group` | End-to-end Alice+Bob round-trip (ephemeral identities). |
### What this crate does NOT do
- No server-side logic.
- No Noise transport (QUIC/TLS only for server communication).
- No direct crypto beyond calling `GroupMember` and verifying SHA-256
fingerprints.
### Key dependencies
`quicnprotochat-core`, `quicnprotochat-proto`, `quinn`, `quinn-proto`,
`rustls`, `capnp`, `capnp-rpc`, `tokio`, `tokio-util`, `clap`, `sha2`,
`serde`, `bincode`, `anyhow`, `thiserror`, `tracing`.
---
## Layering Rules
1. **proto** depends on nothing in-workspace. It is pure data definition.
2. **core** depends on **proto** (for `ParsedEnvelope` and envelope helpers).
It does not depend on server or client.
3. **server** depends on **core** and **proto**. It does not depend on client.
4. **client** depends on **core** and **proto**. It does not depend on server.
5. **server** and **client** never depend on each other. They communicate
exclusively via the Cap'n Proto RPC wire protocol.
This layering ensures that:
- Crypto code can be tested in isolation (`cargo test -p quicnprotochat-core`).
- Schema changes propagate automatically through codegen.
- The server binary contains no client-side MLS orchestration logic.
- The client binary contains no server-side storage or listener logic.
---
## Further Reading
- [Architecture Overview](overview.md) -- high-level system diagram
- [Service Architecture](service-architecture.md) -- NodeService RPC details
- [Wire Format Overview](../wire-format/overview.md) -- Cap'n Proto schema reference
- [GroupMember Lifecycle](../internals/group-member-lifecycle.md) -- MLS state machine details
- [Storage Backend](../internals/storage-backend.md) -- FileBackedStore internals

View File

@@ -0,0 +1,350 @@
# End-to-End Data Flow
This page traces the three core data flows through the quicnprotochat system:
registration, group creation, and message exchange. Each flow is illustrated
with an ASCII sequence diagram showing control-plane (AS) and data-plane (DS)
traffic.
Throughout these flows the server is **MLS-unaware** -- it stores and forwards
opaque byte blobs without parsing their MLS content.
---
## 1. Registration Flow
Before a client can join any MLS group, it must generate an Ed25519 identity
keypair and upload at least one KeyPackage to the Authentication Service. Peers
fetch these KeyPackages to add the client to groups.
### Sequence Diagram
```text
Client (Alice) NodeService (AS)
────────────── ────────────────
│ │
│ 1. Generate Ed25519 identity keypair │
│ (IdentityKeypair::generate) │
│ │
│ 2. Generate MLS KeyPackage │
│ (GroupMember::generate_key_package) │
│ - Creates HPKE init keypair │
│ - Embeds Ed25519 pk in credential │
│ - Signs leaf node with Ed25519 sk │
│ - TLS-encodes the KeyPackage │
│ │
│ 3. QUIC connect + TLS 1.3 handshake │
│ ────────────────────────────────────────>│
│ │
│ 4. uploadKeyPackage(identityKey, pkg) │
│ ────────────────────────────────────────>│
│ │ 5. Validate:
│ │ - identityKey == 32 bytes
│ │ - package non-empty, <= 1 MB
│ │ - auth version allowed
│ │
│ │ 6. Compute SHA-256(package)
│ │
│ │ 7. Append to per-identity queue:
│ │ keyPackages[identityKey].push(pkg)
│ │
│ │ 8. Flush keypackages.bin to disk
│ │
│ fingerprint (SHA-256) │
│ <────────────────────────────────────────│
│ │
│ 9. Compare local fingerprint with │
│ server-returned fingerprint │
│ (tamper detection) │
│ │
```
### Key Points
- **KeyPackages are single-use** (RFC 9420 requirement). Each `fetchKeyPackage`
call atomically removes and returns one package. The client should upload
multiple KeyPackages if it expects to be added to several groups.
- The `identityKey` used as the AS index is the **raw 32-byte Ed25519 public
key**, not a fingerprint or hash. Peers must know Alice's public key out-of-
band (QR code, directory, etc.) to fetch her KeyPackage.
- The HPKE init private key generated during `generate_key_package` is stored
in the client's `DiskKeyStore`. The **same `GroupMember` instance** (or a
restored instance with the same key store) must later call `join_group` to
decrypt the Welcome message.
- The optional hybrid public key (`uploadHybridKey`) can also be uploaded
during registration for post-quantum envelope encryption.
---
## 2. Group Creation Flow
Alice creates a new MLS group, fetches Bob's KeyPackage from the AS, adds Bob
to the group (producing a Commit and a Welcome), and delivers the Welcome to
Bob via the DS.
### Sequence Diagram
```text
Alice NodeService (AS+DS) Bob
───── ────────────────── ───
│ │ │
│ 1. create_group("my-group") │ │
│ (local MLS operation -- │ │
│ Alice is sole member, │ │
│ epoch 0) │ │
│ │ │
│ 2. fetchKeyPackage(bob_pk) │ │
│ ───────────────────────────────>│ │
│ │ 3. Pop bob's KeyPackage │
│ │ from queue (atomic) │
│ bob_kp bytes │ │
│ <───────────────────────────────│ │
│ │ │
│ 4. add_member(bob_kp) │ │
│ Local MLS operations: │ │
│ a. Deserialise & validate │ │
│ Bob's KeyPackage │ │
│ b. Produce Commit message │ │
│ (adds Bob to ratchet │ │
│ tree, advances epoch) │ │
│ c. Produce Welcome message │ │
│ (encrypted to Bob's │ │
│ HPKE init key, contains │ │
│ group secrets + tree) │ │
│ d. merge_pending_commit() │ │
│ (Alice advances to │ │
│ epoch 1 locally) │ │
│ │ │
│ 5. enqueue(bob_pk, welcome) │ │
│ ───────────────────────────────>│ │
│ │ 6. Append welcome to │
│ │ deliveries[(ch, bob_pk)] │
│ │ │
│ │ 7. Notify bob_pk waiters │
│ │ │
│ │ │
│ │ 8. Bob connects and fetches │
│ │ <─────────────────────────────│
│ │ fetch(bob_pk) │
│ │ │
│ │ 9. Drain bob's queue │
│ │ (returns [welcome]) │
│ │ │
│ │ [welcome_bytes] │
│ │ ─────────────────────────────>│
│ │ │
│ │ │ 10. join_group(welcome)
│ │ │ - Decrypt Welcome with
│ │ │ HPKE init private key
│ │ │ - Extract ratchet tree
│ │ │ from GroupInfo ext
│ │ │ - Initialise MlsGroup
│ │ │ at epoch 1
│ │ │
│ │ │ Bob is now a group member
│ │ │
```
### Key Points
- The **Commit** message is relevant for groups with more than two members. In
the two-party case, Alice is the sole existing member and merges the commit
herself. In a multi-member group, the Commit would be sent to all existing
members via the DS so they can advance their epoch.
- The **Welcome** message is encrypted to Bob's HPKE init key (derived from
the KeyPackage). Only the `GroupMember` instance that generated that
KeyPackage holds the corresponding private key.
- The `use_ratchet_tree_extension = true` MLS config embeds the full ratchet
tree in the Welcome's `GroupInfo` extension. This means Bob does not need a
separate tree fetch -- `new_from_welcome` extracts it automatically.
- The DS routes solely by `recipientKey` (Bob's Ed25519 public key). It does
not parse the Welcome, the Commit, or any MLS structure.
---
## 3. Message Exchange Flow
After both Alice and Bob are group members, they exchange MLS Application
messages through the DS.
### Sequence Diagram
```text
Alice NodeService (DS) Bob
───── ────────────────── ───
│ │ │
│ ─── Alice sends a message to Bob ─── │
│ │ │
│ 1. send_message("hello bob") │ │
│ MLS create_message(): │ │
│ - Derive message key from │ │
│ epoch secret + gen counter│ │
│ - Encrypt plaintext with │ │
│ AES-128-GCM │ │
│ - Produce MlsMessageOut │ │
│ (PrivateMessage variant) │ │
│ - TLS-encode to bytes │ │
│ │ │
│ 2. enqueue(bob_pk, ciphertext) │ │
│ ───────────────────────────────>│ │
│ │ 3. Store in bob's queue │
│ │ 4. Notify bob_pk waiters │
│ │ │
│ │ (time passes) │
│ │ │
│ │ 5. Bob polls for messages │
│ │ <─────────────────────────────│
│ │ fetchWait(bob_pk, 30000) │
│ │ │
│ │ 6. Drain bob's queue │
│ │ [ciphertext] │
│ │ ─────────────────────────────>│
│ │ │
│ │ │ 7. receive_message(ct)
│ │ │ MLS process_message():
│ │ │ - Identify sender from
│ │ │ PrivateMessage header
│ │ │ - Derive decryption key
│ │ │ from epoch secret
│ │ │ - Decrypt AES-128-GCM
│ │ │ - Return plaintext:
│ │ │ "hello bob"
│ │ │
│ ─── Bob replies to Alice ─── │
│ │ │
│ │ │ 8. send_message("hello alice")
│ │ │ (same MLS encrypt flow)
│ │ │
│ │ 9. enqueue(alice_pk, ct) │
│ │ <─────────────────────────────│
│ │ 10. Store + notify │
│ │ │
│ 11. fetch(alice_pk) │ │
│ ───────────────────────────────>│ │
│ [ciphertext] │ │
│ <───────────────────────────────│ │
│ │ │
│ 12. receive_message(ct) │ │
│ -> "hello alice" │ │
│ │ │
```
### Key Points
- **MLS provides forward secrecy**: each message is encrypted with a key
derived from the current epoch secret and a per-sender generation counter.
Compromising a future key does not reveal past messages.
- **The DS is a dumb relay**: it does not decrypt, inspect, or reorder
messages. It stores opaque byte blobs in a FIFO queue keyed by recipient.
- **Long-polling** via `fetchWait` avoids the need for persistent connections
or WebSocket-style push. The client specifies a timeout in milliseconds; the
server blocks up to that duration using `tokio::sync::Notify`. The `recv
--stream` CLI flag loops `fetchWait` indefinitely for continuous message
reception.
- **Channel-aware routing** is supported: the `channelId` field in `enqueue`
and `fetch` allows scoping queues by channel (e.g., a 16-byte UUID for
1:1 conversations). When `channelId` is empty, messages go to the default
(legacy) queue.
---
## Control-Plane vs. Data-Plane Summary
```text
┌─────────────────────────────────────────────────────────────────────┐
│ Control Plane (AS) │
│ │
│ uploadKeyPackage ────> Store KeyPackage for identity │
│ fetchKeyPackage <──── Pop and return one KeyPackage │
│ uploadHybridKey ────> Store hybrid PQ public key │
│ fetchHybridKey <──── Return hybrid PQ public key │
│ │
│ Traffic: Infrequent. Once per group join (upload before, │
│ fetch during group add). │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Data Plane (DS) │
│ │
│ enqueue ────> Append payload to recipient queue │
│ fetch <──── Drain and return all queued payloads │
│ fetchWait <──── Long-poll drain with timeout │
│ │
│ Traffic: High-frequency. Every MLS message (Welcome, Commit, │
│ Application) flows through the DS. │
└─────────────────────────────────────────────────────────────────────┘
```
The separation means the AS can be rate-limited or placed behind stricter
access controls without affecting message throughput on the DS.
---
## State Transitions
The following diagram summarises the client-side state machine across all three
flows:
```text
┌──────────────┐
│ No State │
└──────┬───────┘
IdentityKeypair::generate()
┌──────────────┐
│ Identity │ Ed25519 keypair exists
│ Generated │ No KeyPackage, no group
└──────┬───────┘
generate_key_package() + uploadKeyPackage()
┌──────────────┐
│ Registered │ KeyPackage on AS
│ │ HPKE init key in DiskKeyStore
└──────┬───────┘
┌──────────────┴──────────────┐
│ │
create_group() join_group(welcome)
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Group Owner │ │ Group Member │
│ (epoch 0) │ │ (epoch N) │
└──────┬──────┘ └──────┬───────┘
│ │
add_member() │
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ Active Group Member │
│ │
│ send_message() -> enqueue via DS │
│ receive_message() <- fetch from DS │
│ │
│ Epoch advances on each Commit │
└──────────────────────────────────────────┘
```
---
## Further Reading
- [Architecture Overview](overview.md) -- system diagram and two-service model
- [Service Architecture](service-architecture.md) -- RPC method details and long-polling internals
- [GroupMember Lifecycle](../internals/group-member-lifecycle.md) -- detailed MLS state machine
- [KeyPackage Exchange Flow](../internals/keypackage-exchange.md) -- single-use semantics and AS internals
- [MLS (RFC 9420)](../protocol-layers/mls.md) -- key schedule, ratchet tree, and ciphersuite details
- [Forward Secrecy](../cryptography/forward-secrecy.md) -- how MLS provides forward secrecy
- [Post-Compromise Security](../cryptography/post-compromise-security.md) -- group healing after key compromise

View File

@@ -0,0 +1,171 @@
# Architecture Overview
quicnprotochat is an end-to-end encrypted group messaging system built in Rust.
This page describes the high-level architecture: the services that compose the
system, the dual-key cryptographic model, and how the pieces fit together.
---
## Two-Service Model
The server exposes two logical services through a single **NodeService** RPC
interface, bound to **port 7000** over QUIC + TLS 1.3:
| Logical Service | Responsibility |
|--------------------------|-----------------------------------------------------------------|
| **Authentication Service (AS)** | Stores and distributes single-use MLS KeyPackages. Clients upload KeyPackages after identity generation; peers fetch them to add new members to a group. |
| **Delivery Service (DS)** | Store-and-forward relay for opaque payloads. The DS never inspects MLS ciphertext -- it routes solely by recipient Ed25519 public key (and optional channel ID). |
Combining both services into a single endpoint simplifies deployment and
reduces round-trips. The schema is defined in
[`schemas/node.capnp`](../wire-format/node-service-schema.md) as a unified
`NodeService` interface.
See [Service Architecture](service-architecture.md) for per-method details,
connection lifecycle, and the long-polling `fetchWait` mechanism.
---
## Dual-Key Model
quicnprotochat uses two independent asymmetric key pairs per client, each
serving a distinct role:
```text
quicnprotochat Key Model
┌──────────────────────────────────────────────────┐
│ │
│ X25519 static keypair (Noise transport) │
│ ───────────────────────────────────── │
│ - Generated once per node identity │
│ - Used in the Noise_XX handshake (M1 stack) │
│ - Provides mutual authentication + │
│ channel confidentiality at the TCP layer │
│ - Classical only (no PQ protection) │
│ - Managed by NoiseKeypair, zeroize-on-drop │
│ │
│ Ed25519 signing keypair (MLS identity) │
│ ────────────────────────────────────── │
│ - Generated once per user/device │
│ - Embedded in MLS BasicCredential │
│ - Signs KeyPackages, Commits, and group ops │
│ - Raw 32-byte public key is the AS index │
│ - Managed by IdentityKeypair, zeroize-on-drop │
│ │
└──────────────────────────────────────────────────┘
```
| Property | X25519 (Noise) | Ed25519 (MLS) |
|-------------------|-------------------------------------|--------------------------------------------|
| Curve | Curve25519 (Montgomery) | Ed25519 (Twisted Edwards) |
| Purpose | Transport authentication + secrecy | Identity binding, signing, MLS credentials |
| Crate | `x25519-dalek` | `ed25519-dalek` |
| Zeroize on drop | Yes (`StaticSecret`) | Yes (`Zeroizing<[u8; 32]>`) |
| PQ protection | None (classical X25519) | MLS key schedule uses DHKEM(X25519); hybrid PQ KEM available at envelope level |
For details on the cryptographic properties of each key type, see
[Ed25519 Identity Keys](../cryptography/identity-keys.md) and
[X25519 Transport Keys](../cryptography/transport-keys.md).
---
## System Diagram
```text
┌─────────────────┐ ┌─────────────────┐
│ Alice Client │ │ Bob Client │
│ │ │ │
│ IdentityKeypair │ │ IdentityKeypair │
│ (Ed25519) │ │ (Ed25519) │
│ │ │ │
│ GroupMember │ │ GroupMember │
│ (MLS state) │ │ (MLS state) │
│ │ │ │
│ NoiseKeypair* │ │ NoiseKeypair* │
│ (X25519, M1) │ │ (X25519, M1) │
└────────┬─────────┘ └────────┬─────────┘
│ │
│ QUIC + TLS 1.3 (quinn/rustls) │
│ ─── or ─── │
│ Noise_XX over TCP (snow, M1 stack) │
│ │
▼ ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ NodeService (port 7000) │
│ │
│ ┌──────────────────────────┐ ┌───────────────────────────────────┐ │
│ │ Authentication Service │ │ Delivery Service │ │
│ │ │ │ │ │
│ │ uploadKeyPackage() │ │ enqueue(recipientKey, payload) │ │
│ │ fetchKeyPackage() │ │ fetch(recipientKey) │ │
│ │ uploadHybridKey() │ │ fetchWait(recipientKey, timeout) │ │
│ │ fetchHybridKey() │ │ │ │
│ │ │ │ Queues: DashMap + FileBackedStore│ │
│ │ Store: DashMap + │ │ │ │
│ │ FileBackedStore │ │ │ │
│ └──────────────────────────┘ └───────────────────────────────────┘ │
│ │
│ health() │
│ │
└────────────────────────────────────────────────────────────────────────────┘
```
**Key observations:**
1. The server never sees plaintext message content. MLS ciphertext is opaque to
the DS -- it merely routes by `recipientKey`.
2. KeyPackages are single-use (RFC 9420 requirement). The AS atomically removes
a KeyPackage on fetch to enforce this invariant.
3. The QUIC + TLS 1.3 stack is the primary transport (M3+). The Noise_XX over
TCP stack from M1 remains available for environments where QUIC is blocked.
---
## Protocol Layering
The system stacks three protocol layers:
1. **Transport** -- QUIC + TLS 1.3 (primary) or Noise_XX over TCP (M1
fallback). Provides confidentiality, integrity, and server authentication.
See [Protocol Stack](protocol-stack.md).
2. **Framing / RPC** -- Cap'n Proto serialisation and RPC. Provides zero-copy
typed messages, schema versioning, and async method dispatch.
See [Cap'n Proto Serialisation and RPC](../protocol-layers/capn-proto.md).
3. **End-to-End Encryption** -- MLS (RFC 9420). Provides group key agreement,
forward secrecy, and post-compromise security. The server never holds group
keys.
See [MLS (RFC 9420)](../protocol-layers/mls.md).
An optional fourth layer -- the **hybrid KEM envelope** (X25519 + ML-KEM-768)
-- wraps MLS payloads for post-quantum confidentiality at the per-message level.
See [Hybrid KEM](../protocol-layers/hybrid-kem.md).
---
## Crate Map
The implementation is split across four workspace crates:
| Crate | Role |
|----------------------------|-------------------------------------------------------------------|
| `quicnprotochat-core` | Crypto primitives, Noise transport, MLS state machine, hybrid KEM |
| `quicnprotochat-proto` | Cap'n Proto schemas, codegen, and serialisation helpers |
| `quicnprotochat-server` | QUIC listener, NodeService RPC, storage |
| `quicnprotochat-client` | QUIC client, CLI subcommands, state persistence |
See [Crate Responsibilities](crate-responsibilities.md) for a full breakdown
and dependency diagram.
---
## Further Reading
- [Protocol Stack](protocol-stack.md) -- layered comparison of the two transport stacks
- [Service Architecture](service-architecture.md) -- NodeService RPC methods, connection lifecycle, long-polling
- [End-to-End Data Flow](data-flow.md) -- registration, group creation, and message exchange sequence diagrams
- [Wire Format Overview](../wire-format/overview.md) -- Cap'n Proto schema reference
- [Cryptography Overview](../cryptography/overview.md) -- detailed cryptographic properties and threat model

View File

@@ -0,0 +1,208 @@
# Protocol Stack
quicnprotochat layers three protocol stages to move a plaintext message from
sender to recipient with end-to-end encryption, typed RPC framing, and
authenticated transport. This page describes each layer, explains why both the
QUIC and Noise transport stacks exist, and provides a side-by-side comparison.
---
## Primary Stack (M3+): QUIC + TLS 1.3
Starting from milestone M3, the primary transport is QUIC over UDP with TLS 1.3
negotiated by `quinn` and `rustls`. Cap'n Proto RPC rides on a bidirectional
QUIC stream.
```text
┌─────────────────────────────────────────────┐
│ Application / MLS ciphertext │ <- group key ratchet (RFC 9420)
├─────────────────────────────────────────────┤
│ Cap'n Proto RPC │ <- typed, schema-versioned framing
├─────────────────────────────────────────────┤
│ QUIC + TLS 1.3 (quinn / rustls) │ <- mutual auth + transport secrecy
└─────────────────────────────────────────────┘
```
### What each layer provides
**QUIC + TLS 1.3** (`quinn`, `rustls`)
- Encrypted, authenticated transport with 0-RTT connection establishment
(where resumed).
- TLS 1.3 provides perfect forward secrecy per connection via ephemeral ECDHE.
- The server presents a self-signed certificate by default; the client pins
the server certificate via `--ca-cert`.
- ALPN protocol identifier: `capnp`.
- Multiplexed streams over a single UDP socket -- one bidirectional stream
per RPC session.
**Cap'n Proto RPC** (`capnp`, `capnp-rpc`)
- Zero-copy, schema-versioned serialisation.
- Asynchronous RPC with promise pipelining (multiple in-flight calls).
- The `NodeService` interface (defined in `schemas/node.capnp`) multiplexes
Authentication and Delivery operations on a single connection.
- The two-party VatNetwork runs over `tokio::io::compat` adapters wrapping
QUIC send/recv streams.
**MLS (RFC 9420)** (`openmls`, `openmls_rust_crypto`)
- Group key agreement with ratchet-tree-based key schedule.
- Forward secrecy: past messages remain confidential if a member's key is
compromised.
- Post-compromise security (PCS): the group heals after a compromise once an
honest update occurs.
- Identity binding: each member's Ed25519 public key is embedded in the MLS
`BasicCredential`.
- Ciphersuite: `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`.
---
## M1 Stack: Noise_XX over TCP
The original milestone-1 transport uses a Noise Protocol Framework handshake
directly over TCP. This stack is retained for environments where QUIC (UDP) is
blocked by middleboxes.
```text
TCP connection
└── Noise_XX handshake (snow)
└── Authenticated encrypted channel (ChaCha20-Poly1305)
└── [u32 frame_len LE][Cap'n Proto encoded message]
└── Cap'n Proto RPC (capnp-rpc)
```
### Layer details
**TCP**
- Reliable, ordered byte stream.
- No built-in encryption or authentication.
**Noise_XX** (`snow`)
- Pattern: `Noise_XX_25519_ChaChaPoly_BLAKE2s`.
- Three-message handshake that mutually authenticates both peers' static
X25519 keys:
```text
XX handshake (3 messages):
-> e (initiator sends ephemeral public key)
<- e, ee, s, es (responder: DH + static key)
-> s, se (initiator: static key + final DH)
```
- After the handshake, every frame is encrypted with ChaCha20-Poly1305 (AEAD)
using session keys derived from the Noise key schedule.
- Maximum Noise message size: 65,535 bytes.
**Length-Prefixed Codec** (`LengthPrefixedCodec` in `quicnprotochat-core`)
- Each frame is prefixed by a 4-byte little-endian `u32` length field.
- Little-endian was chosen for consistency with Cap'n Proto's segment table
encoding.
- Wire format:
```text
┌──────────────────────────┬──────────────────────────────────────┐
│ length (4 bytes, LE u32)│ payload (length bytes) │
└──────────────────────────┴──────────────────────────────────────┘
```
- Maximum payload size is `NOISE_MAX_MSG` (65,535 bytes), enforced on both
encode and decode.
- See [Length-Prefixed Framing Codec](../wire-format/framing-codec.md) for the
full specification.
**Cap'n Proto RPC**
- Same schema and RPC interface as the QUIC stack.
- The `NoiseTransport::into_capnp_io()` method bridges the message-oriented
Noise channel to the byte-stream interface that `capnp-rpc`'s
`twoparty::VatNetwork` expects, using a `tokio::io::duplex` pipe and a
background shuttle task.
---
## Why Both Stacks Exist
| Concern | QUIC + TLS 1.3 | Noise_XX over TCP |
|------------------------|----------------------------------------|----------------------------------------|
| **Milestone** | M3+ (primary) | M1 (original, retained) |
| **UDP availability** | Requires UDP; may be blocked on some networks | TCP-only; works everywhere |
| **Connection setup** | 1-RTT (or 0-RTT on resumption) | 1-RTT TCP + 1.5-RTT Noise handshake |
| **Multiplexing** | Native QUIC stream multiplexing | Single TCP connection, single stream |
| **Authentication** | Server cert (self-signed / CA-issued) | Mutual static-key authentication |
| **PQ gap** | TLS 1.3 key exchange is classical ECDHE | Noise key exchange is classical X25519 |
| **Crate** | `quinn`, `rustls` | `snow` |
Both stacks carry the same Cap'n Proto RPC and MLS layers on top, so
application logic is transport-agnostic. The Noise_XX stack may also serve as a
peer-to-peer transport in future mesh topologies where a QUIC server
certificate model does not apply.
---
## Comparison Table
| Layer | Provides | Crate(s) |
|-------------|------------------------------------------------------------------|-----------------------------------------|
| **Transport: QUIC + TLS 1.3** | Confidentiality, server authentication, forward secrecy, multiplexed streams, congestion control | `quinn`, `rustls` |
| **Transport: Noise_XX** | Confidentiality, mutual authentication, forward secrecy (per-session) | `snow` |
| **Framing: Cap'n Proto** | Zero-copy typed serialisation, schema versioning, async RPC with promise pipelining | `capnp`, `capnp-rpc` |
| **Encryption: MLS** | Group key agreement, forward secrecy, post-compromise security, identity binding | `openmls`, `openmls_rust_crypto` |
| **Encryption: Hybrid KEM** (optional) | Post-quantum confidentiality for individual payloads (X25519 + ML-KEM-768) | `ml-kem`, `x25519-dalek`, `chacha20poly1305`, `hkdf` |
---
## Data Path Summary
A plaintext message traverses the stack as follows:
```text
Sender Recipient
────── ─────────
plaintext bytes
MLS create_message()
│ ── encrypts with group AEAD key (AES-128-GCM) ──
TLS-encoded MlsMessageOut (opaque ciphertext blob)
Cap'n Proto: enqueue(recipientKey, payload)
│ ── serialised into NodeService RPC call ──
QUIC stream (TLS 1.3 encrypted) ─── or ─── Noise frame (ChaCha20-Poly1305)
│ │
▼ ▼
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ network ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
│ │
▼ ▼
Server: NodeService.enqueue() stores payload in FIFO queue
Cap'n Proto: fetch() / fetchWait() returns payload
MLS process_message()
│ ── decrypts with group AEAD key ──
plaintext bytes
```
The server **never** holds the MLS group key. It sees only the encrypted
`MlsMessageOut` blob.
---
## Further Reading
- [Architecture Overview](overview.md) -- high-level system diagram and dual-key model
- [Noise_XX Handshake](../protocol-layers/noise-xx.md) -- deep dive into the three-message handshake
- [QUIC + TLS 1.3](../protocol-layers/quic-tls.md) -- QUIC configuration, ALPN, and certificate handling
- [Cap'n Proto Serialisation and RPC](../protocol-layers/capn-proto.md) -- schema design and VatNetwork wiring
- [MLS (RFC 9420)](../protocol-layers/mls.md) -- ciphersuite selection, key schedule, and ratchet tree
- [Hybrid KEM: X25519 + ML-KEM-768](../protocol-layers/hybrid-kem.md) -- post-quantum envelope encryption

View File

@@ -0,0 +1,259 @@
# Service Architecture
The quicnprotochat server exposes a single **NodeService** RPC endpoint that
combines Authentication and Delivery operations. This page documents the RPC
interface, per-connection lifecycle, storage model, long-polling mechanism, and
authentication context.
---
## NodeService Endpoint
A single QUIC + TLS 1.3 listener on **port 7000** serves all operations.
The schema is defined in `schemas/node.capnp` and documented in
[NodeService Schema](../wire-format/node-service-schema.md).
```text
NodeService (port 7000)
├── Authentication methods
│ ├── uploadKeyPackage(identityKey, package, auth) -> fingerprint
│ ├── fetchKeyPackage(identityKey, auth) -> package
│ ├── uploadHybridKey(identityKey, hybridPublicKey) -> ()
│ └── fetchHybridKey(identityKey) -> hybridPublicKey
├── Delivery methods
│ ├── enqueue(recipientKey, payload, channelId, version, auth) -> ()
│ ├── fetch(recipientKey, channelId, version, auth) -> payloads
│ └── fetchWait(recipientKey, channelId, version, timeoutMs, auth) -> payloads
└── Operational
└── health() -> status
```
---
## RPC Method Reference
### Authentication Service Methods
| Method | Params | Returns | Semantics |
|----------------------|-------------------------------------|------------------|-----------|
| `uploadKeyPackage` | `identityKey` (32 B Ed25519 pk), `package` (TLS-encoded KeyPackage), `auth` | `fingerprint` (SHA-256 of package) | Appends the KeyPackage to a per-identity FIFO queue. The fingerprint lets the client detect server-side tampering. Max package size: 1 MB. |
| `fetchKeyPackage` | `identityKey` (32 B), `auth` | `package` (or empty `Data`) | Atomically pops and returns the oldest KeyPackage for the identity. Returns empty bytes if none are stored. Single-use semantics per RFC 9420. |
| `uploadHybridKey` | `identityKey` (32 B), `hybridPublicKey` (X25519 pk + ML-KEM-768 ek) | `()` | Stores (or replaces) the hybrid PQ public key for envelope-level post-quantum encryption. |
| `fetchHybridKey` | `identityKey` (32 B) | `hybridPublicKey` (or empty `Data`) | Returns the stored hybrid public key for a peer, or empty if none. |
### Delivery Service Methods
| Method | Params | Returns | Semantics |
|--------------|------------------------------------------------------------------------|----------------------|-----------|
| `enqueue` | `recipientKey` (32 B), `payload` (opaque), `channelId`, `version`, `auth` | `()` | Appends `payload` to the recipient's FIFO queue. Max payload: 5 MB. Wakes any `fetchWait` waiter for this recipient. Supported versions: 0 (legacy), 1 (current). |
| `fetch` | `recipientKey` (32 B), `channelId`, `version`, `auth` | `payloads: List(Data)` | Atomically drains and returns the full queue in FIFO order. Returns empty list if nothing is pending. |
| `fetchWait` | `recipientKey` (32 B), `channelId`, `version`, `timeoutMs`, `auth` | `payloads: List(Data)` | Same as `fetch`, but if the queue is empty and `timeoutMs > 0`, blocks up to `timeoutMs` milliseconds waiting for a `Notify` signal from `enqueue`. Returns whatever is in the queue when the wait completes or times out. |
### Operational Methods
| Method | Params | Returns | Semantics |
|----------|--------|-----------------|-----------|
| `health` | none | `status: Text` | Returns `"ok"`. Used for liveness/readiness probes. |
---
## Per-Connection Lifecycle
Each incoming QUIC connection follows this sequence:
```text
┌──────────────────────────────────────────────────────────────────────┐
│ Client Server │
│ │
│ 1. UDP packet -> │
│ QUIC INITIAL │
│ │
│ 2. <- QUIC HANDSHAKE │
│ TLS 1.3 ServerHello + │
│ Certificate (self-signed) │
│ ALPN: "capnp" │
│ │
│ 3. Client verifies server │
│ cert against pinned CA │
│ cert (--ca-cert flag) │
│ │
│ 4. QUIC connection established │
│ │
│ 5. Client opens bidirectional ──────────> Server accepts bi stream │
│ QUIC stream (open_bi) (accept_bi) │
│ │
│ 6. tokio_util::compat adapters wrap the send/recv halves │
│ into AsyncRead + AsyncWrite │
│ │
│ 7. capnp-rpc twoparty::VatNetwork │
│ Client Side::Client Server Side::Server │
│ │
│ 8. RpcSystem::new() starts │
│ promise-pipelined RPC loop │
│ │
│ 9. Client bootstraps │
│ node_service::Client NodeServiceImpl created │
│ (shares Arc<FileBackedStore>, │
│ Arc<DashMap<..., Notify>>) │
│ │
│ 10. RPC calls flow over the bidirectional stream │
│ until either side closes the connection. │
└──────────────────────────────────────────────────────────────────────┘
```
### LocalSet requirement
`capnp-rpc` uses `Rc<RefCell<>>` internally, making it `!Send`. Therefore:
- The server runs the entire accept loop inside a `tokio::task::LocalSet`.
- Each connection handler is `spawn_local`, ensuring all RPC futures stay on a
single thread.
- The client wraps each subcommand invocation in its own `LocalSet::run_until`.
This is a fundamental constraint of the Cap'n Proto RPC runtime in Rust.
Attempts to spawn RPC futures on the multi-threaded Tokio executor will fail
with a compile error.
---
## Storage Model
`NodeServiceImpl` holds two pieces of shared state:
### FileBackedStore
```text
FileBackedStore
├── key_packages: Mutex<HashMap<Vec<u8>, VecDeque<Vec<u8>>>>
│ Key: Ed25519 public key (32 bytes)
│ Value: FIFO queue of TLS-encoded KeyPackage blobs
│ File: data/keypackages.bin (bincode)
├── deliveries: Mutex<HashMap<ChannelKey, VecDeque<Vec<u8>>>>
│ ChannelKey: { channel_id: Vec<u8>, recipient_key: Vec<u8> }
│ Value: FIFO queue of opaque payload blobs
│ File: data/deliveries.bin (bincode, v2 format)
└── hybrid_keys: Mutex<HashMap<Vec<u8>, Vec<u8>>>
Key: Ed25519 public key (32 bytes)
Value: serialised HybridPublicKey blob
File: data/hybridkeys.bin (bincode)
```
Every mutation (upload, fetch, enqueue) acquires the relevant `Mutex`, modifies
the in-memory `HashMap`, and then flushes the entire map to disk as a bincode
blob. This is intentionally simple for MVP-scale workloads. A production
deployment would replace this with an embedded database or external store.
The delivery map supports a **v1 -> v2 upgrade path**: if `deliveries.bin`
contains the legacy `QueueMapV1` format (keyed by `recipientKey` only), the
store transparently upgrades entries by wrapping them in `ChannelKey` with an
empty `channel_id`.
### DashMap Waiters
```text
Arc<DashMap<Vec<u8>, Arc<Notify>>>
Key: recipient Ed25519 public key (32 bytes)
Value: tokio::sync::Notify instance
```
The waiters map is orthogonal to `FileBackedStore`. It lives entirely in
memory and serves the `fetchWait` long-polling mechanism:
1. `enqueue` calls `waiter(&recipient_key).notify_waiters()` after storing the
payload.
2. `fetchWait` first tries a regular `fetch`. If the queue is empty and
`timeoutMs > 0`:
- Look up or insert a `Notify` for the recipient.
- `tokio::time::timeout(Duration::from_millis(timeoutMs), notify.notified())`
- When notified (or on timeout), perform a second `fetch` and return
whatever is available.
This design avoids busy-polling while keeping the implementation lock-free
(DashMap uses sharded RwLocks internally).
---
## Auth Struct
Every RPC method that modifies or reads user-specific state accepts an `Auth`
parameter:
```capnp
struct Auth {
version @0 :UInt16; # 0 = legacy/none, 1 = token-based auth
accessToken @1 :Data; # opaque bearer token
deviceId @2 :Data; # optional UUID for auditing/rate limiting
}
```
### Version semantics
| Version | Meaning |
|---------|------------------------------------------------------------|
| 0 | Legacy / no authentication. The server accepts the request without checking credentials. Suitable for development and testing. |
| 1 | Token-based authentication. The `accessToken` field should contain an opaque bearer token issued at login. The server validates the token against a token store (not yet implemented -- see [Auth, Devices, and Tokens](../roadmap/authz-plan.md)). |
The server validates the `version` field on every request via `validate_auth()`.
Requests with unsupported versions are rejected with a Cap'n Proto error.
### Client-side usage
The client CLI accepts `--access-token` and `--device-id` flags (or the
corresponding environment variables). These are bundled into a `ClientAuth`
struct and injected into every outgoing RPC call via the `set_auth()` helper.
Currently, the client sends `version = 0` with empty token and device ID by
default. When the token-based auth flow is implemented, the client will populate
these fields.
---
## Validation and Limits
The server enforces the following constraints on every RPC call:
| Constraint | Value | Error on violation |
|-----------------------------|--------------------|--------------------|
| `identityKey` / `recipientKey` length | Exactly 32 bytes | Cap'n Proto error: "must be exactly 32 bytes" |
| KeyPackage size | <= 1 MB | Cap'n Proto error: "package exceeds max size" |
| Payload size | <= 5 MB | Cap'n Proto error: "payload exceeds max size" |
| Wire version | 0 or 1 | Cap'n Proto error: "unsupported wire version" |
| Auth version | 0 or 1 | Cap'n Proto error: "unsupported auth version" |
| KeyPackage non-empty | `package.len() > 0`| Cap'n Proto error: "package must not be empty" |
| Payload non-empty | `payload.len() > 0`| Cap'n Proto error: "payload must not be empty" |
---
## Configuration
The server binary is configured via CLI flags or environment variables:
| Flag | Env var | Default | Description |
|----------------|----------------------------|----------------------|-------------|
| `--listen` | `QUICNPROTOCHAT_LISTEN` | `0.0.0.0:7000` | QUIC listen address (host:port). |
| `--data-dir` | `QUICNPROTOCHAT_DATA_DIR` | `data` | Directory for persisted KeyPackages, delivery queues, and hybrid keys. |
| `--tls-cert` | `QUICNPROTOCHAT_TLS_CERT` | `data/server-cert.der` | Path to TLS certificate (DER). Auto-generated if missing. |
| `--tls-key` | `QUICNPROTOCHAT_TLS_KEY` | `data/server-key.der` | Path to TLS private key (DER). Auto-generated if missing. |
If the TLS certificate or key files do not exist at startup, the server
auto-generates a self-signed certificate for `localhost`, `127.0.0.1`, and
`::1` using `rcgen`.
Logging level is controlled by the `RUST_LOG` environment variable (default:
`info`).
---
## Further Reading
- [Architecture Overview](overview.md) -- two-service model and dual-key overview
- [NodeService Schema](../wire-format/node-service-schema.md) -- full Cap'n Proto schema
- [End-to-End Data Flow](data-flow.md) -- sequence diagrams showing registration, group creation, and messaging
- [Delivery Service Internals](../internals/delivery-service.md) -- queue routing and channel-aware delivery
- [Authentication Service Internals](../internals/authentication-service.md) -- KeyPackage lifecycle
- [Storage Backend](../internals/storage-backend.md) -- FileBackedStore details and upgrade path
- [Auth, Devices, and Tokens](../roadmap/authz-plan.md) -- planned token-based authentication