summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_node_status.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-13 16:29:36 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-13 16:29:36 +0200
commit9da3cfc5d31bc4e1b9c4ea70f56e07b9aed8bb85 (patch)
tree3a3164422547658b753aece6a10b9ecb47668f6a /packages/meshbay-node/tests/test_node_status.py
parent6a3f927413d4fd44b708a306e5e053f6660ec357 (diff)
downloadmeshbay-9da3cfc5d31bc4e1b9c4ea70f56e07b9aed8bb85.tar.gz
fix(node): the node's own controls take no authority from a hub token
`_is_node_admin()` is `self._user_id == node_user_id`, and `_user_id` is the `sub` of a JWT the hub issued. Six node-wide controls were gated on that alone: `node_status` — which lists every group on the machine with each root's **absolute path** — plus `node_settings_set`, `roster_read`, `denylist_read`, `denylist_clear` and `node_reload`. So the answer to "are you the operator of this node" was "the hub says so", which NS4 and M3 rule out in as many words: operator authority comes from the node's roster and from nowhere else, and asking the hub is how the hub installs itself as node administrator. The reach is bounded — a completed handshake also needs the group key — but an active hub obtains one legitimately in an open-join group, which §3.5 concedes, and from there it could read the operator's directory layout or clear the denylist, which is the persisted revocation H4 exists to keep. `_operator_device()` requires both halves now: the account is the one the node belongs to, *and* the device on this connection has proved a key the roster holds as an operator. `device_hello` is signed over a transcript naming the node, the group and this connection's nonce, and `operator_pks()` is rebuilt from the roster on each call, so an unpinned browser and a revoked one are both refused at once. The hub holds no user keys and cannot countersign a device. Keeping the account check as well is deliberate: dropping it would widen these node-wide controls to any paired operator of any group on the machine, which is a separate decision. `_is_node_admin()` stays as what it is in the handshake ack — a hint telling a client whether to offer the Node page — and says so. Nothing changes for a paired operator: `device_hello` runs unconditionally after the ack, and anyone using the Node page's controls is already paired, since `root_add` and every other signed op has always verified against `operator_pks()`. A browser that never paired now reads nothing there, which is the state in which it could already write nothing. test_node_status.py's fixture set the account and not the device, which is how it went on passing; it now wires the device the way `device_hello` leaves it. The adversary itself is in test_security_regressions.py — a token naming the owner's account with no proved device, which the previous source answered with `node_status_ack`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMxEQadpzPkYLFf5CYKhpW
Diffstat (limited to 'packages/meshbay-node/tests/test_node_status.py')
-rw-r--r--packages/meshbay-node/tests/test_node_status.py7
1 files changed, 7 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_node_status.py b/packages/meshbay-node/tests/test_node_status.py
index 091b1db..897be33 100644
--- a/packages/meshbay-node/tests/test_node_status.py
+++ b/packages/meshbay-node/tests/test_node_status.py
@@ -110,6 +110,13 @@ async def _session(
session._group_id = GROUP
session._user_id = "grenet" if operator else "mallory"
session._username = session._user_id
+ # The device on the connection, as `device_hello` leaves it. Node-wide
+ # controls are gated on a key the node pinned as an operator and *proved*
+ # on this connection, not on the account id in the token — a hub can choose
+ # the second and cannot produce the first (NS4, M3). A fixture that sets
+ # only the account tests a check that is no longer the check.
+ session._pinned_pk = pk_op if operator else ""
+ session._device_confirmed = bool(operator)
session._pk_user = ""
session._uploads = {}
session._admin_ops = {}