Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
*.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated
HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
330 lines
15 KiB
Markdown
330 lines
15 KiB
Markdown
# quicproquo — Master Project Prompt
|
||
|
||
## Project Identity
|
||
|
||
You are building **quicproquo**, a production-grade end-to-end encrypted group messenger in Rust. It uses the MLS protocol (RFC 9420) for group key agreement, ML-KEM-768 (NIST FIPS 203) hybrid post-quantum key exchange, the Noise Protocol Framework (Noise_XX pattern) over raw TCP as the transport layer, and Cap'n Proto for wire serialisation and RPC. There is no TLS, no HTTP, no WebSocket, no MessagePack.
|
||
|
||
This is not a prototype. Every milestone produces production-ready, tested, deployable code.
|
||
|
||
---
|
||
|
||
## Non-Negotiable Engineering Standards
|
||
|
||
- **Production-ready only.** No stubs, mocks, `todo!()`, `unimplemented!()`, or placeholder logic in deliverables. If something is out of scope for the current milestone, it is explicitly omitted with a documented reason, not silently stubbed.
|
||
- **YAGNI / KISS / DRY.** Do not add features, abstractions, or generics that are not required by the current milestone. Favour clarity over cleverness.
|
||
- **Spec-first.** Document the design (ADR or inline doc comment) before implementing it. Every public API must have a doc comment explaining what it does, its invariants, and any error conditions.
|
||
- **Security-by-design.** Secrets use `zeroize`. No secret material in logs. No `unwrap()` on cryptographic operations — all errors are typed and propagated. Constant-time comparisons where required.
|
||
- **Containerised.** The server runs in Docker. `docker-compose.yml` is always kept up to date.
|
||
- **Dependency hygiene.** Pin major versions. Prefer the `dalek` ecosystem for classical crypto, `snow` for Noise, `openmls` for MLS, `ml-kem` for post-quantum, `capnp`/`capnp-rpc` for serialisation and RPC. Do not introduce new dependencies without justification.
|
||
- **Review before presenting.** Before presenting any code, review it for: missing error handling, security gaps, incomplete implementations, and deviation from these standards. Fix all issues found before output.
|
||
|
||
---
|
||
|
||
## Git Standards
|
||
|
||
- GPG-signed commits only.
|
||
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`.
|
||
- Feature branches per milestone: `feat/m1-noise-transport`, `feat/m2-keypackage-as`, etc.
|
||
- No `Co-authored-by` trailers.
|
||
- Commit messages describe *why*, not just *what*.
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
### Workspace Layout
|
||
|
||
```
|
||
quicproquo/
|
||
├── Cargo.toml # workspace root
|
||
├── crates/
|
||
│ ├── quicproquo-core/ # crypto primitives, MLS wrapper, Noise framing codec
|
||
│ ├── quicproquo-proto/ # Cap'n Proto schemas + generated types, no crypto, no I/O
|
||
│ ├── quicproquo-server/ # Delivery Service (DS) + Authentication Service (AS)
|
||
│ └── quicproquo-client/ # CLI client
|
||
├── schemas/ # .capnp schema files (canonical source of truth)
|
||
│ ├── envelope.capnp
|
||
│ ├── auth.capnp
|
||
│ └── delivery.capnp
|
||
├── docker/
|
||
│ └── Dockerfile
|
||
├── docker-compose.yml
|
||
└── docs/
|
||
└── architecture.md
|
||
```
|
||
|
||
### Crate Responsibilities
|
||
|
||
**quicproquo-core**
|
||
- Noise_XX handshake initiator and responder (via `snow`)
|
||
- Length-prefixed Cap'n Proto frame codec (Tokio `Encoder`/`Decoder` traits)
|
||
- MLS group state machine wrapper around `openmls`
|
||
- Hybrid PQ ciphersuite (X25519 + ML-KEM-768)
|
||
- Key generation and zeroize-on-drop key types
|
||
|
||
**quicproquo-proto**
|
||
- Cap'n Proto `.capnp` schemas in `schemas/` (workspace root, shared)
|
||
- `build.rs` invokes `capnpc` to generate Rust types into `src/generated/`
|
||
- Re-exports generated types with ergonomic builder/reader helpers
|
||
- Canonical serialisation helpers for signing (uses `capnp::message::Builder::canonicalize()`)
|
||
- No crypto, no I/O, no async
|
||
|
||
**quicproquo-server**
|
||
- Authentication Service: KeyPackage store (DashMap → SQLite at M6)
|
||
- Delivery Service: Cap'n Proto RPC interface, fan-out router, per-group append-only message log
|
||
- Tokio TCP listener, Noise handshake per connection, then Cap'n Proto RPC over the encrypted channel
|
||
- Structured logging (tracing)
|
||
|
||
**quicproquo-client**
|
||
- Tokio TCP connection to server
|
||
- Noise handshake, then Cap'n Proto RPC client stub
|
||
- CLI interface (clap)
|
||
- Drives quicproquo-core for all crypto operations
|
||
- Displays received messages to stdout
|
||
|
||
### Transport Stack
|
||
|
||
```
|
||
TCP connection
|
||
└── Noise_XX handshake (snow)
|
||
└── Authenticated encrypted channel (ChaCha20-Poly1305)
|
||
└── [u32 frame_len][Cap'n Proto encoded message]
|
||
└── Cap'n Proto RPC (capnp-rpc, M2+)
|
||
```
|
||
|
||
Both sides hold static X25519 keypairs for the Noise handshake and Ed25519 keypairs for MLS identity. After Noise_XX, mutual authentication is complete. All subsequent frames are Noise-encrypted. Cap'n Proto RPC runs inside the encrypted channel — it has no knowledge of the transport security.
|
||
|
||
### Cap'n Proto Schemas
|
||
|
||
```capnp
|
||
# schemas/envelope.capnp
|
||
@0xDEADBEEFCAFEBABE; # unique file ID (generate with: capnp id)
|
||
|
||
struct Envelope {
|
||
msgType @0 :MsgType;
|
||
groupId @1 :Data; # 32 bytes, SHA-256 of group name
|
||
senderId @2 :Data; # 32 bytes, SHA-256 of sender identity key
|
||
payload @3 :Data; # opaque: MLS blob or control payload
|
||
timestampMs @4 :UInt64; # unix milliseconds
|
||
|
||
enum MsgType {
|
||
ping @0;
|
||
pong @1;
|
||
keyPackageUpload @2;
|
||
keyPackageFetch @3;
|
||
keyPackageResponse @4;
|
||
mlsWelcome @5;
|
||
mlsCommit @6;
|
||
mlsApplication @7;
|
||
error @8;
|
||
}
|
||
}
|
||
|
||
# schemas/auth.capnp
|
||
@0xAAAABBBBCCCCDDDD;
|
||
|
||
interface AuthenticationService {
|
||
# Upload a KeyPackage for later retrieval by peers adding this client to a group.
|
||
# identityKey: Ed25519 public key bytes (32 bytes)
|
||
# package: openmls-serialised KeyPackage blob
|
||
# Returns the SHA-256 fingerprint of the package on success.
|
||
uploadKeyPackage @0 (identityKey :Data, package :Data) -> (fingerprint :Data);
|
||
|
||
# Fetch one KeyPackage for a given identity key.
|
||
# Consuming: the server removes the returned KeyPackage (one-time use, MLS spec).
|
||
# Returns empty Data if no KeyPackage is available for this identity.
|
||
fetchKeyPackage @1 (identityKey :Data) -> (package :Data);
|
||
}
|
||
|
||
# schemas/delivery.capnp
|
||
@0x1111222233334444;
|
||
|
||
interface DeliveryService {
|
||
# Fan out an MLS message to all current members of a group.
|
||
# groupId: 32-byte group identifier
|
||
# message: serialised MLSMessage blob
|
||
# Returns count of recipients the message was queued for.
|
||
fanOut @0 (groupId :Data, message :Data) -> (recipientCount :UInt32);
|
||
|
||
# Subscribe to incoming messages for a group.
|
||
# memberId: 32-byte identity key fingerprint of the subscribing client.
|
||
# Returns a capability stream; server pushes MLS blobs as they arrive.
|
||
subscribe @1 (groupId :Data, memberId :Data) -> (stream :MessageStream);
|
||
}
|
||
|
||
interface MessageStream {
|
||
# Pull the next available message for this subscriber.
|
||
# Blocks (promise does not resolve) until a message is available.
|
||
# sequenceNo is monotonically increasing per group, used for gap detection.
|
||
next @0 () -> (message :Data, sequenceNo :UInt64);
|
||
}
|
||
```
|
||
|
||
### MLS Design
|
||
|
||
- Ciphersuite: `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519` as baseline (M1–M4), replaced with hybrid PQ ciphersuite at M5.
|
||
- DS is MLS-unaware: routes `MLSMessage` blobs by `group_id`. Does not inspect epoch or content.
|
||
- AS stores `KeyPackage` blobs indexed by `(identity_key_fingerprint, package_id)`. One KeyPackage consumed per member-add operation (MLS requirement: KeyPackages are single-use).
|
||
- Welcome messages routed by the DS to the target client using `sender_id` → `target_id` mapping in the Envelope.
|
||
- Cap'n Proto canonical form used when serialising any structure that is subsequently signed (MLS Commit signatures, AS KeyPackage fingerprints).
|
||
|
||
### Post-Quantum (M5)
|
||
|
||
Hybrid KEM construction:
|
||
```
|
||
SharedSecret = HKDF-SHA256(
|
||
ikm = X25519_ss || ML-KEM-768_ss,
|
||
info = "quicproquo-hybrid-v1",
|
||
len = 32
|
||
)
|
||
```
|
||
Follows the combiner approach from draft-ietf-tls-hybrid-design. Implemented as a custom `openmls` `OpenMlsCryptoProvider` trait implementation in `quicproquo-core`.
|
||
|
||
---
|
||
|
||
## Milestones
|
||
|
||
### M1 — Noise Transport ✦ current
|
||
**Goal:** Two processes complete Noise_XX handshake over TCP and exchange typed Cap'n Proto frames.
|
||
|
||
Deliverables:
|
||
- `schemas/envelope.capnp`: `Envelope` + `MsgType` (Ping/Pong only needed at this stage)
|
||
- `quicproquo-proto`: `build.rs` with `capnpc`, generated type re-exports, canonical helper
|
||
- `quicproquo-core`: static X25519 keypair generation, Noise_XX initiator + responder, length-prefixed Cap'n Proto frame codec
|
||
- `quicproquo-server`: TCP listener, Noise handshake, Ping→Pong handler, one tokio task per connection
|
||
- `quicproquo-client`: connects, Noise handshake, sends Ping, receives Pong, exits 0
|
||
- Integration test: server and client in same test binary using `tokio::spawn`
|
||
- `docker-compose.yml` running the server
|
||
|
||
### M2 — Authentication Service + KeyPackage Exchange
|
||
**Goal:** Clients register identity and publish/fetch MLS KeyPackages via Cap'n Proto RPC.
|
||
|
||
Deliverables:
|
||
- `schemas/auth.capnp`: `AuthenticationService` interface
|
||
- `quicproquo-proto`: generated RPC stubs + client/server bootstrap helpers
|
||
- `quicproquo-core`: MLS KeyPackage generation (openmls)
|
||
- `quicproquo-server`: AS RPC server implementation with DashMap store
|
||
- `quicproquo-client`: `register` and `fetch-key` CLI subcommands
|
||
- Test: Alice uploads KeyPackage, Bob fetches it, fingerprints match
|
||
|
||
### M3 — MLS Group Create + Welcome
|
||
**Goal:** Alice creates a group and adds Bob via MLS Welcome. Both hold valid epoch 1 state.
|
||
|
||
Deliverables:
|
||
- `schemas/delivery.capnp`: `DeliveryService` + `MessageStream` interfaces
|
||
- `quicproquo-core`: group create, add member, process Welcome
|
||
- `quicproquo-server`: DS RPC server, Welcome routing by identity
|
||
- `quicproquo-client`: `create-group` and `join` CLI subcommands
|
||
- Test: two clients reach identical epoch 1 group state, verified by comparing group context hashes
|
||
|
||
### M4 — Encrypted Group Messaging
|
||
**Goal:** Alice and Bob exchange MLS Application messages through the DS.
|
||
|
||
Deliverables:
|
||
- `quicproquo-core`: send/receive application message, epoch rotation on Commit
|
||
- `quicproquo-server`: DS fan-out via `MessageStream` capability stream, per-group ordered log (in-memory)
|
||
- `quicproquo-client`: `send` subcommand, live receive loop via `MessageStream.next()`
|
||
- Test: round-trip message integrity, forward secrecy verified by confirming distinct key material across epochs
|
||
|
||
### M5 — Hybrid PQ Ciphersuite
|
||
**Goal:** Replace MLS crypto backend with X25519 + ML-KEM-768 hybrid.
|
||
|
||
Deliverables:
|
||
- `quicproquo-core`: custom `OpenMlsCryptoProvider` with hybrid KEM
|
||
- All M3/M4 tests pass unchanged with new ciphersuite
|
||
- Criterion benchmarks: key generation, encap/decap, group-add latency (10/100/1000 members)
|
||
|
||
### M6 — Persistence + Production Docker
|
||
**Goal:** Server survives restart. Full containerised deployment.
|
||
|
||
Deliverables:
|
||
- `quicproquo-server`: SQLite via `sqlx` for AS key store and DS message log, `migrations/` directory
|
||
- `docker/Dockerfile`: multi-stage build (rust:bookworm builder → debian:bookworm-slim runtime)
|
||
- `docker-compose.yml`: server + SQLite volume, healthcheck
|
||
- Client reconnect with session resume (re-handshake + rejoin group epoch from DS log)
|
||
|
||
---
|
||
|
||
## Dependencies (pinned majors)
|
||
|
||
```toml
|
||
# Crypto
|
||
openmls = "0.6"
|
||
openmls_rust_crypto = "0.6"
|
||
ml-kem = "0.3"
|
||
x25519-dalek = "2"
|
||
ed25519-dalek = "2"
|
||
snow = "0.9"
|
||
chacha20poly1305 = "0.10"
|
||
sha2 = "0.10"
|
||
hkdf = "0.12"
|
||
zeroize = { version = "1", features = ["derive"] }
|
||
rand = "0.8"
|
||
|
||
# Serialisation + RPC
|
||
capnp = "0.19"
|
||
capnp-rpc = "0.19"
|
||
|
||
# Build-time only
|
||
capnpc = "0.19" # build-dependency in quicproquo-proto
|
||
|
||
# Async / networking
|
||
tokio = { version = "1", features = ["full"] }
|
||
tokio-util = { version = "0.7", features = ["codec"] }
|
||
|
||
# Server utilities
|
||
dashmap = "5"
|
||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio"] } # M6+
|
||
tracing = "0.1"
|
||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||
anyhow = "1"
|
||
thiserror = "1"
|
||
|
||
# CLI
|
||
clap = { version = "4", features = ["derive"] }
|
||
```
|
||
|
||
---
|
||
|
||
## Key Design Decisions (ADR Summary)
|
||
|
||
**ADR-001: Noise_XX for transport authentication**
|
||
Both parties hold static keys registered with the AS. XX pattern provides mutual authentication and identity hiding for the initiator. No TLS dependency, no certificate infrastructure.
|
||
|
||
**ADR-002: Cap'n Proto replaces MessagePack**
|
||
Cap'n Proto provides: zero-copy reads, schema-enforced types, canonical serialisation for cryptographic signing, and a built-in async RPC system (capnp-rpc) that eliminates hand-rolled message dispatch. The build-time codegen overhead is accepted as worthwhile.
|
||
|
||
**ADR-003: Cap'n Proto RPC runs inside the Noise tunnel**
|
||
Cap'n Proto RPC has no transport security of its own. It operates over the byte stream produced by the Noise session. Separation of concerns: Noise owns authentication and confidentiality, Cap'n Proto owns framing and dispatch.
|
||
|
||
**ADR-004: DS is MLS-unaware**
|
||
The Delivery Service routes opaque `MLSMessage` blobs by `group_id`. It never decrypts or inspects MLS content. This is the correct MLS architecture (RFC 9420 §4) and is a natural Audit-Core integration point: the DS log is an append-only sequence of authenticated blobs.
|
||
|
||
**ADR-005: Single-use KeyPackages**
|
||
MLS requires that each KeyPackage be used at most once (to preserve forward secrecy of the initial key exchange). The AS consumes a KeyPackage on fetch. Clients should pre-upload multiple KeyPackages. The AS warns when a client's supply runs low (M2+).
|
||
|
||
**ADR-006: PQ gap in Noise transport is accepted**
|
||
The MLS content layer is PQ-protected from M5. The Noise transport (X25519) remains classical — PQ-Noise (draft-noise-pq) is not yet supported by `snow`. Harvest-now-decrypt-later against the handshake metadata is an accepted residual risk for M1–M5. No long-lived content secrets transit the Noise handshake, so the practical impact is limited to identity/timing metadata.
|
||
|
||
---
|
||
|
||
## How to Use This Prompt
|
||
|
||
Paste this document at the start of any session working on quicproquo. Then state which milestone you are working on and what specific task you need. The assistant will:
|
||
|
||
1. Confirm the current milestone and task.
|
||
2. State any design decisions being made (ADR format if significant).
|
||
3. Produce complete, production-ready code for the task.
|
||
4. Review the code internally for gaps before presenting.
|
||
5. State what the next logical task is.
|
||
|
||
When asking for code, always specify:
|
||
- Which crate(s) are affected.
|
||
- Whether this is a new file or modification to existing.
|
||
- Any constraints or context the assistant may not have (e.g. existing types already defined).
|
||
|
||
---
|
||
|
||
*quicproquo — MLS + Post-Quantum + Noise/TCP + Cap'n Proto messenger in Rust*
|
||
*Architecture version: 1.1 | Last updated: 2026-02-19*
|