# 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: 1. **Delivery Service (DS)** — store-and-forward relay for MLS messages (Cap'n Proto RPC on port 7001) 2. **MLS Group Lifecycle** — `GroupMember` struct: 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) -> Self` - `generate_key_package() -> Result, CoreError>` — TLS-encoded KeyPackage bytes - `create_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, CoreError>` — returns TLS-encoded PrivateMessage - `receive_message(bytes: &[u8]) -> Result>, CoreError>` — returns plaintext or None for Commit - `group_id() -> Option>` - `identity() -> &IdentityKeypair` **openmls 0.5 API gotchas resolved:** - `KeyPackage` only has `TlsSerialize`, not `TlsDeserialize` → use `KeyPackageIn::tls_deserialize(...)?.validate(backend.crypto(), ProtocolVersion::Mls10)?` - `MlsMessageIn::into_welcome()` is `#[cfg(any(feature = "test-utils", test))]` → use `match msg_in.extract() { MlsMessageInBody::Welcome(w) => w, ... }` - `MlsMessageIn::into_protocol_message()` is similarly feature-gated → use `match msg_in.extract() { MlsMessageInBody::PrivateMessage(m) => ProtocolMessage::PrivateMessage(m), MlsMessageInBody::PublicMessage(m) => ProtocolMessage::PublicMessage(m), ... }` - `From for ProtocolMessage` is also feature-gated - Must use `OpenMlsCryptoProvider` trait in scope for `backend.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; uses `DashMap, VecDeque>>` 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 ```bash cd /home/c/projects/poc-mes git log --oneline -5 # see where we are cargo test -p quicnprotochat-core # verify green ``` Then: 1. Write `crates/quicnprotochat-client/tests/mls_group.rs` (integration test) — highest priority 2. 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.