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
13 KiB
Demo Walkthrough: Alice and Bob
This page walks through a complete end-to-end encrypted conversation between two participants -- Alice and Bob -- using the persistent group CLI. By the end, you will have started a server, registered two identities, created an MLS group, exchanged a Welcome, and sent encrypted messages in both directions.
You will need three terminal windows: one for the server, one for Alice, and one for Bob.
Overview
┌─────────┐ ┌──────────────────┐ ┌─────────┐
│ Alice │ │ Server │ │ Bob │
│ (client) │──── QUIC ────│ AS + DS (:7000) │──── QUIC ────│ (client)│
└─────────┘ └──────────────────┘ └─────────┘
Sequence diagram
Alice Server (AS+DS) Bob
│ │ │
│ 1. register-state │ │
│ ─── uploadKeyPackage ─────> │ │
│ <── fingerprint ─────────── │ │
│ │ │
│ │ 2. register-state │
│ │ <── uploadKeyPackage ───────── │
│ │ ─── fingerprint ────────────> │
│ │ │
│ 3. create-group │ │
│ (local: epoch 0) │ │
│ │ │
│ 4. invite --peer-key <bob> │ │
│ ─── fetchKeyPackage ──────> │ (Bob's KP removed from AS) │
│ <── package ────────────── │ │
│ (local: add_member → Commit + Welcome) │
│ ─── enqueue(Welcome) ─────> │ (queued for Bob) │
│ │ │
│ │ 5. join │
│ │ <── fetch ──────────────────── │
│ │ ─── Welcome ────────────────> │
│ │ (local: new_from_welcome) │
│ │ │
│ 6. send --msg "Hi Bob" │ │
│ (local: create_message → PrivateMessage) │
│ ─── enqueue(ciphertext) ──> │ (queued for Bob) │
│ │ │
│ │ 7. recv │
│ │ <── fetch ──────────────────── │
│ │ ─── ciphertext ─────────────> │
│ │ (local: process_message) │
│ │ plaintext: "Hi Bob" │
│ │ │
│ │ 8. send --msg "Hi Alice" │
│ │ (local: create_message) │
│ │ <── enqueue(ciphertext) ────── │
│ │ │
│ 9. recv │ │
│ ─── fetch ────────────────> │ │
│ <── ciphertext ─────────── │ │
│ (local: process_message) │ │
│ plaintext: "Hi Alice" │ │
│ │ │
Step-by-step instructions
Step 1: Start the server
In Terminal 1 (Server):
cargo run -p quicprochat-server
Wait for the log line confirming it is accepting connections:
INFO quicprochat_server: accepting QUIC connections addr="0.0.0.0:7000"
If this is the first run, you will also see a log line about generating the self-signed TLS certificate. The certificate is written to data/server-cert.der, which the client will use for TLS verification.
Step 2: Alice registers her identity
In Terminal 2 (Alice):
cargo run -p quicprochat-client -- register-state \
--state alice.bin \
--server 127.0.0.1:7000
This command:
- Generates a fresh Ed25519 identity keypair (or loads one from
alice.binif it already exists). - Creates an MLS KeyPackage signed with that identity.
- Uploads the KeyPackage to the server's Authentication Service.
- Saves the identity seed and key store to
alice.binandalice.ks.
Output:
identity_key : <ALICE_KEY> (64 hex chars)
fingerprint : <fingerprint>
KeyPackage uploaded successfully.
Copy the identity_key value -- Bob will need it in Step 5.
Step 3: Bob registers his identity
In Terminal 3 (Bob):
cargo run -p quicprochat-client -- register-state \
--state bob.bin \
--server 127.0.0.1:7000
Output:
identity_key : <BOB_KEY> (64 hex chars)
fingerprint : <fingerprint>
KeyPackage uploaded successfully.
Copy the identity_key value -- Alice will need it in Step 4.
Step 4: Alice creates a group and invites Bob
In Terminal 2 (Alice):
First, create the group:
cargo run -p quicprochat-client -- create-group \
--state alice.bin \
--group-id "demo-chat"
group created: demo-chat
Alice is now the sole member of the group at epoch 0.
Next, invite Bob using his identity key from Step 3:
cargo run -p quicprochat-client -- invite \
--state alice.bin \
--peer-key <BOB_KEY> \
--server 127.0.0.1:7000
This command:
- Fetches Bob's KeyPackage from the AS (this atomically removes it -- single-use).
- Calls
add_member()on Alice's local MLS group, producing a Commit (applied locally) and a Welcome. - Enqueues the Welcome message to the DS, addressed to Bob's identity key.
invited peer (welcome queued)
Alice's group state has now advanced to epoch 1.
Step 5: Bob joins the group
In Terminal 3 (Bob):
cargo run -p quicprochat-client -- join \
--state bob.bin \
--server 127.0.0.1:7000
This command:
- Fetches all pending messages for Bob's identity key from the DS.
- Finds the Welcome message that Alice enqueued.
- Calls
MlsGroup::new_from_welcome(), which decrypts the Welcome using the HPKE init private key from Bob's key store (bob.ks). - Saves the joined group state to
bob.bin.
joined group successfully
Bob is now a member of the group at epoch 1, sharing the same group secret as Alice.
Step 6: Alice sends an encrypted message
In Terminal 2 (Alice):
cargo run -p quicprochat-client -- send \
--state alice.bin \
--peer-key <BOB_KEY> \
--msg "Hello Bob, this is encrypted with MLS!" \
--server 127.0.0.1:7000
This command:
- Calls
create_message()on Alice's MLS group, encrypting the plaintext as an MLSPrivateMessage. - Enqueues the ciphertext to the DS for Bob's identity key.
message sent
Step 7: Bob receives and decrypts
In Terminal 3 (Bob):
cargo run -p quicprochat-client -- recv \
--state bob.bin \
--server 127.0.0.1:7000
This command:
- Fetches all pending messages from the DS.
- For each message, calls
process_message()on Bob's MLS group, which decrypts thePrivateMessageand returns the plaintext.
[0] plaintext: Hello Bob, this is encrypted with MLS!
Step 8: Bob replies
In Terminal 3 (Bob):
cargo run -p quicprochat-client -- send \
--state bob.bin \
--peer-key <ALICE_KEY> \
--msg "Hi Alice, received loud and clear!" \
--server 127.0.0.1:7000
message sent
Step 9: Alice receives Bob's reply
In Terminal 2 (Alice):
cargo run -p quicprochat-client -- recv \
--state alice.bin \
--server 127.0.0.1:7000
[0] plaintext: Hi Alice, received loud and clear!
Automated demo (single command)
If you want to see the entire flow in a single command without managing three terminals, use the demo-group subcommand. This creates both Alice and Bob in-process with ephemeral identities and runs the full round-trip:
# Ensure the server is running, then:
cargo run -p quicprochat-client -- demo-group --server 127.0.0.1:7000
Alice -> Bob plaintext: hello bob
Bob -> Alice plaintext: hello alice
demo-group complete
What happened under the hood
Here is a summary of the cryptographic operations and network calls that occurred during this walkthrough:
| Step | Client | Crypto operation | Network RPC |
|---|---|---|---|
| 2 | Alice | Ed25519 keygen, MLS KeyPackage creation | uploadKeyPackage |
| 3 | Bob | Ed25519 keygen, MLS KeyPackage creation | uploadKeyPackage |
| 4a | Alice | MlsGroup::new_with_group_id (epoch 0) |
-- |
| 4b | Alice | MlsGroup::add_members (Commit + Welcome, epoch 0 -> 1) |
fetchKeyPackage, enqueue |
| 5 | Bob | MlsGroup::new_from_welcome (HPKE decrypt, epoch 1) |
fetch |
| 6 | Alice | MlsGroup::create_message (AES-128-GCM encrypt) |
enqueue |
| 7 | Bob | MlsGroup::process_message (AES-128-GCM decrypt) |
fetch |
| 8 | Bob | MlsGroup::create_message (AES-128-GCM encrypt) |
enqueue |
| 9 | Alice | MlsGroup::process_message (AES-128-GCM decrypt) |
fetch |
The MLS ciphersuite used throughout is MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519:
- DHKEM(X25519, HKDF-SHA256) for the HPKE key encapsulation in KeyPackages
- AES-128-GCM for symmetric encryption of application messages
- SHA-256 for the key schedule hash function
- Ed25519 for signing KeyPackages, Commits, and leaf nodes
Troubleshooting
join fails with "HPKE init key not found"
This happens when the key store file (.ks) was deleted or when join is run with a different --state path than register-state. The HPKE init private key generated during KeyPackage creation must be available at join time. Solution: use the same --state path for both register-state and join, and do not delete the .ks file between them.
invite fails with "server returned empty KeyPackage for peer"
The peer has not registered yet, or their KeyPackage was already consumed by a previous invite or fetch-key. Ask the peer to run register-state again to upload a fresh KeyPackage.
join fails with "no Welcome found in DS for this identity"
The Welcome message has not been enqueued yet (the inviter has not run invite), or it was already consumed by a previous join. Check that invite completed successfully before running join.
TLS verification fails
Ensure the client has access to the server's TLS certificate. By default, both server and client use data/server-cert.der. If the server regenerated its certificate (e.g., after deleting the data/ directory), clients must pick up the new certificate.
Next steps
- REPL Command Reference -- complete list of 40+ slash commands
- Rich Messaging -- reactions, typing indicators, edit/delete
- File Transfer -- chunked upload/download with SHA-256 verification
- Go SDK -- build Go applications against the qpc server
- TypeScript SDK & Browser Demo -- WASM crypto in the browser
- Mesh Networking -- P2P, broadcast channels, store-and-forward
- MLS (RFC 9420) -- how the MLS group operations work
- GroupMember Lifecycle -- internal state machine details
- Delivery Service Internals -- how the DS queues and delivers messages