# 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 --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 --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