diff options
Diffstat (limited to 'docs/MESHBAY_DESIGN.md')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 34 |
1 files changed, 30 insertions, 4 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 6e90402..78a3261 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -999,13 +999,21 @@ 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. +What pinning does not cover is a **second node** holding the key: the pin is per +node, and a node with its own identity is a first sight. A member's node is exactly +that — every member holds the group key — so what keeps it from being offered to +clients as the group's host is the hub's registration rule (§7.2, **AV32**), not the +pin. + **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` +from `POST /v1/nodes/mnp-token`) that carries the member's `sub`, `jti` and **the +one group the connection is for** — never the member's other groups, which the +operator it is handed to has no business learning — 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 and requires `exp`, `sub` and a known `scope`, so an MNP token captured by an operator is refused there, and so is any other hub-signed token that is not a @@ -1815,7 +1823,13 @@ account has and when each last signed in. Registration on the node socket requires a **node-scoped token**, verifies the node record against the token subject, and **derives group claims from the database**: a node may narrow the set to what it hosts but cannot widen it, and cannot displace a -live registration (**C2**). Narrowing goes all the way down: **an empty +live registration (**C2**). The ceiling is **the groups its account owns, plus those +whose owner approved that node** (`group_hosts`) — not the groups its account belongs +to. Every member holds the group key, so a member's node passes the handshake exactly +as the real host would, and clients keep the first registered node that completes it: +with membership as the ceiling, any member could stand in for the host. A node that +claims a group it may not host is recorded as a request, and the owner is notified +once and approves or refuses it from the group's settings (**AV32**). Narrowing goes all the way down: **an empty claim is a claim on nothing**, never on everything. Reading it as "all of this account's groups" made an unconfigured node a registered source for groups it could not serve — including other members' — and since `/v1/groups/{id}/nodes` answers in @@ -1924,7 +1938,17 @@ row and nothing else. Answering any authenticated account — as it did while on the public case was checked — hands whoever knows the group id the identities of the machines hosting it, and an ex-member knows that id for ever. Nothing needs it before joining: an open join writes the membership row first, and an -invitation registers the invitee's when the code is created. +invitation is accepted before the invitee's client asks for a node. + +**Being added to a group is an invitation, not a membership** (**AV33**). An owner +adding a username — from the Members tab or `meshbay-node member invite` — writes a +`group_invitations` row; the account becomes a member when it accepts on its home +page. Until then the group is not in its sidebar or Search, is named in none of its +tokens, and its nodes refuse it signaling like any non-member's. Without that step +any account could make any other account's client dial a node of its choosing — and +learn its address, and receive from it a first-join identity bundle. Redeeming an +invitation link, joining an open group and creating a group are the account's own +acts and still write the membership directly. ### 7.4 Instance policy @@ -3337,6 +3361,8 @@ 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 | +| **AV32** | **A node hosts a group because its owner said so, not because its account belongs to it** (§7.2). The claim ceiling was membership, and every member holds the key, so any member's node could register as a host and be the first one a client kept. Now: the owner's own nodes, plus nodes the owner approved; anything else is a pending request the owner sees | +| **AV33** | **Nobody is made a member without saying yes** (§7.3). An owner could add any username, and the account's client then listed the group, named it in its tokens and dialled its nodes. An addition is an invitation until accepted, and the MNP token names only the group it is minted for | | **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 |