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,330 @@
# Demo Walkthrough: Alice and Bob
This page walks through a complete end-to-end encrypted conversation between two participants -- Alice and Bob -- using the persistent group CLI. By the end, you will have started a server, registered two identities, created an MLS group, exchanged a Welcome, and sent encrypted messages in both directions.
You will need **three terminal windows**: one for the server, one for Alice, and one for Bob.
---
## Overview
```
┌─────────┐ ┌──────────────────┐ ┌─────────┐
│ Alice │ │ Server │ │ Bob │
│ (client) │──── QUIC ────│ AS + DS (:7000) │──── QUIC ────│ (client)│
└─────────┘ └──────────────────┘ └─────────┘
```
---
## Sequence diagram
```
Alice Server (AS+DS) Bob
│ │ │
│ 1. register-state │ │
│ ─── uploadKeyPackage ─────> │ │
│ <── fingerprint ─────────── │ │
│ │ │
│ │ 2. register-state │
│ │ <── uploadKeyPackage ───────── │
│ │ ─── fingerprint ────────────> │
│ │ │
│ 3. create-group │ │
│ (local: epoch 0) │ │
│ │ │
│ 4. invite --peer-key <bob> │ │
│ ─── fetchKeyPackage ──────> │ (Bob's KP removed from AS) │
│ <── package ────────────── │ │
│ (local: add_member → Commit + Welcome) │
│ ─── enqueue(Welcome) ─────> │ (queued for Bob) │
│ │ │
│ │ 5. join │
│ │ <── fetch ──────────────────── │
│ │ ─── Welcome ────────────────> │
│ │ (local: new_from_welcome) │
│ │ │
│ 6. send --msg "Hi Bob" │ │
│ (local: create_message → PrivateMessage) │
│ ─── enqueue(ciphertext) ──> │ (queued for Bob) │
│ │ │
│ │ 7. recv │
│ │ <── fetch ──────────────────── │
│ │ ─── ciphertext ─────────────> │
│ │ (local: process_message) │
│ │ plaintext: "Hi Bob" │
│ │ │
│ │ 8. send --msg "Hi Alice" │
│ │ (local: create_message) │
│ │ <── enqueue(ciphertext) ────── │
│ │ │
│ 9. recv │ │
│ ─── fetch ────────────────> │ │
│ <── ciphertext ─────────── │ │
│ (local: process_message) │ │
│ plaintext: "Hi Alice" │ │
│ │ │
```
---
## Step-by-step instructions
### Step 1: Start the server
In **Terminal 1** (Server):
```bash
cargo run -p quicnprotochat-server
```
Wait for the log line confirming it is accepting connections:
```
INFO quicnprotochat_server: accepting QUIC connections addr="0.0.0.0:7000"
```
If this is the first run, you will also see a log line about generating the self-signed TLS certificate. The certificate is written to `data/server-cert.der`, which the client will use for TLS verification.
### Step 2: Alice registers her identity
In **Terminal 2** (Alice):
```bash
cargo run -p quicnprotochat-client -- register-state \
--state alice.bin \
--server 127.0.0.1:7000
```
This command:
- Generates a fresh Ed25519 identity keypair (or loads one from `alice.bin` if it already exists).
- Creates an MLS KeyPackage signed with that identity.
- Uploads the KeyPackage to the server's Authentication Service.
- Saves the identity seed and key store to `alice.bin` and `alice.ks`.
**Output:**
```
identity_key : <ALICE_KEY> (64 hex chars)
fingerprint : <fingerprint>
KeyPackage uploaded successfully.
```
**Copy the `identity_key` value** -- Bob will need it in Step 5.
### Step 3: Bob registers his identity
In **Terminal 3** (Bob):
```bash
cargo run -p quicnprotochat-client -- register-state \
--state bob.bin \
--server 127.0.0.1:7000
```
**Output:**
```
identity_key : <BOB_KEY> (64 hex chars)
fingerprint : <fingerprint>
KeyPackage uploaded successfully.
```
**Copy the `identity_key` value** -- Alice will need it in Step 4.
### Step 4: Alice creates a group and invites Bob
In **Terminal 2** (Alice):
First, create the group:
```bash
cargo run -p quicnprotochat-client -- create-group \
--state alice.bin \
--group-id "demo-chat"
```
```
group created: demo-chat
```
Alice is now the sole member of the group at epoch 0.
Next, invite Bob using his identity key from Step 3:
```bash
cargo run -p quicnprotochat-client -- invite \
--state alice.bin \
--peer-key <BOB_KEY> \
--server 127.0.0.1:7000
```
This command:
1. Fetches Bob's KeyPackage from the AS (this atomically removes it -- single-use).
2. Calls `add_member()` on Alice's local MLS group, producing a Commit (applied locally) and a Welcome.
3. Enqueues the Welcome message to the DS, addressed to Bob's identity key.
```
invited peer (welcome queued)
```
Alice's group state has now advanced to epoch 1.
### Step 5: Bob joins the group
In **Terminal 3** (Bob):
```bash
cargo run -p quicnprotochat-client -- join \
--state bob.bin \
--server 127.0.0.1:7000
```
This command:
1. Fetches all pending messages for Bob's identity key from the DS.
2. Finds the Welcome message that Alice enqueued.
3. Calls `MlsGroup::new_from_welcome()`, which decrypts the Welcome using the HPKE init private key from Bob's key store (`bob.ks`).
4. Saves the joined group state to `bob.bin`.
```
joined group successfully
```
Bob is now a member of the group at epoch 1, sharing the same group secret as Alice.
### Step 6: Alice sends an encrypted message
In **Terminal 2** (Alice):
```bash
cargo run -p quicnprotochat-client -- send \
--state alice.bin \
--peer-key <BOB_KEY> \
--msg "Hello Bob, this is encrypted with MLS!" \
--server 127.0.0.1:7000
```
This command:
1. Calls `create_message()` on Alice's MLS group, encrypting the plaintext as an MLS `PrivateMessage`.
2. Enqueues the ciphertext to the DS for Bob's identity key.
```
message sent
```
### Step 7: Bob receives and decrypts
In **Terminal 3** (Bob):
```bash
cargo run -p quicnprotochat-client -- recv \
--state bob.bin \
--server 127.0.0.1:7000
```
This command:
1. Fetches all pending messages from the DS.
2. For each message, calls `process_message()` on Bob's MLS group, which decrypts the `PrivateMessage` and returns the plaintext.
```
[0] plaintext: Hello Bob, this is encrypted with MLS!
```
### Step 8: Bob replies
In **Terminal 3** (Bob):
```bash
cargo run -p quicnprotochat-client -- send \
--state bob.bin \
--peer-key <ALICE_KEY> \
--msg "Hi Alice, received loud and clear!" \
--server 127.0.0.1:7000
```
```
message sent
```
### Step 9: Alice receives Bob's reply
In **Terminal 2** (Alice):
```bash
cargo run -p quicnprotochat-client -- recv \
--state alice.bin \
--server 127.0.0.1:7000
```
```
[0] plaintext: Hi Alice, received loud and clear!
```
---
## Automated demo (single command)
If you want to see the entire flow in a single command without managing three terminals, use the `demo-group` subcommand. This creates both Alice and Bob in-process with ephemeral identities and runs the full round-trip:
```bash
# Ensure the server is running, then:
cargo run -p quicnprotochat-client -- demo-group --server 127.0.0.1:7000
```
```
Alice -> Bob plaintext: hello bob
Bob -> Alice plaintext: hello alice
demo-group complete
```
---
## What happened under the hood
Here is a summary of the cryptographic operations and network calls that occurred during this walkthrough:
| Step | Client | Crypto operation | Network RPC |
|---|---|---|---|
| 2 | Alice | Ed25519 keygen, MLS KeyPackage creation | `uploadKeyPackage` |
| 3 | Bob | Ed25519 keygen, MLS KeyPackage creation | `uploadKeyPackage` |
| 4a | Alice | `MlsGroup::new_with_group_id` (epoch 0) | -- |
| 4b | Alice | `MlsGroup::add_members` (Commit + Welcome, epoch 0 -> 1) | `fetchKeyPackage`, `enqueue` |
| 5 | Bob | `MlsGroup::new_from_welcome` (HPKE decrypt, epoch 1) | `fetch` |
| 6 | Alice | `MlsGroup::create_message` (AES-128-GCM encrypt) | `enqueue` |
| 7 | Bob | `MlsGroup::process_message` (AES-128-GCM decrypt) | `fetch` |
| 8 | Bob | `MlsGroup::create_message` (AES-128-GCM encrypt) | `enqueue` |
| 9 | Alice | `MlsGroup::process_message` (AES-128-GCM decrypt) | `fetch` |
The MLS ciphersuite used throughout is `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`:
- **DHKEM(X25519, HKDF-SHA256)** for the HPKE key encapsulation in KeyPackages
- **AES-128-GCM** for symmetric encryption of application messages
- **SHA-256** for the key schedule hash function
- **Ed25519** for signing KeyPackages, Commits, and leaf nodes
---
## Troubleshooting
### `join` fails with "HPKE init key not found"
This happens when the key store file (`.ks`) was deleted or when `join` is run with a different `--state` path than `register-state`. The HPKE init private key generated during KeyPackage creation must be available at join time. Solution: use the same `--state` path for both `register-state` and `join`, and do not delete the `.ks` file between them.
### `invite` fails with "server returned empty KeyPackage for peer"
The peer has not registered yet, or their KeyPackage was already consumed by a previous `invite` or `fetch-key`. Ask the peer to run `register-state` again to upload a fresh KeyPackage.
### `join` fails with "no Welcome found in DS for this identity"
The Welcome message has not been enqueued yet (the inviter has not run `invite`), or it was already consumed by a previous `join`. Check that `invite` completed successfully before running `join`.
### TLS verification fails
Ensure the client has access to the server's TLS certificate. By default, both server and client use `data/server-cert.der`. If the server regenerated its certificate (e.g., after deleting the `data/` directory), clients must pick up the new certificate.
---
## Next steps
- [Running the Client](running-the-client.md) -- full CLI reference
- [MLS (RFC 9420)](../protocol-layers/mls.md) -- how the MLS group operations work
- [GroupMember Lifecycle](../internals/group-member-lifecycle.md) -- internal state machine details
- [Delivery Service Internals](../internals/delivery-service.md) -- how the DS queues and delivers messages