From 4b3e8c3b8b9d10c8ac333dd8db614a7569052472 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 10 Aug 2026 03:07:56 +0200 Subject: feat: Phase 7 — Node v2 (multi-group, Sender Keys, 0-RTT, chat, denylist) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements all 8 milestones (7.0-7.7): - 7.0: JWT carries `groups` claim; node verifies group membership at MNP handshake (QUIC + TCP+TLS). Resolves security review C2. - 7.1: QUIC 0-RTT session resumption via stored session tickets (17-21ms reconnect vs 47ms cold). - 7.2: Hub→node WebSocket signaling for NAT punch coordination (`client_incoming`/`punch_ready`) + jti denylist push. Denylist class blocks revoked users/jtis at handshake. - 7.3: Multi-group daemon — one QUIC port serves N groups with per-group GEK, shared_root, and index routing. - 7.4: HLS streaming via QUIC (STREAM_SEGMENT message type, ffmpeg segment extraction). - 7.5: Sender Keys protocol for group chat (Signal Groups approach). Each member has own sending chain key, HKDF chain ratchet, AES-256-GCM encryption, Ed25519 signing. Resolves security review C1. - 7.6: Chat store (SQLite via aiosqlite), CHAT_MESSAGE MNP wire type with peer broadcast, web UI with WebSocket push. - 7.7: Argon2id calibration CLI. First security review included (first-review.md). 109 tests, demo-v3 validated against meshbay.org production hub. Co-Authored-By: Claude Opus 4.6 --- docs/meshbay-draft-v3.md | 58 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 17 deletions(-) (limited to 'docs') diff --git a/docs/meshbay-draft-v3.md b/docs/meshbay-draft-v3.md index e11200c..97c391d 100644 --- a/docs/meshbay-draft-v3.md +++ b/docs/meshbay-draft-v3.md @@ -97,7 +97,7 @@ Email is kept in full (not hashed) to support: Phone number: optional, associable after account creation. On Android, both collected at registration. Accounts are fusionable (email + phone pointing to same account). -Email and phone are stored encrypted at rest in the database. +Email and phone are stored encrypted at rest in the database, using a server-side key derived from the hub's configuration secret (not the database). **[NOT YET IMPLEMENTED — currently stored in plaintext. Tracked as open question #10.]** #### 4.1.2 Mandatory IP Logging (Legal Compliance) @@ -140,7 +140,8 @@ Two tokens issued at login: **Access token** (JWT, signed Ed25519): - Validity: 1 hour -- Payload: `jti` (UUID4, **mandatory** — unique per token, enables individual revocation and prevents replay), `user_id`, `PK_user`, `PK_user_x25519`, `hub_id`, `issued_at`, `expires_at`, hub-signed groups membership claim +- Payload: `jti` (UUID4, **mandatory** — unique per token, enables individual revocation and prevents replay), `user_id`, `PK_user`, `PK_user_x25519`, `hub_id`, `issued_at`, `expires_at`, `groups` (list of group_ids the user is a member of — hub-signed membership claim) +- The `groups` claim is **mandatory** for node-side authorization: the node checks that the requested group_id appears in the JWT before serving any content. Without this claim, any authenticated user could access any group on the node. - Presented to nodes for authentication and group access verification - Verified locally by nodes using the hub's known public key — no hub roundtrip - Compromise window: 1 hour maximum @@ -237,6 +238,11 @@ in the client JWT. The server routes each connection to the appropriate DirectoryIndexer and GEK after JWT verification. Rationale: one NAT hole to maintain, one port to forward manually if needed. +**Authorization invariant:** the node MUST verify that the JWT's `groups` claim +contains the requested group_id before serving any content. Without this check, +any authenticated user could access any group on the node. This is enforced at +the MNP handshake layer, not the transport layer. + **Platform:** Linux primary, cross-platform from the start (Windows/macOS). Python ensures portability. #### 4.2.1 Keystore and Unlock @@ -512,7 +518,7 @@ Disk (plaintext) → zstd compress → GEK encrypt (per-chunk) → TCP+TLS 1.3 s **Chunking:** - Chunk size: 1 MB (amortizes AEAD overhead; enables seeking) -- Per-chunk key derivation: `chunk_key = HKDF(GEK, salt="file:" || blake3(file) || "chunk:" || index)` +- Per-chunk key derivation: `chunk_key = HKDF(GEK, salt=None, info="file:" || blake3(file) || ":chunk:" || index)` — salt is omitted because the GEK is a CSPRNG output (already uniform); the file/chunk context goes in `info` for domain separation, which is the correct HKDF usage per RFC 5869 - Each chunk independently decryptable → enables VOD seeking - Compress before encrypt (compression is ineffective on ciphertext) @@ -600,11 +606,21 @@ Group chat is a **core feature** (not an extension module). - **Push/pull:** connected members get real-time push (WebSocket); offline members pull history on reconnect - **Retention:** managed by the group admin (no automatic expiry) -**Encryption:** Double Ratchet algorithm (implemented in `meshbay_common.ratchet`): -- Forward secrecy and break-in recovery per message -- Each message independently encrypted, out-of-order delivery handled -- Attachment files: encrypted with GEK-derived key (same as file chunks), hash in message -- Group scope: all members share the same ratchet state seeded from the group GEK +**Encryption — Sender Keys protocol (decided in first security review, 2026-08-10):** + +The Double Ratchet (implemented in `meshbay_common.ratchet`) is a **pairwise** (1:1) protocol. Using a shared ratchet state for N group members would cause chain key desynchronization and nonce/key reuse — a catastrophic AEAD failure. The architecture uses **Sender Keys** instead (same approach as Signal Groups): + +- Each group member generates a **sender key** (random symmetric chain key + signing keypair) +- On joining a group, the new member's sender key is distributed to all existing members via pairwise channels (GEK-wrapped or direct) +- Each existing member sends their current sender key to the new member +- Messages are encrypted with the sender's chain key (symmetric ratchet, one direction) +- Forward secrecy at **member rotation** granularity: when a member is removed, all remaining members rotate their sender keys +- O(N) state per member (one chain per group member), not O(N^2) +- The existing Double Ratchet implementation is kept for future 1:1 direct messaging + +Attachment files: encrypted with GEK-derived key (same as file chunks), hash referenced in the message. + +> **Why not MLS (RFC 9420)?** MLS provides O(log N) message overhead and per-message forward secrecy via tree-based ratcheting. It is the superior long-term choice, but its complexity is not justified for v1 group sizes (< 50 members). Sender Keys is proven at scale (Signal, WhatsApp) and simpler to implement. Migration to MLS is a v2 option if group sizes grow. --- @@ -854,16 +870,24 @@ The node loads extension modules (Python) in a sandboxed subprocess. **Chat is a | R6 | Hub API: which endpoints for GEK distribution? | `GET /v1/users/{username}/pubkeys`, `POST /v1/groups`, `POST /v1/groups/{group_id}/members/{username}/gek`, `GET /v1/groups/{group_id}/gek` | Spike 6 | | R7 | Package structure? | 3 packages: `meshbay-common`, `meshbay-hub`, `meshbay-node`. uv workspace monorepo. | POC | +**Resolved by first security review (2026-08-10):** + +| # | Question | Resolution | Source | +|---|---|---|---| +| R8 | Group chat encryption model? | Sender Keys protocol (Signal Groups approach). Double Ratchet kept for future 1:1 DM only. MLS considered for v2 if groups > 50 members. | Security review C1 | +| R9 | Token denylist distribution? | Push via existing hub→node WebSocket. Node maintains an in-memory jti set. MNP handshake checks the set before accepting a JWT. No periodic polling needed. | Security review S3 | +| R10 | Chunk key HKDF: salt or info? | `info` (domain separation), `salt=None`. GEK is CSPRNG output (already uniform), so HKDF extract step doesn't need a random salt. Spec wording corrected to match code (RFC 5869 compliant). | Security review M5 | +| R11 | AES-GCM keystore IV size? | 96-bit (12 bytes), per NIST SP 800-38D recommendation. Code fixed from 128-bit to 96-bit. | Security review S4 | + **Still open:** 1. **Refresh token validity:** 30 or 90 days? 2. **Group address scheme:** final URL format confirmation -3. **Double Ratchet library:** identify best Python implementation (evaluate `python-doubleratchet`, `axolotl`, or custom) -4. **GEK bundle location for groups with mixed access** (public-restricted): hub or node? -5. **MHP federation sync frequency and conflict resolution** -6. **Hub mirror replication strategy** (when implemented) -7. **Chat attachment storage:** stored on node like regular files, or separate store? -8. **Relay registration protocol design** (when implemented) -9. **Token denylist distribution:** how do nodes fetch and cache the `jti` denylist? Push (hub WebSocket) or pull (periodic poll)? Cache TTL? -10. **QUIC migration timeline:** when is the application protocol considered stable enough to begin v2 transport implementation? -11. **Port configuration conflict:** `18000` used for both local web UI and (in some proposals) MNP listener — needs final port allocation decision (proposed split: 18000 for web UI, 18001 for MNP). +3. **GEK bundle location for groups with mixed access** (public-restricted): hub or node? +4. **MHP federation sync frequency and conflict resolution** +5. **Hub mirror replication strategy** (when implemented) +6. **Chat attachment storage:** stored on node like regular files, or separate store? +7. **Relay registration protocol design** (when implemented) +8. **QUIC migration timeline:** when is the application protocol considered stable enough to begin v2 transport implementation? +9. **Refresh token rotation:** implement one-time-use refresh tokens (rotate on each use, detect reuse as theft indicator). RFC 6819 §5.2.2.3. +10. **Email encryption at rest:** spec requires encrypted email/phone in DB, implementation stores plaintext. Needs server-side encryption with key from hub config. -- cgit v1.2.3