Files
quicproquo/M3_STATUS.md

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:

  1. Delivery Service (DS) — store-and-forward relay for MLS messages (Cap'n Proto RPC on port 7001)
  2. MLS Group LifecycleGroupMember 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<IdentityKeypair>) -> Self
  • generate_key_package() -> Result<Vec<u8>, 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<Vec<u8>, CoreError> — returns TLS-encoded PrivateMessage
  • receive_message(bytes: &[u8]) -> Result<Option<Vec<u8>>, CoreError> — returns plaintext or None for Commit
  • group_id() -> Option<Vec<u8>>
  • 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<MlsMessageIn> 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<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:

  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.