diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_hub_api.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_hub_api.py | 66 |
1 files changed, 50 insertions, 16 deletions
diff --git a/packages/meshbay-hub/tests/test_hub_api.py b/packages/meshbay-hub/tests/test_hub_api.py index a8232c1..5a2cf86 100644 --- a/packages/meshbay-hub/tests/test_hub_api.py +++ b/packages/meshbay-hub/tests/test_hub_api.py @@ -24,6 +24,35 @@ def _gen_user_keys(): ) + +async def _announce_signed(client, token: str) -> tuple[str, str]: + """ + Announce a node with proof of possession (M8). + + The node key is independent of the user's identity key, so this mints a fresh + one and signs the domain-separated announce message with it. + """ + import base64 as _b64, time as _t + + me = await client.get("/v1/users/me", + headers={"Authorization": f"Bearer {token}"}) + user_id = me.json()["user_id"] + + sk_node = Ed25519PrivateKey.generate() + pk_node = pk_to_b64(sk_node.public_key()) + ts = _t.time().__trunc__() + msg = f"meshbay:node_announce:{user_id}:{pk_node}:{ts}".encode() + + r = await client.post("/v1/nodes/announce", json={ + "pk_node": pk_node, + "endpoint_hint": "1.2.3.4:19000", + "timestamp": ts, + "signature": _b64.b64encode(sk_node.sign(msg)).decode(), + }, headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 201, r.text + return r.json()["node_id"], pk_node + + # ── Hub info ────────────────────────────────────────────────────────────────── @pytest.mark.asyncio @@ -115,8 +144,11 @@ async def test_jwt_offline_verify(client, hub_key_path): hub_pk_pem = r_pk.json()["pk_hub_pem"].encode() decoded = pyjwt.decode(token, hub_pk_pem, algorithms=["EdDSA"]) - assert decoded["pk_user"] == pk_ed assert "jti" in decoded # mandatory + # The token carries no user key. It used to, and the node recorded it as the + # uploader's identity — so whoever issued tokens decided who could delete a + # file. The hub certifies accounts; nodes pin keys. + assert "pk_user" not in decoded @pytest.mark.asyncio @@ -165,11 +197,17 @@ async def test_refresh_token_rotation_old_rejected(client): @pytest.mark.asyncio async def test_get_user_pubkeys(client): + """ + The endpoint resolves an account; it is not a key directory any more. + + Publishing user identity keys is what finding H3 exploited — the invite flow + wrapped the group key for whatever came back. Keys are now generated per node + and pinned there, so there is nothing here to substitute. + """ pk_ed, pk_x, _ = _gen_user_keys() await client.post("/v1/users/register", json={ "username": "frank", "email": "frank@example.com", - "password": "frankpass99", - "pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x}) + "password": "frankpass99"}) login = await client.post("/v1/users/login", json={ "username": "frank", "password": "frankpass99"}) token = login.json()["access_token"] @@ -177,8 +215,10 @@ async def test_get_user_pubkeys(client): r = await client.get("/v1/users/frank/pubkeys", headers={"Authorization": f"Bearer {token}"}) assert r.status_code == 200 - assert r.json()["pk_ed25519"] == pk_ed - assert r.json()["pk_x25519"] == pk_x + body = r.json() + assert body["user_id"] and body["username"] == "frank" + assert "pk_ed25519" not in body, "user identity keys must not be published (H3)" + assert "pk_x25519" not in body, "user identity keys must not be published (H3)" # ── Nodes ───────────────────────────────────────────────────────────────────── @@ -195,15 +235,12 @@ async def test_announce_and_get_node(client): token = login.json()["access_token"] hdrs = {"Authorization": f"Bearer {token}"} - r = await client.post("/v1/nodes/announce", - json={"pk_node": pk_ed, "endpoint_hint": "1.2.3.4:19000"}, - headers=hdrs) - assert r.status_code == 201 - node_id = r.json()["node_id"] + node_id, pk_node = await _announce_signed(client, token) r2 = await client.get(f"/v1/nodes/{node_id}", headers=hdrs) assert r2.status_code == 200 - assert r2.json()["pk_node"] == pk_ed + # The node key is independent of the user identity key (M8). + assert r2.json()["pk_node"] == pk_node assert r2.json()["endpoint_hint"] == "1.2.3.4:19000" @@ -409,10 +446,7 @@ async def test_group_online_nodes(client): json={"username": "gn_user", "password": "gnpass999"})).json()["access_token"] # Announce a node - r = await client.post("/v1/nodes/announce", json={ - "pk_node": pk_ed, "endpoint_hint": "1.2.3.4:19000"}, - headers={"Authorization": f"Bearer {token}"}) - node_id = r.json()["node_id"] + node_id, pk_node = await _announce_signed(client, token) # No nodes online yet r = await client.get(f"/v1/groups/{group_id}/nodes", @@ -433,7 +467,7 @@ async def test_group_online_nodes(client): nodes = r.json()["nodes"] assert len(nodes) == 1 assert nodes[0]["node_id"] == node_id - assert nodes[0]["pk_node"] == pk_ed + assert nodes[0]["pk_node"] == pk_node finally: _connected_nodes.pop(node_id, None) _node_groups.pop(node_id, None) |