aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_hub_api.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_hub_api.py')
-rw-r--r--packages/meshbay-hub/tests/test_hub_api.py66
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)