From a421a03d2be16670dc8d9076d26f4a7eac669986 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 28 Sep 2026 21:14:10 +0200 Subject: fix: bound pending admin challenges and sign every value an op acts on Any member could make a node hold unbounded challenge requests; a connection now keeps at most 8, 64 KiB each. root_add, group_attach, invite_create and tmdb_config signed less than they did; their subjects are now canonical JSON of every value (the TMDB token by SHA-256). MNP 5.0, floor kept at 4.0. Co-Authored-By: Claude Opus 5.5 --- docs/MESHBAY_DESIGN.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'docs/MESHBAY_DESIGN.md') diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 6daa778..07aef05 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 4.0** (oldest peer accepted 4.0), +> Wire versions at the time of writing: **MNP 5.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. @@ -99,7 +99,7 @@ opens them. │ └─────────┘ MHP 0.1 │ signalling (SDP/ICE, <1 KB), presence, revocation push MHP │ - ┌────┴────┐ MNP 4.0 ┌──────────┐ + ┌────┴────┐ MNP 5.0 ┌──────────┐ │ node │◄──────── WebRTC DataChannel / QUIC ──────────►│ client │ └─────────┘ index, file chunks, streams, chat, admin └──────────┘ holds the files browser SPA or desktop @@ -1070,7 +1070,15 @@ TTL 120 s. **The client reconstructs the transcript from announced fields and refuses to sign if the operation or subject is not what the user asked for** (**H5**) — a challenge of opaque random bytes signed blind is an unbound signing oracle. The transcript's subject names the *outcome*, not the operation: what the -operator is shown before signing has to be what happens. +operator is shown before signing has to be what happens. **Every value the node acts +on is in the subject**: the signature covers nothing else of the request, so an +operation whose effect is several values signs all of them as canonical JSON — a +root's path *and* whether every member may write there, a group's name *and* the +directory it exposes — and a secret by its SHA-256, since the subject is audited. + +What waits for a signature is bounded: anyone authenticated can ask for a challenge, +so a connection holds at most eight pending, each at most 64 KiB, expired ones +dropped (**AV31**). Verification is against `roster.operator_pks()`, rebuilt from node state, **never** from anything in the response. @@ -1285,10 +1293,11 @@ checks the version its peer declared and **branches on none of it**. > which point it silently takes the other. **A field kept "just in case" is how > the branches come back.** -**The floor is not necessarily the current version, and MINOR additions are why.** -It is `MNP_MIN_SUPPORTED` in `handshake.py` and it equals the last MAJOR. Today the -two coincide at 4.0, but 3.1, 3.2, 3.3 and 3.4 were each added above the 3.0 floor -without moving it, and the next MINOR will be added above 4.0 the same way. So a +**The floor is not necessarily the current version, and what is added above it is why.** +It is `MNP_MIN_SUPPORTED` in `handshake.py`, and it is the last MAJOR that had to +refuse at the handshake: 3.1–3.4 were added above the 3.0 floor without moving it, +and 5.0, a MAJOR confined to four signed operations that a peer across the break +refuses to sign, sits above the 4.0 floor. So a peer can be reachable and still not do something the current version can, and the client has to cope with that — **by reading the peer's own answer, never by comparing version numbers**. @@ -3270,6 +3279,7 @@ had already been asked. | **AV29** | **An invitation link is bounded on both halves and its mail on the sender** (§3.4, §7.3). Twenty outstanding per group on the node (bearer codes) and on the hub (tickets); and because a link mail reaches an address the hub has no relationship with, at the request of anyone who owns a group, it is counted **per sending account per day** (`mail.invite_link_daily_cap`, 10), under the recipient and instance bounds and outside the recovery reserve (`invite_link` is not a recovery purpose) | | **AV28** | **How many node keys one account may announce is bounded** (§7.2). Each is a row plus an IP-log row under a one-year retention, so an account in a loop writes a year of storage on the operator's disk having paid only for signatures. Proof of possession (**M8**) settles whose key it is and not how many. Counted only where a row is added: re-announcing a key already held keeps working at the ceiling, or a node that reached it could never refresh its address again | | **AV30** | **What one member's offers cost a node is bounded per account and per node, and the bound admits the heaviest ordinary account** (§7.2). Each offer makes the node allocate a peer connection. A budget of 120 per node refilled at two a second bounds a member there without touching their other nodes, and it is counted by account because a mobile carrier shares one IPv4 address among many subscribers. Pending offers are capped at 32 per account. Both refusals carry `Retry-After` and the client retries them, because a refused offer otherwise reads as a node that is down | +| **AV31** | **What waits for a signature is bounded** (§5.4). Any authenticated member can ask for an admin challenge, since the signature is checked afterwards, and a pending challenge kept its whole request until answered — measured, 200 requests of 1 MiB held 400 MiB for the life of one connection. At most eight pending per connection, 64 KiB each, expired ones dropped | ### 13.6 Chat design findings -- cgit v1.2.3