4.9 KiB
M3 Implementation Status
Last updated: 2026-02-20 Branch: feat/m1-noise-transport (all milestones on this branch so far)
What is M3?
M3 adds:
- Delivery Service (DS) — store-and-forward relay for MLS messages (Cap'n Proto RPC on port 7001)
- MLS Group Lifecycle —
GroupMemberstruct: create group, add member (Welcome), join group, send/receive encrypted application messages
Completed in M3
schemas/delivery.capnp ✅
Simple DS schema: enqueue(recipientKey, payload) + fetch(recipientKey) → List(Data).
quicnprotochat-proto/build.rs ✅
Compiles delivery.capnp alongside envelope.capnp and auth.capnp.
quicnprotochat-proto/src/lib.rs ✅
Exposes pub mod delivery_capnp.
quicnprotochat-core/src/group.rs ✅ (FULLY FIXED, ALL TESTS PASS)
GroupMember struct with methods:
new(identity: Arc<IdentityKeypair>) -> Selfgenerate_key_package() -> Result<Vec<u8>, CoreError>— TLS-encoded KeyPackage bytescreate_group(group_id: &[u8]) -> Result<(), CoreError>add_member(key_package_bytes: &[u8]) -> Result<(commit_bytes, welcome_bytes), CoreError>join_group(welcome_bytes: &[u8]) -> Result<(), CoreError>send_message(plaintext: &[u8]) -> Result<Vec<u8>, CoreError>— returns TLS-encoded PrivateMessagereceive_message(bytes: &[u8]) -> Result<Option<Vec<u8>>, CoreError>— returns plaintext or None for Commitgroup_id() -> Option<Vec<u8>>identity() -> &IdentityKeypair
openmls 0.5 API gotchas resolved:
KeyPackageonly hasTlsSerialize, notTlsDeserialize→ useKeyPackageIn::tls_deserialize(...)?.validate(backend.crypto(), ProtocolVersion::Mls10)?MlsMessageIn::into_welcome()is#[cfg(any(feature = "test-utils", test))]→ usematch msg_in.extract() { MlsMessageInBody::Welcome(w) => w, ... }MlsMessageIn::into_protocol_message()is similarly feature-gated → usematch msg_in.extract() { MlsMessageInBody::PrivateMessage(m) => ProtocolMessage::PrivateMessage(m), MlsMessageInBody::PublicMessage(m) => ProtocolMessage::PublicMessage(m), ... }From<MlsMessageIn> for ProtocolMessageis also feature-gated- Must use
OpenMlsCryptoProvidertrait in scope forbackend.crypto()
quicnprotochat-core/src/lib.rs ✅
Exposes pub use group::GroupMember.
quicnprotochat-server/src/main.rs ✅
Two listeners on one LocalSet:
- Port 7000 (AS):
AuthServiceImpl— unchanged from M2 - Port 7001 (DS):
DeliveryServiceImpl— new; usesDashMap<Vec<u8>, VecDeque<Vec<u8>>>keyed by Ed25519 public key
New CLI flag: --ds-listen (default 0.0.0.0:7001, env QUICNPROTOCHAT_DS_LISTEN).
quicnprotochat-client/src/main.rs ✅
Added demo-group subcommand to exercise the full Alice↔Bob MLS flow against live AS (7000) and DS (7001): uploads both KeyPackages, delivers Welcome via DS, and exchanges application messages.
quicnprotochat-client/tests ✅
cargo test -p quicnprotochat-client --tests passes, including the MLS round-trip integration test.
Notes
Open question (future work): if we need persistent groups instead of ephemeral demo runs, enable openmls serde feature and add statefile-backed subcommands (create-group, invite, join, send, recv). For M3, the demo path is sufficient.
Key Design Decisions
DS Port (7001) vs same port
The server uses two separate listeners (7000 for AS, 7001 for DS) because capnp-rpc supports only one bootstrap capability per connection. No new schema was needed.
GroupMember lifecycle (CRITICAL)
The OpenMlsRustCrypto backend holds the HPKE init private key in memory. The same GroupMember instance must be used from generate_key_package() through join_group(). Do NOT create a new GroupMember between these calls.
KeyPackage wire format
GroupMember::generate_key_package() returns raw TLS-encoded KeyPackage bytes (NOT wrapped in MlsMessageOut). This is the same format as the standalone generate_key_package() function used in M2 tests. The AS stores these raw bytes.
When adding a member, add_member() deserializes via KeyPackageIn::tls_deserialize(...)?.validate(...).
Test Results (all passing)
test codec::tests::* ... ok (5 tests)
test keypair::tests::* ... ok (3 tests)
test group::tests::two_party_mls_round_trip ... ok
test group::tests::group_id_lifecycle ... ok
How to continue tomorrow
cd /home/c/projects/poc-mes
git log --oneline -5 # see where we are
cargo test -p quicnprotochat-core # verify green
Then:
- Write
crates/quicnprotochat-client/tests/mls_group.rs(integration test) — highest priority - Add group subcommands to
crates/quicnprotochat-client/src/main.rs
The integration test is the most important piece — it proves the full M3 stack works end-to-end.
For the test, see the pattern in crates/quicnprotochat-client/tests/auth_service.rs (M2 test) for how to spin up the server and connect clients.