feat: M2 + M3 — AuthService, MLS group lifecycle, Delivery Service
M2: - schemas/auth.capnp: AuthenticationService (upload/fetch KeyPackage) - noiseml-core: IdentityKeypair (Ed25519), generate_key_package, NoiseTransport with send_envelope/recv_envelope, Noise_XX handshake (initiator + responder) - noiseml-proto: auth_capnp module, ParsedEnvelope helpers - noiseml-server: AuthServiceImpl backed by DashMap queue (single-use KPs) - noiseml-client: register + fetch-key subcommands, ping over Noise_XX - tests: auth_service integration test (upload → fetch round-trip) M3: - schemas/delivery.capnp: DeliveryService (enqueue/fetch opaque payloads) - noiseml-core/group.rs: GroupMember — MLS group lifecycle create_group, add_member (→ Commit+Welcome), join_group, send_message, receive_message; uses openmls 0.5 public API (extract() not into_welcome, KeyPackageIn::validate() not From<KeyPackageIn>) - noiseml-server: DeliveryServiceImpl on port 7001 alongside AS on 7000 - noiseml-proto: delivery_capnp module TODO (see M3_STATUS.md): - noiseml-client: group subcommands (create-group, invite, join, send, recv) - noiseml-client/tests/mls_group.rs: full MLS round-trip integration test
This commit is contained in:
153
M3_STATUS.md
Normal file
153
M3_STATUS.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# M3 Implementation Status
|
||||
|
||||
**Last updated:** 2026-02-19
|
||||
**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)`.
|
||||
|
||||
### `noiseml-proto/build.rs` ✅
|
||||
Compiles `delivery.capnp` alongside `envelope.capnp` and `auth.capnp`.
|
||||
|
||||
### `noiseml-proto/src/lib.rs` ✅
|
||||
Exposes `pub mod delivery_capnp`.
|
||||
|
||||
### `noiseml-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()`
|
||||
|
||||
### `noiseml-core/src/lib.rs` ✅
|
||||
Exposes `pub use group::GroupMember`.
|
||||
|
||||
### `noiseml-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 `NOISEML_DS_LISTEN`).
|
||||
|
||||
---
|
||||
|
||||
## NOT YET DONE (continue tomorrow)
|
||||
|
||||
### 1. `noiseml-client/src/main.rs` — Group subcommands
|
||||
|
||||
Add these subcommands (note: need state persistence or a `demo` command approach):
|
||||
|
||||
**Recommended approach:** Add a `demo-group` subcommand that runs the full Alice-Bob MLS round-trip in a single process invocation against a live server. This avoids the `MlsGroup` serialization problem (openmls 0.5 MlsGroup state is hard to persist without the `serde` feature).
|
||||
|
||||
**Alternatively (with state file):** Enable `serde` feature on openmls in `Cargo.toml` and store `MlsGroup` state to disk. The workspace Cargo.toml uses `features = ["crypto-subtle"]` for openmls — add `"serde"` to that list.
|
||||
|
||||
Subcommands needed:
|
||||
- `create-group --as-server --ds-server --group-id <NAME>` — creates group, saves state
|
||||
- `invite --as-server --ds-server --peer-key <HEX>` — fetches peer KP from AS, creates Welcome, enqueues to DS
|
||||
- `join --ds-server` — fetches Welcome from DS, joins group, saves state
|
||||
- `send --ds-server --peer-key <HEX> --msg <TEXT>` — sends application message to DS
|
||||
- `recv --ds-server` — fetches and decrypts messages from DS
|
||||
|
||||
OR: just add `demo-group --server --ds-server` that does the whole flow.
|
||||
|
||||
### 2. `noiseml-client/tests/mls_group.rs` — Integration test
|
||||
|
||||
This is the PRIORITY for testing. The integration test should:
|
||||
|
||||
```rust
|
||||
// 1. Spawn server (AS on port X, DS on port Y) with tokio::process::Command
|
||||
// or by directly calling the server's accept loop in a LocalSet
|
||||
// 2. Alice: GroupMember::new, generate_key_package, upload to AS
|
||||
// 3. Bob: GroupMember::new, generate_key_package, upload to AS
|
||||
// 4. Alice: create_group, fetch Bob's KP from AS, add_member → (commit, welcome)
|
||||
// Alice: enqueue welcome for Bob via DS (recipient = bob's identity.public_key_bytes())
|
||||
// 5. Bob: fetch from DS, join_group(welcome)
|
||||
// 6. Alice: send_message(b"hello bob"), enqueue to DS
|
||||
// 7. Bob: fetch from DS, receive_message → assert plaintext == b"hello bob"
|
||||
// 8. Bob: send_message(b"hello alice"), enqueue to DS
|
||||
// 9. Alice: fetch from DS, receive_message → assert plaintext == b"hello alice"
|
||||
```
|
||||
|
||||
**Important:** For the integration test, you can bypass the CLI and use `GroupMember` + capnp-rpc client helpers directly.
|
||||
|
||||
Connect to DS (port 7001):
|
||||
```rust
|
||||
async fn connect_ds(server: &str, keypair: &NoiseKeypair) -> anyhow::Result<delivery_service::Client> {
|
||||
let stream = TcpStream::connect(server).await?;
|
||||
let transport = handshake_initiator(stream, keypair).await?;
|
||||
let (reader, writer) = transport.into_capnp_io();
|
||||
let network = twoparty::VatNetwork::new(reader.compat(), writer.compat_write(), Side::Client, Default::default());
|
||||
let mut rpc = RpcSystem::new(Box::new(network), None);
|
||||
let ds: delivery_service::Client = rpc.bootstrap(Side::Server);
|
||||
tokio::task::spawn_local(rpc);
|
||||
Ok(ds)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 noiseml-core # verify green
|
||||
```
|
||||
|
||||
Then:
|
||||
1. Write `crates/noiseml-client/tests/mls_group.rs` (integration test) — highest priority
|
||||
2. Add group subcommands to `crates/noiseml-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/noiseml-client/tests/auth_service.rs` (M2 test) for how to spin up the server and connect clients.
|
||||
Reference in New Issue
Block a user