diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 11:57:03 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 11:57:03 +0200 |
| commit | 5612dbbac41609b3f84784f57f1de538262db9a9 (patch) | |
| tree | 571e0bbd18c31be32ca33db35ca42f8aff672cc9 /docs | |
| parent | d3ad243c4ae3a273f623bd5fc631e3266aa4d0e4 (diff) | |
| download | meshbay-5612dbbac41609b3f84784f57f1de538262db9a9.tar.gz | |
fix(node): the roster, not the key alone, decides who gets a session
The handshake opened a session for anyone holding the group key with a hub
token naming the group; the roster was consulted only when wrapping the key
in a join. A member revoked or unpinned on the node but still a member on
the hub kept a full session with the key they already held — and was handed
the chat epoch their removal had just opened, since chat keys go to any
session. An honest client never met this (it asks for the key through
join_request every time); one that kept the key did not have to.
- After the proof, the node asks the roster and refuses with
`not_authorized_for_group` unless the account is an active member of the
group or the node's operator.
- A removal from any door — MNP, the node page, the CLI — now opens a new
chat epoch in each group the person could read, broadcasts it, and closes
every connection they hold (`ops.members._after_removal`). The CLI and the
node page did neither.
- Design §5.2, protocol §6.1, §6.3, §14.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 8 | ||||
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 9 |
2 files changed, 14 insertions, 3 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 78a3261..d2bec10 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -1046,6 +1046,14 @@ requester and it therefore grants nothing across accounts. - 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**). +- **The roster must admit the account for the group** before a session opens, + after the proof and whatever the token says (`not_authorized_for_group`). The + key proves possession, the token proves the hub's view of membership, and + neither is the node's own answer: without this, a member revoked or unpinned + here but still on the hub kept a full session with the key they held, and was + handed the chat epoch their removal had just opened. A removal, from any door + (MNP, the node page, the CLI), also opens a new chat epoch in each group the + person could read and closes every connection they hold. **Refusals carry a code**, not only a sentence, because a client can act on a code. `not_a_member` means the hub did not count the account a member when it minted the diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index 70047b2..47f508b 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -413,7 +413,8 @@ fails if a transport skips a step. |-------------------------------------------------------------->| | 8. rebuild binding; | | refuse if empty; | - | compare_digest(proof) | + | compare_digest(proof); | + | roster admits the user | | 9. session authenticated: | | frame limit -> 64 MiB, | | peer registry, audit | @@ -480,6 +481,7 @@ absent. `verify_proof` compares with `hmac.compare_digest`. | not on the denylist for `user_id`, `jti` **or** `group_id` | `Token revoked` | all three targets, and persisted to disk: a revocation that a restart forgets is not one | | `group_id ∈ token.groups` | `Not a member of this group`, code `not_a_member` | the membership check itself — a token is proof of an account, never of a group | | `group_id ∈ node.hosted_groups` | `Group not hosted on this node`, code `not_hosted` | the hub may hand a client several nodes for one group, and only some of them host it | +| *(after the proof)* the roster admits `sub` for `group_id` — an active member row, or the node-wide operator row | `This node has not admitted you to this group`, code `not_authorized_for_group` | the key proves possession and the token the hub's view; the node's own answer is the roster. Without it, someone revoked here but still a hub member kept a session with the key they held | `AuthorizedPeer` carries `user_id`, `group_id`, `username`, `jti` — and deliberately **no user public key**. `username` is read from a `username` claim that neither the MNP @@ -2294,8 +2296,9 @@ walks through the gate meant to stop it. filename and no path anywhere in them (§11.1a). * **Rotation is the only thing that removes access.** Revoking a member stops the node serving the next key; the current key and anything already downloaded stay readable. - A chat epoch is opened at the same time, which stops them reading what is said next — - not what was said before, which they could already read. + A chat epoch is opened at the same time, and their connections are closed and refused + from then on (the handshake consults the roster), which stops them reading what is + said next — not what was said before, which they could already read. --- |