247 lines
9.4 KiB
Markdown
247 lines
9.4 KiB
Markdown
# quicnprotochat
|
||
|
||
> End-to-end encrypted group messaging over **QUIC + TLS 1.3 + MLS** (RFC 9420), written in Rust.
|
||
|
||
Every byte on the wire is protected by a QUIC transport secured with TLS 1.3
|
||
(`quinn` + `rustls`). The inner **MLS** layer provides post-compromise security
|
||
and ratcheted group key agreement across any number of participants. Messages
|
||
are framed with **Cap'n Proto**, keeping serialisation zero-copy and
|
||
schema-versioned.
|
||
|
||
---
|
||
|
||
## Protocol stack
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ 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
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
| Property | Mechanism |
|
||
|---|---|
|
||
| Transport confidentiality | TLS 1.3 over QUIC (rustls) |
|
||
| Transport authentication | TLS 1.3 server cert (self-signed by default) |
|
||
| Group key agreement | MLS `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519` |
|
||
| Post-compromise security | MLS epoch ratchet |
|
||
| Identity | Ed25519 (MLS credential + leaf node signature) |
|
||
| Message framing | Cap'n Proto (unpacked wire format) |
|
||
|
||
---
|
||
|
||
## Repository layout
|
||
|
||
```
|
||
quicnprotochat/
|
||
├── crates/
|
||
│ ├── quicnprotochat-core/ # Crypto primitives, QUIC/TLS client helpers, MLS group state machine
|
||
│ │ ├── src/codec.rs # LengthPrefixedCodec — Tokio Encoder + Decoder
|
||
│ │ ├── src/keypair.rs # Transport key helpers (X25519, zeroize-on-drop)
|
||
│ │ ├── src/identity.rs # IdentityKeypair — Ed25519 identity + MLS Signer
|
||
│ │ ├── src/keypackage.rs# generate_key_package — standalone KeyPackage helper
|
||
│ │ └── src/group.rs # GroupMember — full MLS group lifecycle
|
||
│ │
|
||
│ ├── quicnprotochat-proto/ # Cap'n Proto schemas + generated types + serde helpers
|
||
│ │ └── schemas/ → # (symlinked to workspace root schemas/)
|
||
│ │
|
||
│ ├── quicnprotochat-server/ # Authentication Service (AS) + Delivery Service (DS) binary
|
||
│ └── quicnprotochat-client/ # CLI client (ping, register, fetch-key, …)
|
||
│
|
||
└── schemas/
|
||
├── envelope.capnp # Top-level wire envelope (MsgType discriminant + payload)
|
||
├── auth.capnp # AuthenticationService RPC (KeyPackage upload / fetch)
|
||
└── delivery.capnp # DeliveryService RPC (enqueue / fetch MLS messages)
|
||
```
|
||
|
||
---
|
||
|
||
## Services
|
||
|
||
### Node Service (Auth + Delivery) — port 4201
|
||
|
||
Single QUIC + TLS 1.3 endpoint exposing Cap'n Proto `NodeService` that combines
|
||
Authentication (KeyPackage upload/fetch) and Delivery (enqueue/fetch) operations.
|
||
|
||
```
|
||
uploadKeyPackage(identityKey: Data, package: Data) -> (fingerprint: Data)
|
||
fetchKeyPackage(identityKey: Data) -> (package: Data)
|
||
```
|
||
|
||
Packages are indexed by the raw Ed25519 public key (32 bytes) and consumed
|
||
exactly once on fetch, matching the MLS single-use KeyPackage requirement.
|
||
|
||
A simple store-and-forward relay for MLS messages. The server never inspects
|
||
payloads — it routes opaque blobs by recipient public key.
|
||
|
||
```
|
||
enqueue(recipientKey: Data, payload: Data) -> ()
|
||
fetch(recipientKey: Data) -> (payloads: List(Data))
|
||
```
|
||
|
||
`fetch` atomically drains the entire queue in FIFO order.
|
||
|
||
---
|
||
|
||
## MLS group lifecycle
|
||
|
||
```
|
||
GroupMember::new(identity)
|
||
│
|
||
├─ generate_key_package() → upload bytes to AS
|
||
│
|
||
├─ create_group(group_id) → epoch 0, sole member
|
||
│ └─ add_member(kp_bytes)→ (commit_bytes, welcome_bytes)
|
||
│ ↑ │ │
|
||
│ fetched from AS discard send to joiner via DS
|
||
│
|
||
└─ join_group(welcome_bytes) → joined; ready to encrypt
|
||
├─ send_message(plain) → TLS-encoded PrivateMessage → DS
|
||
└─ receive_message(ct) → Some(plaintext) | None (Commit)
|
||
```
|
||
|
||
The `OpenMlsRustCrypto` backend is **persistent across calls** on the same
|
||
`GroupMember` instance — it holds the HPKE init private key in its in-memory
|
||
key store between `generate_key_package` and `join_group`.
|
||
|
||
---
|
||
|
||
## Building
|
||
|
||
**Prerequisites:**
|
||
|
||
- Rust (stable, 1.77+)
|
||
- `capnp` CLI — the Cap'n Proto schema compiler
|
||
|
||
```bash
|
||
# Debian / Ubuntu
|
||
apt-get install capnproto
|
||
|
||
# macOS
|
||
brew install capnp
|
||
```
|
||
|
||
**Build everything:**
|
||
|
||
```bash
|
||
cargo build --workspace
|
||
```
|
||
|
||
**Run tests:**
|
||
|
||
```bash
|
||
cargo test --workspace
|
||
```
|
||
|
||
---
|
||
|
||
## Running
|
||
|
||
**Start the server** (NodeService on :4201):
|
||
|
||
```bash
|
||
cargo run -p quicnprotochat-server
|
||
# or with a custom port:
|
||
cargo run -p quicnprotochat-server -- --listen 0.0.0.0:4201
|
||
```
|
||
|
||
Current TLS defaults (development): self-signed cert/key written to `data/` if
|
||
missing. Override via CLI flags or env vars:
|
||
|
||
| Purpose | Flag | Env var | Default |
|
||
|---|---|---|---|
|
||
| Listen address | `--listen` | `QUICNPROTOCHAT_LISTEN` | `0.0.0.0:4201` |
|
||
| TLS cert (DER) | `--tls-cert` | `QUICNPROTOCHAT_TLS_CERT` | `data/server-cert.der` |
|
||
| TLS key (DER) | `--tls-key` | `QUICNPROTOCHAT_TLS_KEY` | `data/server-key.der` |
|
||
|
||
**Client commands:**
|
||
|
||
```bash
|
||
# Check connectivity
|
||
cargo run -p quicnprotochat-client -- ping
|
||
|
||
# Generate a fresh identity + KeyPackage, upload to AS
|
||
# Prints your identity_key (hex) — share this with peers
|
||
cargo run -p quicnprotochat-client -- register
|
||
|
||
# Fetch a peer's KeyPackage (they must have registered first)
|
||
cargo run -p quicnprotochat-client -- fetch-key <64-hex-char identity key>
|
||
|
||
# Run an end-to-end Alice↔Bob demo against live AS + DS
|
||
cargo run -p quicnprotochat-client -- demo-group \
|
||
--server 127.0.0.1:4201 \
|
||
--ds-server 127.0.0.1:4201
|
||
|
||
# Persistent group CLI (stateful)
|
||
cargo run -p quicnprotochat-client -- register-state --state state.bin --server 127.0.0.1:4201
|
||
cargo run -p quicnprotochat-client -- create-group --state state.bin --group-id my-group
|
||
cargo run -p quicnprotochat-client -- invite --state state.bin --peer-key <peer hex> --server 127.0.0.1:4201 --ds-server 127.0.0.1:4201
|
||
cargo run -p quicnprotochat-client -- join --state state.bin --ds-server 127.0.0.1:4201
|
||
cargo run -p quicnprotochat-client -- send --state state.bin --peer-key <peer hex> --msg "hello" --ds-server 127.0.0.1:4201
|
||
cargo run -p quicnprotochat-client -- recv --state state.bin --ds-server 127.0.0.1:4201
|
||
```
|
||
|
||
Server address defaults to `127.0.0.1:4201`; override with `--server` or
|
||
`QUICNPROTOCHAT_SERVER`. The same endpoint serves both Authentication and
|
||
Delivery.
|
||
|
||
State file notes: the persisted state stores your identity and MLS group state
|
||
after you have joined. If you generate a KeyPackage (`register-state`) and then
|
||
restart before consuming the Welcome, the join may fail because the HPKE init
|
||
key is not retained; run join in the same session you register.
|
||
|
||
---
|
||
|
||
## Milestones
|
||
|
||
| # | Name | Status | What it adds |
|
||
|---|------|--------|--------------|
|
||
| M1 | QUIC/TLS transport | ✅ | QUIC + TLS 1.3 endpoint, length-prefixed framing, Ping/Pong |
|
||
| M2 | Authentication Service | ✅ | Ed25519 identity, KeyPackage generation, AS upload/fetch |
|
||
| M3 | Delivery Service + MLS groups | ✅ | DS relay, `GroupMember` create/join/add/send/recv |
|
||
| M4 | Group CLI subcommands | 🔜 | Persistent CLI (`create-group`, `invite`, `join`, `send`, `recv`); demo-group already available |
|
||
| M5 | Multi-party groups | 🔜 | N > 2 members, Commit fan-out, Proposal handling |
|
||
| M6 | Persistence | 🔜 | SQLite key store, durable group state |
|
||
| M7 | Post-quantum | 🔜 | PQ hybrid for MLS/HPKE |
|
||
|
||
---
|
||
|
||
## Production hardening roadmap (high level)
|
||
|
||
1) **Transport & identity**: ACME/Let’s Encrypt, pinned identities, TLS policy
|
||
hardening, server identity via CA.
|
||
2) **Persistence**: Move AS/DS and MLS state to Postgres; encrypted at rest;
|
||
retention/TTL and migrations.
|
||
3) **AuthZ & accounts**: User/device accounts (OIDC/passwordless), device
|
||
binding, revocation/recovery; bind MLS credentials to issued identities.
|
||
4) **Delivery semantics**: Message IDs, idempotent enqueue/fetch, ordering per
|
||
conversation, backpressure/retries; attachment pipeline via encrypted
|
||
object storage.
|
||
5) **Observability & ops**: Structured logs with correlation IDs; Prometheus
|
||
metrics; tracing; alerting + SLOs; audit logs for auth/key events.
|
||
6) **Client resilience**: Reconnect/resume, offline queue, multi-device key
|
||
handling; key verification UX (QR/safety numbers); recovery flows.
|
||
7) **Security & compliance**: Dependency audits, fuzzing, SAST/DAST, pentest;
|
||
SBOM/signed releases; PII minimization and retention controls.
|
||
|
||
---
|
||
|
||
## Security notes
|
||
|
||
- This is a **proof-of-concept**. It has not been audited.
|
||
- The server uses a self-signed TLS cert by default; clients trust it via a
|
||
local DER file. No pinning or CA-based identity is enforced yet.
|
||
- MLS credentials use `CredentialType::Basic` (public key only). A real
|
||
deployment would bind credentials to a certificate authority.
|
||
- The Delivery operation does no authentication of the `recipientKey` field —
|
||
anyone can enqueue for any recipient. Access control is a future milestone.
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
MIT
|