Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
20 KiB
End-to-End Data Flow
This page traces the three core data flows through the quicprochat system: registration, group creation, and message exchange. Each flow is illustrated with an ASCII sequence diagram showing control-plane (AS) and data-plane (DS) traffic.
Throughout these flows the server is MLS-unaware -- it stores and forwards
opaque byte blobs without parsing their MLS content. All RPC calls use the v2
Protobuf framing protocol over QUIC (ALPN: qpc, port 5001).
1. Registration Flow
Before a client can join any MLS group, it must authenticate with OPAQUE, generate an Ed25519 identity keypair, and upload at least one KeyPackage to the Authentication Service.
Sequence Diagram
Client (Alice) Server (port 5001)
-------------- ------------------
| |
| 1. OpaqueRegisterStart (100) |
| username, registration_request |
| ---------------------------------------->|
| |
| registration_response |
| <----------------------------------------|
| |
| 2. OpaqueRegisterFinish (101) |
| username, upload, identity_key |
| ---------------------------------------->|
| | 3. Store OPAQUE record +
| success | identity key mapping
| <----------------------------------------|
| |
| 4. Generate MLS KeyPackage |
| (GroupMember::generate_key_package) |
| - Creates HPKE init keypair |
| - Embeds Ed25519 pk in credential |
| - Signs leaf node with Ed25519 sk |
| - TLS-encodes the KeyPackage |
| |
| 5. OpaqueLoginStart (102) |
| username, login_request |
| ---------------------------------------->|
| login_response |
| <----------------------------------------|
| |
| 6. OpaqueLoginFinish (103) |
| username, finalization, identity_key |
| ---------------------------------------->|
| session_token |
| <----------------------------------------|
| |
| 7. UploadKeyPackage (300) |
| identity_key, package, session_token |
| ---------------------------------------->|
| | 8. Validate + store
| fingerprint (SHA-256) | in KeyPackage queue
| <----------------------------------------|
| |
| 9. Compare local fingerprint with |
| server-returned fingerprint |
| (tamper detection) |
| |
Key Points
-
KeyPackages are single-use (RFC 9420 requirement). Each
FetchKeyPackagecall atomically removes and returns one package. The client should upload multiple KeyPackages if it expects to be added to several groups. -
The
identity_keyused as the AS index is the raw 32-byte Ed25519 public key, not a fingerprint or hash. Peers must know Alice's public key out-of- band (QR code, directory lookup viaResolveUser, etc.) to fetch her KeyPackage. -
The HPKE init private key generated during
generate_key_packageis stored in the client'sDiskKeyStore. The sameGroupMemberinstance (or a restored instance with the same key store) must later calljoin_groupto decrypt the Welcome message. -
The optional hybrid public key (
UploadHybridKey, method 302) can also be uploaded during registration for post-quantum envelope encryption.
2. Group Creation Flow
Alice creates a new MLS group, fetches Bob's KeyPackage from the AS, adds Bob to the group (producing a Commit and a Welcome), and delivers the Welcome to Bob via the DS.
Sequence Diagram
Alice Server (AS+DS, port 5001) Bob
----- ------------------------- ---
| | |
| 1. create_group("my-group") | |
| (local MLS operation -- | |
| Alice is sole member, | |
| epoch 0) | |
| | |
| 2. FetchKeyPackage (301) | |
| bob_identity_key | |
| --------------------------------> |
| | 3. Pop bob's KeyPackage |
| | from queue (atomic) |
| bob_kp bytes | |
| <-------------------------------- |
| | |
| 4. add_member(bob_kp) | |
| Local MLS operations: | |
| a. Deserialise & validate | |
| Bob's KeyPackage | |
| b. Produce Commit message | |
| (adds Bob to ratchet | |
| tree, advances epoch) | |
| c. Produce Welcome message | |
| (encrypted to Bob's | |
| HPKE init key, contains | |
| group secrets + tree) | |
| d. merge_pending_commit() | |
| (Alice advances to | |
| epoch 1 locally) | |
| | |
| 5. Enqueue (200) | |
| recipient=bob_pk, payload=welcome |
| --------------------------------> |
| | 6. Append welcome to |
| | deliveries[bob_pk] |
| | |
| | 7. Notify bob_pk waiters |
| | (FetchWait wakes up) |
| | |
| | 8. Bob connects and polls |
| | <------------------------------
| | FetchWait (202) |
| | |
| | 9. Drain bob's queue |
| | (returns [welcome]) |
| | |
| | [welcome_bytes] |
| | ------------------------------>
| | |
| | | 10. join_group(welcome)
| | | - Decrypt Welcome with
| | | HPKE init private key
| | | - Extract ratchet tree
| | | from GroupInfo ext
| | | - Initialise MlsGroup
| | | at epoch 1
| | |
| | | Bob is now a group member
| | |
Key Points
-
The Commit message is relevant for groups with more than two members. In the two-party case, Alice is the sole existing member and merges the commit herself. In a multi-member group, the Commit would be sent to all existing members via the DS so they can advance their epoch.
-
The Welcome message is encrypted to Bob's HPKE init key (derived from the KeyPackage). Only the
GroupMemberinstance that generated that KeyPackage holds the corresponding private key. -
The
use_ratchet_tree_extension = trueMLS config embeds the full ratchet tree in the Welcome'sGroupInfoextension. This means Bob does not need a separate tree fetch --new_from_welcomeextracts it automatically. -
The DS routes solely by
recipient_key(Bob's Ed25519 public key). It does not parse the Welcome, the Commit, or any MLS structure.
3. Message Exchange Flow
After both Alice and Bob are group members, they exchange MLS Application messages through the DS.
Sequence Diagram
Alice Server (DS, port 5001) Bob
----- ---------------------- ---
| | |
| -- Alice sends a message to Bob -- |
| | |
| 1. send_message("hello bob") | |
| MLS create_message(): | |
| - Derive message key from | |
| epoch secret + gen counter| |
| - Encrypt plaintext with | |
| AES-128-GCM | |
| - Produce MlsMessageOut | |
| (PrivateMessage variant) | |
| - TLS-encode to bytes | |
| | |
| 2. Enqueue (200) | |
| recipient=bob_pk, payload | |
| --------------------------------> |
| | 3. Store in bob's queue |
| | 4. Notify bob_pk waiters |
| | (or push PushNewMessage) |
| | |
| | (time passes) |
| | |
| | 5. Bob polls for messages |
| | <------------------------------
| | FetchWait (202) |
| | |
| | 6. Drain bob's queue |
| | [ciphertext] |
| | ------------------------------>
| | |
| | | 7. receive_message(ct)
| | | MLS process_message():
| | | - Identify sender from
| | | PrivateMessage header
| | | - Derive decryption key
| | | from epoch secret
| | | - Decrypt AES-128-GCM
| | | - Return plaintext:
| | | "hello bob"
| | |
| -- Bob replies to Alice -- |
| | |
| | | 8. send_message("hello alice")
| | | (same MLS encrypt flow)
| | |
| | 9. Enqueue (200) |
| | recipient=alice_pk |
| | <------------------------------
| | 10. Store + notify |
| | |
| 11. Fetch (201) | |
| --------------------------------> |
| [ciphertext] | |
| <-------------------------------- |
| | |
| 12. receive_message(ct) | |
| -> "hello alice" | |
| | |
Key Points
-
MLS provides forward secrecy: each message is encrypted with a key derived from the current epoch secret and a per-sender generation counter. Compromising a future key does not reveal past messages.
-
The DS is a dumb relay: it does not decrypt, inspect, or reorder messages. It stores opaque byte blobs in a FIFO queue keyed by recipient.
-
Long-polling via
FetchWait(202) avoids the need for persistent connections or WebSocket-style push. The client specifies a timeout in milliseconds; the server blocks up to that duration usingtokio::sync::Notify. Push events (method 1000PushNewMessage) deliver real-time notifications on a separate QUIC uni-stream. -
Channel-aware routing is supported: the
channel_idfield inEnqueueandFetchallows scoping queues by channel (e.g., a UUID for a 1:1 conversation or group). Whenchannel_idis empty, messages go to the default queue.
Control-Plane vs. Data-Plane Summary
+---------------------------------------------------------------------+
| Control Plane (AS) |
| |
| UploadKeyPackage (300) ----> Store KeyPackage for identity |
| FetchKeyPackage (301) <---- Pop and return one KeyPackage |
| UploadHybridKey (302) ----> Store hybrid PQ public key |
| FetchHybridKey (303) <---- Return hybrid PQ public key |
| FetchHybridKeys (304) <---- Return hybrid keys for N identities|
| |
| Traffic: Infrequent. Once per group join (upload before, |
| fetch during group add). |
+---------------------------------------------------------------------+
+---------------------------------------------------------------------+
| Data Plane (DS) |
| |
| Enqueue (200) ----> Append payload to recipient queue |
| Fetch (201) <---- Drain and return all queued payloads|
| FetchWait (202) <---- Long-poll drain with timeout |
| Peek (203) <---- Inspect without removing |
| Ack (204) ----> Acknowledge and remove by seq num |
| BatchEnqueue (205) ----> Enqueue multiple payloads at once |
| |
| Traffic: High-frequency. Every MLS message (Welcome, Commit, |
| Application) flows through the DS. |
+---------------------------------------------------------------------+
The separation means the AS can be rate-limited or placed behind stricter access controls without affecting message throughput on the DS.
State Transitions
The following diagram summarises the client-side state machine across all three flows:
+--------------+
| No State |
+------+-------+
|
OPAQUE register + login
|
v
+--------------+
| Authenticated | session_token obtained
| | No identity yet
+------+--------+
|
IdentityKeypair::generate()
+ UploadKeyPackage (300)
|
v
+--------------+
| Registered | KeyPackage on AS
| | HPKE init key in DiskKeyStore
+------+-------+
|
+--------------+--------------+
| |
create_group() join_group(welcome)
| |
v v
+-------------+ +--------------+
| Group Owner | | Group Member |
| (epoch 0) | | (epoch N) |
+------+------+ +------+-------+
| |
add_member() |
| |
v v
+------------------------------------------+
| Active Group Member |
| |
| send_message() -> Enqueue (200) |
| receive_message() <- Fetch/FetchWait |
| or PushNewMessage |
| |
| Epoch advances on each Commit |
+------------------------------------------+
Further Reading
- Architecture Overview -- system diagram and two-service model
- Service Architecture -- RPC method details and push events
- GroupMember Lifecycle -- detailed MLS state machine
- KeyPackage Exchange Flow -- single-use semantics and AS internals
- MLS (RFC 9420) -- key schedule, ratchet tree, and ciphersuite details
- Forward Secrecy -- how MLS provides forward secrecy
- Post-Compromise Security -- group healing after key compromise