From 675beed6ff688733a9598f9d82d41578f48316be Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 3 Sep 2026 16:16:55 +0200 Subject: feat!: MNP 1.0 — seal index and handshake_ack under the group key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `index_sync`, `index_delta` and the `handshake_ack` config payload now travel sealed under a GEK-derived subkey (`meshbay_common/groupbox.py`, mirrored by `sealGroup`/`openGroup` in `crypto.js`). Only `type`, `v`, `group_id` and the ack's `node_pk`/`proof`/`sig` stay in clear — a receiver must route and authenticate before it would trust a decryption. Verify, then decrypt. The ack line is integrity, not confidentiality: the signed handshake transcript names no ack field, so `is_node_admin`, `enabled_apps`, `video_root` and the rest were authenticated by the DTLS channel alone. The index line is defence in depth against a repeat of C1/C6 — a peer served before the handshake completes now gets ciphertext, not filenames. Nothing against an observer, the hub, or a member; that is the whole claim. `index_progress` stays clear (D3, counters only). Chat is out of scope. Failure is fatal: a payload that does not open ends the session naming the message type — never an empty index or an empty `enabled_apps`, both of which are legitimate states. Version negotiation ships here too (phase 15.6, brought forward): `v` + `v_min` on `handshake` and `handshake_challenge`, refused with `version_too_old` / `version_too_new` / `version_unreadable`. The flag day was already being paid for; the next breaking change now costs a refusal message. BREAKING CHANGE: breaks the WebRTC wire every deployed client speaks. Hub and every node must deploy together; the SPA is served by the hub, so a browser picks up the new client on reload. See MESHBAY_NODE_PROTOCOL.md §11.1a, §13.1. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HkzbhmMmK8PqQBtGz5zCvY --- CLAUDE.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) (limited to 'CLAUDE.md') diff --git a/CLAUDE.md b/CLAUDE.md index 0292a82..4a4175f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,12 @@ dans le bon `pyproject.toml`. - All three packages share the same version number (released together) ### Protocol versions (independent) -- MNP: `0.4` → bumped independently of package version +- MNP: `1.0` → bumped independently of package version. **`handshake.py`'s + `MNP_MIN_SUPPORTED` is the other half**: both peers declare `v` and `v_min` on + the handshake and refuse each other with a code (`version_too_old` / + `version_too_new` / `version_unreadable`), so a mismatch is a refusal rather + than a field that turns up missing. Shipped with 1.0 because that flag day was + already being paid for; the next breaking change now costs a refusal message - 0.2 added `PING`/`PONG` and backward chat paging (`before` / `has_more`). Additive, so an 0.1 peer still works: it sends no `before` and is answered with the newest page, which is what it wanted @@ -685,7 +690,29 @@ anything that assumes one key per person. `GroupSenderKeyStore.add_sender` currently does `self._states[dist.sender_id] = ...`, so a second device under the same `sender_id` silently overwrites the first. Revoking a device must rotate, like revoking a member. See `docs/devel-phases-next.md` §15.0b -- Chat is plaintext on the wire and at rest; the index is plaintext on the WebRTC path +- ~~Chat is plaintext on the wire and at rest; the index is plaintext on the WebRTC + path.~~ **The index half changed 2026-09-03 (MNP 1.0).** `index_sync`, + `index_delta` and the `handshake_ack` configuration payload are sealed under a + GEK-derived subkey (`meshbay_common/groupbox.py`, mirrored by `sealGroup`/ + `openGroup` in `crypto.js`); only `type`, `v`, `group_id` and the ack's own + `node_pk`/`proof`/`sig` stay in clear, because a receiver must route and + **authenticate** before it would trust a decryption. Chat is unchanged and out of + scope by decision. Read `MESHBAY_NODE_PROTOCOL.md` §11.1a before touching either + message. Three things to keep straight: + - **The ack line is integrity, not confidentiality.** `handshake_transcript` + names no ack field, so `is_node_admin`, `enabled_apps`, `video_root` and the + rest were authenticated by the DTLS channel alone. The AEAD tag comes from a + key the hub does not hold + - **The index line is defence in depth against our own next bug**, of a class + already shipped twice: C1 (node HTTP API served the index on `0.0.0.0` + unauthenticated) and C6 (TCP accepted a bare JWT with no GEK proof). It buys + nothing against an observer, the hub, or a member. That is the whole claim + - **A payload that does not open ends the session**, never a default: an + unopenable `enabled_apps` reads as "the operator disabled every app" and an + unopenable index as "the group is empty", both indistinguishable from + legitimate states. `index_progress` is deliberately *not* sealed (counters + only, every 2 s) — the reason lives next to the code, re-read it before + changing it ## Known calibration TODOs @@ -731,6 +758,8 @@ SFR residential Fedora 44 → meshbay.org OVH VPS: | Need | Module | File | |---|---|---| | Chunk encryption (prod) | `meshbay_common.crypto` | `crypto.py` | +| Sealing a payload under the GEK | `meshbay_common.groupbox` | `groupbox.py` + `sealGroup`/`openGroup` in `static/crypto.js`. `index_sync`, `index_delta`, `handshake_ack` — one envelope, purpose-separated subkeys, AAD = `"\|"`. **Never reuse `chunk_key_aes` with a pseudo-file for this** | +| MNP version range | `meshbay_common.handshake` | `MNP_MIN_SUPPORTED`, `check_version` — read by both servers and both clients | | Key derivation from password | `meshbay_common.keyderive` | `keyderive.py` | | Key bundle (web) | `meshbay_common.keyderive` | `keyderive.py` + `static/keyderive.js` | | GEK wrap/unwrap (ECIES) | `meshbay_common.crypto` | `crypto.py` | -- cgit v1.2.3