From 3f3c67a4aff7b800c271e88e2bc5e5294b010fb9 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 25 Sep 2026 17:43:28 +0200 Subject: feat(protocol): MNP 4.0 flag day for the node-audience token (B2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The node-audience token (previous commit) is a change to what a peer must present, so it is a MAJOR per the versioning rule (§5.6): a pre-4.0 client presents its hub session token and a 4.0 node refuses it, and there is no compatibility branch, because leaving one would keep a hub credential reachable by every node (C6's lesson). So the floor moves with the version. - MNP_VERSION 3.4 -> 4.0 and MNP_MIN_SUPPORTED 3.0 -> 4.0 (meshbay_common); transport.js MNP_V/MNP_V_MIN -> 4.0 to match. - MIN_CLIENT_VERSION 0.13.0 -> 0.16.0 so a stale desktop client is told to update before connecting rather than meeting a handshake refusal it cannot read; the browser reloads this build from the hub. - Regenerate tests/golden/dispatch.json: the only change is the `v` the node stamps on outbound messages, 3.4 -> 4.0 (56 cases, v field only). - Document the split and the flag day: MESHBAY_DESIGN.md §5.2 (the handshake token is the MNP-audience token), §5.6 (the 4.0 flag day), register E10 and decision 23; MESHBAY_NODE_PROTOCOL.md §6.3 (authorize_token binds MNP_AUD) and the wire-version banner. Deploy is coordinated and atomic (common+hub+node+SPA together); a live browser-to-node validation and the deploy itself remain. common (173), node (1489, the pre-existing test_cli_golden argparse/prog artifact aside) and hub (1471) suites all green. Co-Authored-By: Claude Opus 4.8 --- docs/MESHBAY_DESIGN.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) (limited to 'docs/MESHBAY_DESIGN.md') diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index c6d3cc7..394dfb5 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -16,7 +16,7 @@ > them — it names the invariant that holds today, not the incident that produced > it. §13 is the register of those labels. > -> Wire versions at the time of writing: **MNP 3.4** (oldest peer accepted 3.0), +> Wire versions at the time of writing: **MNP 4.0** (oldest peer accepted 4.0), > **MHP 0.1**, packages **0.16.0**. The normative source for the wire format is > `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol > serves, not its byte layout. @@ -992,12 +992,29 @@ already fails the key proof. Pinning covers the case where an attacker *holds* t group key — an ex-member, a leaked key — and swaps the node underneath, which the proof alone cannot distinguish from the genuine node. +**The token the member presents is a node-audience token, never the hub session +token (E10).** A member hands whatever it presents here to the node operator, +who is in the threat model, so the credential must open nothing at the hub. The +hub signs two audiences with its one key: a session token (`aud` = the hub API) +for `hubFetch` and signaling, and a short-lived **MNP token** (`aud = MNP_AUD`, +from `POST /v1/nodes/mnp-token`) that carries the member's `sub`, `groups` and +`jti` and is the only thing presented in the handshake. The node binds `MNP_AUD` +when it decodes, so a session token is refused here; the hub API binds its own +audience, so an MNP token captured by an operator is refused there. It is +checked once, before the proof, so its short life never interrupts a transfer or +a film already playing — a reconnect fetches a fresh one. `meshbay_common/tokens.py` +holds the two audience strings, shared by the hub that issues and the node that +verifies. + **Authorization rules:** - `group_id` is **mandatory**. Omitting it once skipped the membership check and fell back to the node's first group (**M1**). - `scope == "user"` is enforced by default; node-scoped daemon tokens are refused on the client path (**M9**, **NS7**). +- **The audience must be `MNP_AUD`.** A hub session token, or one with no + audience, is refused — a member's credential to a node is not a credential to + the hub (**E10**). - The denylist is consulted for user, `jti` **and** group. - The node **refuses connections when it holds no group key** — there is no `gek_required: false` bypass (**NS8**). @@ -1229,6 +1246,16 @@ client on reload; the desktop client ships its own UI, which is why connecting and says "this version can no longer connect" rather than showing a handshake refusal nobody can act on. +**MNP 4.0 (2026-09-25) is the worked example of a MAJOR that had to be one.** A +member now presents a node-audience token in the handshake, not its hub session +token (§5.2, **E10**), so a pre-4.0 client — which presents the session token — +cannot authenticate to a 4.0 node, and a 4.0 client cannot authenticate to a +pre-4.0 node, which expected the session token. Leaving a branch that still +accepted the session token would keep the very disclosure the change closes +reachable on every node, which is exactly what "no compatibility switch" forbids. +So the floor moved to 4.0, `client.minimum` moved to the release that carries the +new client, and the hub, node and SPA deploy together. + **Every *requirement* is true of every peer the client can reach.** The floor moves with each MAJOR, so `check_version` refuses at the handshake any peer that cannot meet one: an upload is sealed or it is not sent; a transfer has a real lease or it @@ -3210,6 +3237,7 @@ had already been asked. | **E7** | **Several named roots** per group, forming one virtual root (§6.2) | | **E8** | **exFAT/NTFS and Windows are the common case.** Linux ships first; that is build order, not population (§10) | | **E9** | **Group-related server state lives on the node. Always** (§1.3) | +| **E10** | **The credential a member presents to a node is not the one the hub API accepts.** A member hands its handshake token to the node operator, who is in the threat model, so it is a short-lived node-audience token (`aud = MNP_AUD`, §5.2), useless at the hub API; the session token (`aud` = hub API) is never disclosed to a node. Before this the member presented its hub session token, so an operator held a live hub credential for the member — enough to enumerate their other groups and act as them at the hub. Closed by the audience split and the MNP 4.0 flag day (§5.6) | | **O1** | Initial key setup in the pre-proof window — deferred; that window is where C4 and C5b came from | | **O2** | A LAN enrolment door — one endpoint, bounded window, one-time code, closing permanently on success | | **O3** | `device_policy {allow_bundle: false}`, signed by a pinned key — **the mechanism that actually closes C4** (§3.7) | @@ -3251,6 +3279,7 @@ had already been asked. 20. **Private keys never leave the device on native clients.** Qualified: a browser has no durable storage of its own and still needs a bundle on each node, so C4 closes for an *account* only when it opts out of browser use. 21. **Hub minimisation is enforced by an acceptance test, not by policy.** The hub must be *unable* to see keys, content or file listings. 22. **No new code exchanges between people.** Safety numbers are refused for identity verification, permanently. The device-linking code is between a person's own devices and is unaffected. The total user-visible cost of the whole authorship story is **one notice**: *"this account's key changed"*. +23. **A member's credential to a node is audience-separated from its hub credential** (**E10**, §5.2). The handshake carries a short-lived `aud = MNP_AUD` token, never the hub session token; the hub API accepts only its own audience. A node operator therefore holds nothing that acts at the hub. ### 14.2 Client architecture -- cgit v1.2.3