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
# Debian / Ubuntu
apt-get install capnproto

# macOS
brew install capnp

Build everything:

cargo build --workspace

Run tests:

cargo test --workspace

Running

Start the server (NodeService on :4201):

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:

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

Description
End-to-end encrypted messaging over QUIC + TLS 1.3 + MLS (RFC 9420), written in Rust.
Readme 6.9 MiB
Languages
Rust 86.4%
HTML 3.6%
Python 2.1%
TeX 1.9%
Shell 1.6%
Other 4.2%