summaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-03 16:16:55 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-03 16:16:55 +0200
commit675beed6ff688733a9598f9d82d41578f48316be (patch)
tree78dd4f8dff312f0ad99bd63bc679bf402591c5ed /CLAUDE.md
parent15087b0e8fdb872602310119f14680aaa443fd93 (diff)
downloadmeshbay-675beed6ff688733a9598f9d82d41578f48316be.tar.gz
feat!: MNP 1.0 — seal index and handshake_ack under the group key
`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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HkzbhmMmK8PqQBtGz5zCvY
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md33
1 files changed, 31 insertions, 2 deletions
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 = `"<msg_type>\|<group_id>"`. **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` |