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,139 @@
# Building from Source
This page covers compiling the workspace, running the test suite, and understanding the build-time Cap'n Proto code generation step.
---
## Building the workspace
From the repository root:
```bash
cargo build --workspace
```
This compiles all four crates:
| Crate | Type | Purpose |
|---|---|---|
| `quicnprotochat-core` | library | Crypto primitives, Noise transport, MLS `GroupMember` state machine, frame codec |
| `quicnprotochat-proto` | library | Cap'n Proto schemas, generated types, envelope serialisation helpers |
| `quicnprotochat-server` | binary | Unified Authentication + Delivery Service (`NodeService`) |
| `quicnprotochat-client` | binary | CLI client with subcommands (`ping`, `register`, `send`, `recv`, etc.) |
For a release build with LTO, symbol stripping, and single codegen unit:
```bash
cargo build --workspace --release
```
The release profile is configured in the workspace `Cargo.toml`:
```toml
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
strip = "symbols"
```
---
## Running the test suite
```bash
cargo test --workspace
```
The test suite includes:
- **`quicnprotochat-proto`**: Round-trip serialisation tests for Cap'n Proto `Envelope` messages (Ping, Pong, corrupted-input error handling).
- **`quicnprotochat-core`**: Two-party MLS round-trip (`create_group` / `add_member` / `send_message` / `receive_message`), group ID lifecycle assertions.
- **`quicnprotochat-client`**: Integration tests for MLS group operations and auth service interactions (require a running server or use in-process mocks).
To run tests for a single crate:
```bash
cargo test -p quicnprotochat-core
```
---
## Cap'n Proto code generation
The `quicnprotochat-proto` crate does not contain hand-written Rust types for wire messages. Instead, its `build.rs` script invokes the `capnp` compiler at build time to generate Rust source from the `.capnp` schema files.
### How it works
1. `build.rs` locates the workspace-root `schemas/` directory (two levels above `crates/quicnprotochat-proto/`).
2. It invokes `capnpc::CompilerCommand` on all four schema files:
- `schemas/envelope.capnp` -- top-level wire envelope with `MsgType` discriminant
- `schemas/auth.capnp` -- `AuthenticationService` RPC interface
- `schemas/delivery.capnp` -- `DeliveryService` RPC interface
- `schemas/node.capnp` -- `NodeService` RPC interface (unified AS + DS)
3. The generated Rust source is written to `$OUT_DIR` (Cargo's build output directory).
4. `src/lib.rs` includes the generated code via `include!(concat!(env!("OUT_DIR"), "/envelope_capnp.rs"))` and similar macros for each schema.
### Rebuild triggers
The `build.rs` script emits `cargo:rerun-if-changed` directives for each schema file. If you modify a `.capnp` file, the next `cargo build` will automatically re-run code generation.
### Schema include path
The `src_prefix` is set to the `schemas/` directory so that inter-schema imports (e.g., `using Auth = import "auth.capnp".Auth;` inside `node.capnp`) resolve correctly.
### Design constraints of quicnprotochat-proto
The proto crate is intentionally restricted:
- **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.
For details on the wire format, see the [Wire Format Reference](../wire-format/overview.md).
---
## Troubleshooting
### `capnp` binary not found
**Symptom:**
```
Cap'n Proto schema compilation failed.
Is `capnp` installed? (apt-get install capnproto / brew install capnp)
```
**Fix:** Install the Cap'n Proto compiler for your platform. See [Prerequisites](prerequisites.md) for platform-specific instructions.
Verify it is on your `PATH`:
```bash
which capnp
capnp --version
```
### Version mismatch between `capnp` CLI and `capnpc` Rust crate
The workspace uses `capnpc = "0.19"` (the Rust bindings for the Cap'n Proto compiler). If your system `capnp` binary is significantly older or newer, generated code may be incompatible. The recommended approach is to use a `capnp` binary whose major version matches the `capnpc` crate version. On most systems, the package manager version is compatible.
### linker errors on macOS with Apple Silicon
If you see linker errors related to `ring` or `aws-lc-sys` (used transitively by `rustls`), ensure you have Xcode Command Line Tools installed:
```bash
xcode-select --install
```
### Slow first build
The first build downloads and compiles all dependencies (including `openmls`, `quinn`, `rustls`, `capnp-rpc`, etc.). This can take several minutes depending on your hardware. Subsequent builds are incremental and much faster.
---
## Next steps
- [Running the Server](running-the-server.md) -- start the NodeService endpoint
- [Running the Client](running-the-client.md) -- CLI subcommands and usage examples
- [Docker Deployment](docker.md) -- build and run in containers

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

View File

@@ -0,0 +1,196 @@
# Docker Deployment
quicnprotochat includes a multi-stage Dockerfile and a Docker Compose configuration for building and running the server in containers.
---
## Quick start
```bash
docker compose up
```
This builds the server image (if not already built) and starts a single `server` service listening on port `7000`. The server will generate a self-signed TLS certificate on first launch and begin accepting QUIC connections.
To rebuild after code changes:
```bash
docker compose up --build
```
To run in the background:
```bash
docker compose up -d
```
---
## Docker Compose configuration
The `docker-compose.yml` at the repository root defines a single service:
```yaml
services:
server:
build:
context: .
dockerfile: docker/Dockerfile
ports:
- "7000:7000"
environment:
RUST_LOG: "info"
QUICNPROTOCHAT_LISTEN: "0.0.0.0:7000"
healthcheck:
test: ["CMD", "bash", "-c", "echo '' > /dev/tcp/localhost/7000"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
restart: unless-stopped
```
### Port mapping
The container exposes port `7000` (QUIC/UDP). The `ports` directive maps host port `7000` to the container's `7000`. Note that QUIC uses UDP, so ensure your firewall allows UDP traffic on this port.
### Health check
The health check uses a TCP connection probe (`/dev/tcp/localhost/7000`). While QUIC is a UDP protocol, the TCP probe verifies that the process is running and the port is bound. A QUIC-aware health check (e.g., using the client's `ping` command) would be more precise but requires the client binary in the runtime image.
### Restart policy
`restart: unless-stopped` ensures the server restarts automatically after crashes but stays stopped if you explicitly `docker compose stop` or `docker compose down`.
---
## Multi-stage Docker build
The Dockerfile at `docker/Dockerfile` uses a two-stage build to produce a minimal runtime image.
### Stage 1: Builder (`rust:bookworm`)
```dockerfile
FROM rust:bookworm AS builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends capnproto \
&& rm -rf /var/lib/apt/lists/*
```
Key steps:
1. **Base image**: `rust:bookworm` (Debian Bookworm with the Rust toolchain pre-installed).
2. **Install `capnproto`**: Required by `quicnprotochat-proto/build.rs` to compile `.capnp` schemas at build time.
3. **Copy manifests first**: `Cargo.toml` and `Cargo.lock` are copied before source code. Dummy `main.rs` / `lib.rs` stubs are created so that `cargo build` can resolve and cache the dependency graph. This ensures that dependency compilation is cached in a separate Docker layer -- subsequent builds that only change source code skip the dependency compilation step entirely.
4. **Copy schemas**: The `schemas/` directory is copied before the dependency build because `quicnprotochat-proto/build.rs` requires the `.capnp` files during compilation.
5. **Copy real source and build**: After the dependency cache layer, real source files are copied in and `cargo build --release` is run.
### Stage 2: Runtime (`debian:bookworm-slim`)
```dockerfile
FROM debian:bookworm-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/target/release/quicnprotochat-server /usr/local/bin/quicnprotochat-server
EXPOSE 7000
ENV RUST_LOG=info \
QUICNPROTOCHAT_LISTEN=0.0.0.0:7000
USER nobody
CMD ["quicnprotochat-server"]
```
Key characteristics:
- **Minimal image**: No Rust toolchain, no `capnp` compiler, no build artifacts.
- **`ca-certificates`**: Included for future HTTPS calls (e.g., ACME certificate provisioning or key sync endpoints).
- **Non-root execution**: The container runs as `nobody` for defense in depth.
- **Default port**: The Dockerfile defaults to port `7000` via `QUICNPROTOCHAT_LISTEN`, but the `docker-compose.yml` overrides this to `7000` for consistency with the development workflow.
> **Note**: The `EXPOSE 7000` directive in the Dockerfile and the `QUICNPROTOCHAT_LISTEN=0.0.0.0:7000` override in `docker-compose.yml` mean the effective listen port is `7000` when using Compose. If you run the Docker image directly without Compose, the server will listen on `7000` by default.
---
## Volume persistence
The server stores its state (TLS certificates, KeyPackages, delivery queues, hybrid keys) in the data directory (default `data/`). To persist this data across container restarts, mount a volume:
```yaml
services:
server:
# ... existing config ...
volumes:
- server-data:/data
environment:
QUICNPROTOCHAT_DATA_DIR: "/data"
volumes:
server-data:
```
Or use a bind mount for easier inspection:
```bash
docker compose run \
-v $(pwd)/server-data:/data \
-e QUICNPROTOCHAT_DATA_DIR=/data \
server
```
Without a volume, all server state (including TLS certificates and message queues) is lost when the container is removed. The server will generate a new self-signed certificate on each fresh start, which means clients will need the new certificate to connect.
---
## Building just the image
To build the Docker image without starting a container:
```bash
docker build -t quicnprotochat-server -f docker/Dockerfile .
```
To run it manually:
```bash
docker run -d \
--name quicnprotochat \
-p 7000:7000/udp \
-e QUICNPROTOCHAT_LISTEN=0.0.0.0:7000 \
-e RUST_LOG=info \
quicnprotochat-server
```
Note the `/udp` suffix on the port mapping -- QUIC runs over UDP.
---
## Connecting the client to a containerised server
When the server runs in Docker with `docker compose up`, the client can connect from the host:
```bash
# Extract the server's TLS cert from the container
docker compose cp server:/data/server-cert.der ./data/server-cert.der
# Connect
cargo run -p quicnprotochat-client -- ping \
--ca-cert ./data/server-cert.der \
--server-name localhost
```
If you mounted a volume (e.g., `./server-data:/data`), the certificate is directly accessible at `./server-data/server-cert.der`.
---
## Next steps
- [Running the Server](running-the-server.md) -- server configuration without Docker
- [Running the Client](running-the-client.md) -- CLI subcommands
- [Demo Walkthrough](demo-walkthrough.md) -- step-by-step messaging scenario

View File

@@ -0,0 +1,101 @@
# Prerequisites
Before building quicnprotochat you need a Rust toolchain and the Cap'n Proto schema compiler. Docker is optional but useful for reproducible builds and deployment.
---
## Rust toolchain
**Minimum supported Rust version: 1.77+ (stable)**
quicnprotochat uses the 2021 edition and workspace resolver v2. Any stable Rust release from 1.77 onward should work. Install or update via [rustup](https://rustup.rs/):
```bash
# Install rustup (if not already present)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Ensure you are on a recent stable release
rustup update stable
rustup default stable
# Verify
rustc --version # should print 1.77.0 or later
cargo --version
```
The workspace depends on several crates that use procedural macros (`serde_derive`, `clap_derive`, `tls_codec_derive`, `thiserror`). These compile during the build step and require no additional system libraries beyond what `rustc` ships.
---
## Cap'n Proto compiler (`capnp`)
The `quicnprotochat-proto` crate runs a `build.rs` script that invokes the `capnp` binary at compile time to generate Rust types from the `.capnp` schema files in `schemas/`. The `capnp` binary must be on your `PATH`.
### Debian / Ubuntu
```bash
sudo apt-get update
sudo apt-get install -y capnproto
```
### macOS (Homebrew)
```bash
brew install capnp
```
### Verify installation
```bash
capnp --version
# Expected output: Cap'n Proto version X.Y.Z
```
If `capnp` is not found, the build will fail with an error from `capnpc::CompilerCommand`:
```
Cap'n Proto schema compilation failed. Is `capnp` installed?
(apt-get install capnproto / brew install capnp)
```
See [Building from Source -- Troubleshooting](building.md#troubleshooting) for more details.
### Other platforms
| Platform | Install command |
|---|---|
| Fedora / RHEL | `dnf install capnproto` |
| Arch Linux | `pacman -S capnproto` |
| Nix | `nix-env -iA nixpkgs.capnproto` |
| Windows (vcpkg) | `vcpkg install capnproto` |
| From source | [capnproto.org/install.html](https://capnproto.org/install.html) |
---
## Optional: Docker and Docker Compose
If you prefer to build and run quicnprotochat in containers, you will need:
- **Docker Engine** 20.10+ (or Docker Desktop)
- **Docker Compose** v2+ (the `docker compose` plugin, not the legacy `docker-compose` binary)
```bash
docker --version # 20.10+
docker compose version # v2+
```
The provided `docker/Dockerfile` is a multi-stage build that installs `capnproto` in the builder stage, so you do **not** need the `capnp` binary on your host when building via Docker.
See [Docker Deployment](docker.md) for full instructions.
---
## Summary checklist
| Dependency | Required? | How to check |
|---|---|---|
| Rust stable 1.77+ | Yes | `rustc --version` |
| `capnp` CLI | Yes (host builds) | `capnp --version` |
| Docker + Compose | No (container builds only) | `docker --version` / `docker compose version` |
Once all prerequisites are satisfied, proceed to [Building from Source](building.md).

View File

@@ -0,0 +1,285 @@
# Running the Client
The quicnprotochat CLI client provides subcommands for connectivity testing, identity registration, KeyPackage exchange, and persistent group messaging. All commands connect to the server over QUIC + TLS 1.3 and issue Cap'n Proto RPC calls against the `NodeService` endpoint.
---
## Global flags
These flags apply to every subcommand:
| Flag | Env var | Default | Purpose |
|---|---|---|---|
| `--ca-cert` | `QUICNPROTOCHAT_CA_CERT` | `data/server-cert.der` | Path to the server's TLS certificate (DER format). The client uses this to verify the server's identity during the TLS handshake. |
| `--server-name` | `QUICNPROTOCHAT_SERVER_NAME` | `localhost` | Expected TLS server name. Must match a SAN in the server's certificate. |
Most subcommands also accept `--server` (default `127.0.0.1:7000`) to specify the server address.
---
## Connectivity
### `ping`
Send a health probe to the server and print the round-trip time.
```bash
cargo run -p quicnprotochat-client -- ping
```
```bash
cargo run -p quicnprotochat-client -- ping --server 192.168.1.10:7000
```
**Output:**
```
health=ok rtt=3ms
```
This exercises the full QUIC + TLS 1.3 connection setup plus a single Cap'n Proto `health()` RPC call. Useful for verifying that the server is reachable and TLS verification succeeds.
---
## Ephemeral identity commands
These commands generate a fresh identity keypair in memory each time they run. The identity is not persisted and is discarded when the process exits. They are useful for quick tests and for the automated `demo-group` scenario.
### `register`
Generate a fresh Ed25519 identity, create an MLS KeyPackage, and upload it to the Authentication Service.
```bash
cargo run -p quicnprotochat-client -- register
```
**Output:**
```
identity_key : a1b2c3d4e5f6... (64 hex chars = 32 bytes)
fingerprint : 9f8e7d6c5b4a... (SHA-256 of the KeyPackage)
KeyPackage uploaded successfully.
```
Share the `identity_key` value with peers who want to add you to a group. They will pass it to `fetch-key` or `invite --peer-key`.
### `fetch-key <identity_key>`
Fetch a peer's KeyPackage from the Authentication Service by their Ed25519 public key.
```bash
cargo run -p quicnprotochat-client -- fetch-key a1b2c3d4e5f6...
```
The `identity_key` argument must be exactly 64 lowercase hex characters (32 bytes).
**Output (success):**
```
fingerprint : 9f8e7d6c5b4a...
package_len : 742 bytes
KeyPackage fetched successfully.
```
**Output (no KeyPackage available):**
```
No KeyPackage available for this identity.
```
KeyPackages are single-use: fetching a KeyPackage atomically removes it from the server. If the peer needs to be added to another group, they must upload a new KeyPackage.
### `demo-group`
Run a complete Alice-and-Bob MLS round-trip against a live server. Both identities are created in-process; both communicate through the server's AS and DS.
```bash
cargo run -p quicnprotochat-client -- demo-group --server 127.0.0.1:7000
```
**Output:**
```
Alice -> Bob plaintext: hello bob
Bob -> Alice plaintext: hello alice
demo-group complete
```
This is the fastest way to verify that the entire stack (QUIC + TLS + Cap'n Proto RPC + MLS group operations + DS relay) is working end to end. For a detailed breakdown of what happens during `demo-group`, see the [Demo Walkthrough](demo-walkthrough.md).
---
## Persistent group commands
These commands use a state file (`--state`, default `quicnprotochat-state.bin`) to persist the Ed25519 identity seed and MLS group state between invocations. A companion key store file (same path with `.ks` extension) holds HPKE init private keys.
All persistent commands share the `--state` flag:
| Flag | Env var | Default |
|---|---|---|
| `--state` | `QUICNPROTOCHAT_STATE` | `quicnprotochat-state.bin` |
| `--server` | `QUICNPROTOCHAT_SERVER` | `127.0.0.1:7000` |
### `register-state`
Create or load a persistent identity, generate a KeyPackage, and upload it to the AS.
```bash
cargo run -p quicnprotochat-client -- register-state \
--state alice.bin \
--server 127.0.0.1:7000
```
If `alice.bin` does not exist, a new identity is generated and saved. If it already exists, the existing identity is loaded and a new KeyPackage is generated from it.
**Output:**
```
identity_key : a1b2c3d4e5f6...
fingerprint : 9f8e7d6c5b4a...
KeyPackage uploaded successfully.
```
### `create-group`
Create a new MLS group. The caller becomes the sole member at epoch 0.
```bash
cargo run -p quicnprotochat-client -- create-group \
--state alice.bin \
--group-id "project-chat"
```
**Output:**
```
group created: project-chat
```
The group state is saved to the state file. You can now invite peers with `invite`.
### `invite`
Fetch a peer's KeyPackage from the AS, add them to the group, and deliver the Welcome message via the DS.
```bash
cargo run -p quicnprotochat-client -- invite \
--state alice.bin \
--peer-key b9a8c7d6e5f4... \
--server 127.0.0.1:7000
```
This command performs three operations in sequence:
1. Fetches the peer's KeyPackage from the AS (`fetchKeyPackage` RPC).
2. Calls `add_member()` on the local MLS group, producing a Commit and a Welcome.
3. Enqueues the Welcome to the DS for the peer's identity key (`enqueue` RPC).
**Output:**
```
invited peer (welcome queued)
```
### `join`
Join a group by consuming a Welcome message from the DS.
```bash
cargo run -p quicnprotochat-client -- join \
--state bob.bin \
--server 127.0.0.1:7000
```
The command fetches all pending messages for the local identity from the DS and expects to find a Welcome. The Welcome is processed by `MlsGroup::new_from_welcome()`, which decrypts it using the HPKE init private key stored in the key store.
**Output:**
```
joined group successfully
```
### `send`
Encrypt and send an application message to a peer via the DS.
```bash
cargo run -p quicnprotochat-client -- send \
--state alice.bin \
--peer-key b9a8c7d6e5f4... \
--msg "hello from alice" \
--server 127.0.0.1:7000
```
The message is encrypted as an MLS `PrivateMessage` using the current epoch's key schedule, then enqueued to the DS for the specified recipient.
**Output:**
```
message sent
```
### `recv`
Receive and decrypt all pending messages from the DS.
```bash
cargo run -p quicnprotochat-client -- recv \
--state bob.bin \
--server 127.0.0.1:7000
```
**Output:**
```
[0] plaintext: hello from alice
```
Additional flags:
| Flag | Default | Purpose |
|---|---|---|
| `--wait-ms` | `0` | Long-poll timeout in milliseconds. If no messages are queued, wait up to this long before returning. Uses the `fetchWait` RPC. |
| `--stream` | `false` | Continuously long-poll for messages. The process will not exit until interrupted. |
```bash
# Wait up to 5 seconds for messages
cargo run -p quicnprotochat-client -- recv \
--state bob.bin \
--wait-ms 5000
# Stream messages continuously
cargo run -p quicnprotochat-client -- recv \
--state bob.bin \
--stream --wait-ms 10000
```
---
## HPKE init key lifecycle warning
The MLS protocol requires that the HPKE init private key generated during KeyPackage creation is available when processing the corresponding Welcome message. In quicnprotochat, this private key is stored in the key store file (`.ks` extension alongside the state file).
**The same state file and key store must be used for both `register-state` and `join`.** If you:
- Run `register-state` with `--state bob.bin` (which generates `bob.ks`)
- Delete or move `bob.ks` before running `join`
- Or use a different `--state` path for `join`
...then `join` will fail because the HPKE init private key cannot be found.
In ephemeral mode (`register` and `demo-group`), the key is held in process memory and is only valid for the lifetime of that process.
---
## Command reference summary
| Command | Persistent? | Description |
|---|---|---|
| `ping` | No | Health check, prints RTT |
| `register` | No | Generate ephemeral identity + KeyPackage, upload to AS |
| `fetch-key <hex>` | No | Fetch a peer's KeyPackage from AS |
| `demo-group` | No | Automated Alice-and-Bob round-trip |
| `register-state` | Yes | Upload KeyPackage for persistent identity |
| `create-group` | Yes | Create MLS group (sole member, epoch 0) |
| `invite` | Yes | Add peer to group, deliver Welcome via DS |
| `join` | Yes | Consume Welcome from DS, join group |
| `send` | Yes | Encrypt and enqueue application message via DS |
| `recv` | Yes | Fetch, decrypt, and display pending messages |
---
## Next steps
- [Demo Walkthrough](demo-walkthrough.md) -- step-by-step narrative with two terminals
- [Running the Server](running-the-server.md) -- server configuration and TLS setup
- [MLS (RFC 9420)](../protocol-layers/mls.md) -- how MLS group operations work under the hood

View File

@@ -0,0 +1,166 @@
# Running the Server
The quicnprotochat server is a single binary (`quicnprotochat-server`) that exposes a unified **NodeService** endpoint combining Authentication Service (KeyPackage management) and Delivery Service (message relay) operations over a single QUIC + TLS 1.3 connection.
---
## Quick start
```bash
cargo run -p quicnprotochat-server
```
On first launch the server will:
1. Create the `data/` directory if it does not exist.
2. Generate a self-signed TLS certificate and private key (`data/server-cert.der`, `data/server-key.der`) with SANs `localhost`, `127.0.0.1`, and `::1`.
3. Open a QUIC endpoint on `0.0.0.0:7000`.
4. Begin accepting connections.
You should see output similar to:
```
2025-01-01T00:00:00.000000Z INFO quicnprotochat_server: generated self-signed TLS certificate cert="data/server-cert.der" key="data/server-key.der"
2025-01-01T00:00:00.000000Z INFO quicnprotochat_server: accepting QUIC connections addr="0.0.0.0:7000"
```
---
## Configuration
All configuration is available via CLI flags and environment variables. Environment variables take precedence when both are specified.
| Purpose | CLI flag | Env var | Default |
|---|---|---|---|
| QUIC listen address | `--listen` | `QUICNPROTOCHAT_LISTEN` | `0.0.0.0:7000` |
| TLS certificate (DER) | `--tls-cert` | `QUICNPROTOCHAT_TLS_CERT` | `data/server-cert.der` |
| TLS private key (DER) | `--tls-key` | `QUICNPROTOCHAT_TLS_KEY` | `data/server-key.der` |
| Data directory | `--data-dir` | `QUICNPROTOCHAT_DATA_DIR` | `data` |
| Log level | -- | `RUST_LOG` | `info` |
### Examples
```bash
# Listen on a custom port
cargo run -p quicnprotochat-server -- --listen 0.0.0.0:9000
# Use pre-existing TLS credentials
cargo run -p quicnprotochat-server -- \
--tls-cert /etc/quicnprotochat/cert.der \
--tls-key /etc/quicnprotochat/key.der
# Via environment variables
QUICNPROTOCHAT_LISTEN=0.0.0.0:9000 \
RUST_LOG=debug \
cargo run -p quicnprotochat-server
```
---
## TLS certificate handling
### Self-signed certificate auto-generation
If the files at `--tls-cert` and `--tls-key` do not exist when the server starts, it generates a self-signed certificate using the `rcgen` crate. The generated certificate includes three Subject Alternative Names:
- `localhost`
- `127.0.0.1`
- `::1`
The certificate and key are written in DER format. Parent directories are created automatically.
### Using your own certificate
To use a certificate issued by a CA or a custom self-signed certificate:
1. Convert your certificate and key to DER format if they are in PEM:
```bash
openssl x509 -in cert.pem -outform DER -out cert.der
openssl pkcs8 -topk8 -inform PEM -outform DER -in key.pem -out key.der -nocrypt
```
2. Point the server at them:
```bash
cargo run -p quicnprotochat-server -- \
--tls-cert cert.der \
--tls-key key.der
```
3. Distribute the certificate (or its CA root) to clients so they can verify the server. The client's `--ca-cert` flag accepts a DER file.
### TLS configuration details
The server's TLS stack is configured as follows:
- **Protocol versions**: TLS 1.3 only (`rustls::version::TLS13`). TLS 1.2 and below are rejected.
- **Client authentication**: Disabled (`with_no_client_auth()`). The server does not request a client certificate. Client identity is established at the MLS layer via Ed25519 credentials, not at the TLS layer.
- **ALPN**: The server advertises `b"capnp"` as the application-layer protocol.
---
## ALPN negotiation
Both the server and client must agree on the ALPN token `b"capnp"` during the TLS handshake. This token is hardcoded in the server's TLS configuration:
```rust
tls.alpn_protocols = vec![b"capnp".to_vec()];
```
If a client connects with a different (or no) ALPN token, the QUIC handshake will fail with an ALPN mismatch error.
---
## Storage
The server persists its state to the data directory (`--data-dir`, default `data/`):
| File | Contents |
|---|---|
| `data/server-cert.der` | TLS certificate (DER) |
| `data/server-key.der` | TLS private key (DER) |
| `data/keypackages.bin` | `bincode`-serialised map of identity keys to KeyPackage queues |
| `data/deliveries.bin` | `bincode`-serialised map of `(channelId, recipientKey)` to message queues |
| `data/hybridkeys.bin` | `bincode`-serialised map of identity keys to hybrid (X25519 + ML-KEM-768) public keys |
Storage is implemented by the `FileBackedStore` in `crates/quicnprotochat-server/src/storage.rs`. Every mutation (upload, enqueue, fetch) flushes the entire map to disk synchronously. This is suitable for proof-of-concept workloads but not production traffic. See [Storage Backend](../internals/storage-backend.md) for details.
---
## Connection handling
Each incoming QUIC connection is handled in a `tokio::task::spawn_local` task on a shared `LocalSet`. The `capnp-rpc` library uses `Rc<RefCell<>>` internally, making it `!Send`, which is why all RPC tasks must run on a `LocalSet` rather than being spawned with `tokio::spawn`.
The connection lifecycle:
1. Accept incoming QUIC connection.
2. Complete TLS 1.3 handshake.
3. Accept a bidirectional QUIC stream.
4. Wrap the stream in a `capnp_rpc::twoparty::VatNetwork`.
5. Bootstrap a `NodeService` RPC endpoint.
6. Serve requests until the client disconnects or an error occurs.
---
## Logging
The server uses `tracing` with `tracing-subscriber` and respects the `RUST_LOG` environment variable:
```bash
# Default: info level
RUST_LOG=info cargo run -p quicnprotochat-server
# Debug level for detailed RPC tracing
RUST_LOG=debug cargo run -p quicnprotochat-server
# Trace level for maximum verbosity
RUST_LOG=trace cargo run -p quicnprotochat-server
# Filter to specific crates
RUST_LOG=quicnprotochat_server=debug,quinn=warn cargo run -p quicnprotochat-server
```
---
## Next steps
- [Running the Client](running-the-client.md) -- connect to the server and exercise the CLI
- [Demo Walkthrough](demo-walkthrough.md) -- step-by-step Alice-and-Bob group messaging scenario
- [Service Architecture](../architecture/service-architecture.md) -- how the NodeService combines AS and DS