summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_federation.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_federation.py')
-rw-r--r--packages/meshbay-hub/tests/test_federation.py179
1 files changed, 179 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_federation.py b/packages/meshbay-hub/tests/test_federation.py
new file mode 100644
index 0000000..6035b0c
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_federation.py
@@ -0,0 +1,179 @@
+"""
+MHP federation — what a registered peer hub may and may not do.
+
+A peer is trusted enough to advertise its own public groups into our directory
+and to withdraw them. It is not trusted to speak for a third hub, to shadow a
+local group, to revoke our users, or to replay a state-changing request.
+"""
+
+import base64
+import hashlib
+import time
+import uuid
+
+import jwt
+import pytest
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from meshbay_hub.api.deps import set_admin_usernames
+
+
+def _auth_key(password: str, username: str) -> str:
+ salt = hashlib.sha256(f"meshbay:auth:v1:{username}".encode()).digest()
+ return base64.b64encode(
+ hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 600_000, 32)).decode()
+
+
+async def _admin(client, username="root"):
+ pw = "a-long-enough-passphrase"
+ await client.post("/v1/users/register", json={
+ "username": username, "email": f"{username}@example.com",
+ "auth_key": _auth_key(pw, username)})
+ set_admin_usernames([username])
+ r = await client.post("/v1/users/login", json={
+ "username": username, "auth_key": _auth_key(pw, username)})
+ return {"Authorization": f"Bearer {r.json()['access_token']}"}
+
+
+class Peer:
+ def __init__(self, hub_id: str):
+ self.hub_id = hub_id
+ self._sk = Ed25519PrivateKey.generate()
+ self.pk_pem = self._sk.public_key().public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo).decode()
+
+ def _sk_pem(self) -> bytes:
+ return self._sk.private_bytes(
+ serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption())
+
+ def envelope(self, jti: str | None = None) -> str:
+ now = int(time.time())
+ return jwt.encode(
+ {"iss": self.hub_id, "sub": self.hub_id,
+ "jti": jti or str(uuid.uuid4()), "iat": now, "exp": now + 300},
+ self._sk_pem(), algorithm="EdDSA")
+
+ def revocation(self, target: str, target_id: str) -> str:
+ return jwt.encode(
+ {"type": "revocation", "target": target, "target_id": target_id,
+ "iss": self.hub_id, "iat": int(time.time())},
+ self._sk_pem(), algorithm="EdDSA")
+
+ def header(self, **kw) -> dict:
+ return {"Authorization": f"Bearer {self.envelope(**kw)}"}
+
+
+async def _register_peer(client, admin, peer: Peer):
+ r = await client.post("/mhp/peers", headers=admin, json={
+ "hub_id": peer.hub_id, "hub_url": f"https://{peer.hub_id}",
+ "pk_hub_pem": peer.pk_pem})
+ assert r.status_code == 201, r.text
+
+
+# ── receive_directory ──────────────────────────────────────────────────────
+
+@pytest.mark.asyncio
+async def test_unknown_peer_is_refused(client):
+ stranger = Peer("nobody.example")
+ r = await client.post("/mhp/directory", headers=stranger.header(),
+ json={"hub_id": "nobody.example", "groups": []})
+ assert r.status_code == 401
+
+
+@pytest.mark.asyncio
+async def test_source_hub_is_the_signer_not_the_body(client):
+ admin = await _admin(client)
+ peer = Peer("peer-a.example")
+ await _register_peer(client, admin, peer)
+
+ r = await client.post("/mhp/directory", headers=peer.header(), json={
+ "hub_id": "peer-b.example", # claims to relay another hub
+ "groups": [{"id": "g-1", "name": "Shared", "join_policy": "open"}]})
+ assert r.status_code == 202
+
+ listing = (await client.get("/v1/groups")).json()["groups"]
+ row = next(g for g in listing if g["id"] == "g-1")
+ assert row["source"] == "peer-a.example" # the signer, not "peer-b.example"
+
+
+@pytest.mark.asyncio
+async def test_a_federated_id_cannot_shadow_a_local_group(client):
+ admin = await _admin(client)
+ peer = Peer("peer-a.example")
+ await _register_peer(client, admin, peer)
+
+ owner = await _admin(client, "owner")
+ r = await client.post("/v1/groups", headers=owner, json={
+ "name": "mine", "visibility": "public", "join_policy": "open"})
+ local_id = r.json()["group_id"]
+
+ r = await client.post("/mhp/directory", headers=peer.header(), json={
+ "hub_id": peer.hub_id,
+ "groups": [{"id": local_id, "name": "evil twin", "join_policy": "open"}]})
+ assert r.status_code == 202
+ assert r.json()["accepted"] == 0
+
+
+@pytest.mark.asyncio
+async def test_a_state_changing_token_cannot_be_replayed(client):
+ admin = await _admin(client)
+ peer = Peer("peer-a.example")
+ await _register_peer(client, admin, peer)
+
+ env = peer.envelope(jti="fixed-jti")
+ h = {"Authorization": f"Bearer {env}"}
+ body = {"hub_id": peer.hub_id,
+ "groups": [{"id": "g-9", "name": "Once", "join_policy": "open"}]}
+
+ assert (await client.post("/mhp/directory", headers=h, json=body)).status_code == 202
+ assert (await client.post("/mhp/directory", headers=h, json=body)).status_code == 401
+
+
+# ── receive_revocation ─────────────────────────────────────────────────────
+
+@pytest.mark.asyncio
+async def test_a_peer_may_withdraw_its_own_group(client):
+ admin = await _admin(client)
+ peer = Peer("peer-a.example")
+ await _register_peer(client, admin, peer)
+
+ await client.post("/mhp/directory", headers=peer.header(), json={
+ "hub_id": peer.hub_id,
+ "groups": [{"id": "g-77", "name": "Bye", "join_policy": "open"}]})
+ assert any(g["id"] == "g-77" for g in (await client.get("/v1/groups")).json()["groups"])
+
+ r = await client.post("/mhp/revoke", headers=peer.header(),
+ json={"token": peer.revocation("group", "g-77")})
+ assert r.status_code == 202 and r.json()["pruned"] == 1
+ assert not any(g["id"] == "g-77" for g in (await client.get("/v1/groups")).json()["groups"])
+
+
+@pytest.mark.asyncio
+async def test_a_peer_cannot_withdraw_another_hubs_group(client):
+ admin = await _admin(client)
+ a, b = Peer("peer-a.example"), Peer("peer-b.example")
+ await _register_peer(client, admin, a)
+ await _register_peer(client, admin, b)
+
+ await client.post("/mhp/directory", headers=a.header(), json={
+ "hub_id": a.hub_id,
+ "groups": [{"id": "g-a", "name": "A's", "join_policy": "open"}]})
+
+ # b signs a revocation for a's group and presents it under b's envelope.
+ r = await client.post("/mhp/revoke", headers=b.header(),
+ json={"token": b.revocation("group", "g-a")})
+ assert r.status_code == 202 and r.json()["pruned"] == 0
+ assert any(g["id"] == "g-a" for g in (await client.get("/v1/groups")).json()["groups"])
+
+
+@pytest.mark.asyncio
+async def test_federation_cannot_revoke_a_user(client):
+ admin = await _admin(client)
+ peer = Peer("peer-a.example")
+ await _register_peer(client, admin, peer)
+
+ r = await client.post("/mhp/revoke", headers=peer.header(),
+ json={"token": peer.revocation("user", "some-user-id")})
+ assert r.status_code == 202 and r.json()["pruned"] == 0