aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/MESHBAY_DESIGN.md34
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md6
-rw-r--r--docs/USERGUIDE.md21
3 files changed, 50 insertions, 11 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
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md
index 2e5aca3..70047b2 100644
--- a/docs/MESHBAY_NODE_PROTOCOL.md
+++ b/docs/MESHBAY_NODE_PROTOCOL.md
@@ -489,8 +489,10 @@ would let whoever issues tokens decide it instead. Identity keys are pinned by t
node's roster. The hub certifies accounts, not keys.
`not_a_member` means the hub did not count this account a member of the group when it
-minted the token. The MNP token is minted for each connection, from the membership the
-hub holds at that moment, so a stale `groups` claim is no longer the usual cause; the
+minted the token. The MNP token is minted for each connection and names **only** that
+connection's group (`POST /v1/nodes/mnp-token {node_pk, group_id}`; `groups` is empty
+for a non-member), so the operator it is handed to learns nothing of the member's
+other groups. A stale `groups` claim is therefore no longer the usual cause; the
client still refreshes its session once and retries on that code before telling someone
who was just invited that they are not a member.
diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md
index fc9cf40..a802ae1 100644
--- a/docs/USERGUIDE.md
+++ b/docs/USERGUIDE.md
@@ -212,7 +212,9 @@ Someone who runs a node invites you. You need an account on the same hub first.
1. **They send you a code** — eight characters like `K7P2-9WQX`, by message,
mail, or read out loud. It is good for 7 days by default, works once, and
only for your account in that one group.
-2. **You sign in**, and the group is already in your sidebar.
+2. **You sign in**, and the group is waiting under **Invitations** on your
+ home page. **Accept** it — until you do, nothing connects to it — and it
+ joins your sidebar. **Decline** if you did not expect it.
3. **You open it.** It says *"This node needs to recognise you"*. Paste the
code.
4. Done — the machine hosting the group recognises you from now on, and the
@@ -566,10 +568,19 @@ meshbay-node member invite alice_dupont
Or the group's **Members** tab, from a paired browser.
-They need an account on the same hub first. The invitation registers their
-membership on the hub and produces a code that never goes near it. Send the
-code out of band; they enter it the first time they open the group. You do not
-need to be online then.
+They need an account on the same hub first. The invitation appears on their
+home page, where they accept it, and produces a code that never goes near the
+hub. Send the code out of band; they enter it the first time they open the
+group. You do not need to be online then.
+
+### Other nodes hosting your group
+
+Your own nodes serve your groups without asking. Any other node — a member's,
+say, offering a second copy — serves one of your groups only after you approve
+it: it appears in the group's settings under **Hosts**, and you are notified
+the first time it asks. **Approve** or **Refuse**; either can be changed later.
+A member's node holds the same group key as everyone, so a node you did not
+approve could otherwise present itself to your members as the group's host.
**Inviting someone who has no account yet** is a link: