aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/golden/dispatch.json128
-rw-r--r--packages/meshbay-node/tests/test_admin_challenge_bounds.py135
-rw-r--r--packages/meshbay-node/tests/test_tmdb_config_policy.py13
-rw-r--r--packages/meshbay-node/tests/test_webrtc_transport.py4
4 files changed, 211 insertions, 69 deletions
diff --git a/packages/meshbay-node/tests/golden/dispatch.json b/packages/meshbay-node/tests/golden/dispatch.json
index 4767996..a7bb543 100644
--- a/packages/meshbay-node/tests/golden/dispatch.json
+++ b/packages/meshbay-node/tests/golden/dispatch.json
@@ -2068,7 +2068,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2391,7 +2391,7 @@
"subject": "gggggggggggggggggggggggggggggggg",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2410,7 +2410,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2429,7 +2429,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2448,7 +2448,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2718,7 +2718,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2732,7 +2732,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2746,7 +2746,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2760,7 +2760,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2774,7 +2774,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2788,7 +2788,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2802,7 +2802,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -2816,7 +2816,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -8535,7 +8535,7 @@
"subject": "gggggggggggggggggggggggggggggggg",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -8554,7 +8554,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -8573,7 +8573,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -8592,7 +8592,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -8924,10 +8924,10 @@
"op": "group_attach",
"op_id": "<volatile>",
"req_id": 4242,
- "subject": "['x']",
+ "subject": "{\"name\":\"['x']\",\"shared_dir\":\"['x']\",\"writable\":true}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -8943,10 +8943,10 @@
"op": "group_attach",
"op_id": "<volatile>",
"req_id": 4242,
- "subject": "7",
+ "subject": "{\"name\":\"7\",\"shared_dir\":\"7\",\"writable\":true}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -8962,10 +8962,10 @@
"op": "group_attach",
"op_id": "<volatile>",
"req_id": 4242,
- "subject": "x",
+ "subject": "{\"name\":\"x\",\"shared_dir\":\"x\",\"writable\":true}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -9300,7 +9300,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -9319,7 +9319,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -9338,7 +9338,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -11641,7 +11641,7 @@
"subject": "link:gggggggggggggggggggggggggggggggg",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -13740,7 +13740,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -13759,7 +13759,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -13778,7 +13778,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -14113,7 +14113,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -14132,7 +14132,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -14151,7 +14151,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16212,7 +16212,7 @@
"req_id": 4242,
"token": null,
"type": "pong",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16227,7 +16227,7 @@
"x"
],
"type": "pong",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16240,7 +16240,7 @@
"req_id": 4242,
"token": 7,
"type": "pong",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16253,7 +16253,7 @@
"req_id": 4242,
"token": "x",
"type": "pong",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16266,7 +16266,7 @@
"req_id": 4242,
"token": null,
"type": "pong",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16281,7 +16281,7 @@
"x"
],
"type": "pong",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16294,7 +16294,7 @@
"req_id": 4242,
"token": 7,
"type": "pong",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16307,7 +16307,7 @@
"req_id": 4242,
"token": "x",
"type": "pong",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16639,10 +16639,10 @@
"op": "root_add",
"op_id": "<volatile>",
"req_id": 4242,
- "subject": "['x']",
+ "subject": "{\"kind\":\"['x']\",\"name\":\"['x']\",\"path\":\"['x']\",\"removable\":true,\"writable\":true}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16658,10 +16658,10 @@
"op": "root_add",
"op_id": "<volatile>",
"req_id": 4242,
- "subject": "7",
+ "subject": "{\"kind\":\"7\",\"name\":\"7\",\"path\":\"7\",\"removable\":true,\"writable\":true}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -16677,10 +16677,10 @@
"op": "root_add",
"op_id": "<volatile>",
"req_id": 4242,
- "subject": "x",
+ "subject": "{\"kind\":\"x\",\"name\":\"x\",\"path\":\"x\",\"removable\":true,\"writable\":true}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17015,7 +17015,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17034,7 +17034,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17053,7 +17053,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17388,7 +17388,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17407,7 +17407,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17426,7 +17426,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17761,7 +17761,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17780,7 +17780,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -17799,7 +17799,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -18134,7 +18134,7 @@
"subject": "['x']:rw=on,rem=on",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -18153,7 +18153,7 @@
"subject": "7:rw=on,rem=on",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -18172,7 +18172,7 @@
"subject": "x:rw=on,rem=on",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -21116,10 +21116,10 @@
"op": "tmdb_config",
"op_id": "<volatile>",
"req_id": 4242,
- "subject": "custom_token=no,language=default",
+ "subject": "{\"language\":null,\"token\":null}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -21159,10 +21159,10 @@
"op": "tmdb_config",
"op_id": "<volatile>",
"req_id": 4242,
- "subject": "custom_token=yes,language=x",
+ "subject": "{\"language\":\"x\",\"token\":\"sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881\"}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
@@ -23049,7 +23049,7 @@
"subject": "d=7,u=7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "4.0"
+ "v": "5.0"
}
],
"spawned": []
diff --git a/packages/meshbay-node/tests/test_admin_challenge_bounds.py b/packages/meshbay-node/tests/test_admin_challenge_bounds.py
new file mode 100644
index 0000000..fd3b2f1
--- /dev/null
+++ b/packages/meshbay-node/tests/test_admin_challenge_bounds.py
@@ -0,0 +1,135 @@
+"""
+What a connection may leave waiting for a signature (docs/MESHBAY_DESIGN.md §13.5b).
+
+Anyone authenticated can ask for an admin challenge — the signature is checked
+later — so a member who never answers must not make the node keep every request.
+Measured before the bound: 200 `root_add` of 1 MiB each from a plain member held
+200 pending operations and ~400 MiB for the life of the connection.
+"""
+
+import struct
+import time
+
+import msgpack
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from meshbay_node.transport.webrtc.admin import MAX_ADMIN_OP_BYTES, MAX_PENDING_ADMIN_OPS
+from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+
+GROUP = "g" * 32
+
+
+class _Channel:
+ readyState = "open"
+
+ def __init__(self):
+ self.sent = []
+
+ def send(self, data: bytes) -> None:
+ (n,) = struct.unpack(">I", data[:4])
+ self.sent.append(msgpack.unpackb(data[4:4 + n], raw=False))
+
+
+class _PC:
+ connectionState = "connected"
+ iceConnectionState = "connected"
+ remoteDescription = None
+ localDescription = None
+ sctp = None
+
+
+def _member_session():
+ """An authenticated member — not the operator — on a node that has one."""
+ ctx = {"sk_node": Ed25519PrivateKey.from_private_bytes(b"\x01" * 32),
+ "groups": {GROUP: {}}, "has_admin_authority": True}
+ s = WebRTCPeerSession(_PC(), ctx, peer_id="peer")
+ s._channel = _Channel()
+ s._audit = lambda *a, **k: None
+ s._user_id, s._group_id = "member-1", GROUP
+ return s
+
+
+def _root_add(s, path: str) -> dict:
+ s._dispatch_message({"type": "root_add", "group_id": GROUP, "path": path})
+ return s._channel.sent[-1]
+
+
+def test_a_member_cannot_pile_up_challenges():
+ s = _member_session()
+ for i in range(MAX_PENDING_ADMIN_OPS):
+ assert _root_add(s, f"/srv/{i}")["type"] == "admin_challenge"
+ refused = _root_add(s, "/srv/one-too-many")
+ assert refused["type"] == "error" and refused["code"] == "too_many_pending"
+ assert len(s._admin_ops) == MAX_PENDING_ADMIN_OPS
+
+
+def test_an_oversized_request_is_not_kept():
+ s = _member_session()
+ refused = _root_add(s, "x" * (MAX_ADMIN_OP_BYTES + 1))
+ assert refused["type"] == "error" and refused["code"] == "too_large"
+ assert s._admin_ops == {}
+
+
+def test_an_expired_challenge_frees_its_place():
+ s = _member_session()
+ for i in range(MAX_PENDING_ADMIN_OPS):
+ _root_add(s, f"/srv/{i}")
+ for pending in s._admin_ops.values():
+ pending["ts"] -= 10_000
+ assert _root_add(s, "/srv/after-expiry")["type"] == "admin_challenge"
+ assert len(s._admin_ops) == 1
+
+
+def test_answering_a_challenge_frees_its_place():
+ s = _member_session()
+ for i in range(MAX_PENDING_ADMIN_OPS):
+ _root_add(s, f"/srv/{i}")
+ op_id = next(iter(s._admin_ops))
+ s._dispatch_message({"type": "admin_response", "op_id": op_id, "signature": "!!"})
+ assert len(s._admin_ops) == MAX_PENDING_ADMIN_OPS - 1
+ assert _root_add(s, "/srv/next")["type"] == "admin_challenge"
+ assert all(time.time() - p["ts"] < 5 for p in s._admin_ops.values())
+
+
+# ── What a challenge covers (docs/MESHBAY_DESIGN.md §5.4) ────────────────────
+#
+# The signature covers the subject and nothing else of a request, so every value
+# the executor acts on has to be in it.
+
+def test_root_add_signs_whether_members_may_write():
+ from meshbay_common.adminop import root_add_subject
+ s = _member_session()
+ s._dispatch_message({"type": "root_add", "group_id": GROUP, "path": "/srv/drop",
+ "name": "Drop", "writable": True, "removable": False})
+ challenge = s._channel.sent[-1]
+ assert challenge["subject"] == root_add_subject("/srv/drop", "Drop", "generic",
+ True, False)
+ assert challenge["subject"] != root_add_subject("/srv/drop", "Drop", "generic",
+ False, False)
+
+
+def test_group_attach_signs_the_directory_it_exposes():
+ from meshbay_common.adminop import group_attach_subject
+ s = _member_session()
+ s._dispatch_message({"type": "group_attach", "name": "photos",
+ "shared_dir": "/home/me/Photos"})
+ assert s._channel.sent[-1]["subject"] == group_attach_subject(
+ "photos", "/home/me/Photos", True)
+
+
+def test_invite_create_signs_the_name_it_records():
+ from meshbay_common.adminop import invite_create_subject
+ s = _member_session()
+ s._ctx["roster"] = object() # only its presence is checked before the challenge
+ s._dispatch_message({"type": "invite_create", "group_id": GROUP,
+ "user_id": "u-1", "username": "alice"})
+ assert s._channel.sent[-1]["subject"] == invite_create_subject("u-1", "alice")
+
+
+def test_tmdb_config_signs_the_token_without_writing_it():
+ from meshbay_common.adminop import tmdb_config_subject
+ s = _member_session()
+ s._dispatch_message({"type": "tmdb_config", "token": "secret-token",
+ "language": "fr-FR"})
+ subject = s._channel.sent[-1]["subject"]
+ assert subject == tmdb_config_subject("secret-token", "fr-FR")
+ assert "secret-token" not in subject
diff --git a/packages/meshbay-node/tests/test_tmdb_config_policy.py b/packages/meshbay-node/tests/test_tmdb_config_policy.py
index 6a51eb0..c671ac8 100644
--- a/packages/meshbay-node/tests/test_tmdb_config_policy.py
+++ b/packages/meshbay-node/tests/test_tmdb_config_policy.py
@@ -22,7 +22,7 @@ from pathlib import Path
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
-from meshbay_common.adminop import OP_TMDB_CONFIG
+from meshbay_common.adminop import OP_TMDB_CONFIG, tmdb_config_subject
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roster import Roster
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
@@ -134,7 +134,9 @@ async def test_subject_reflects_whether_a_token_was_supplied(tmp_path):
session._do_tmdb_config({"token": "x"})
_, subject, _, _ = issued[0]
- assert subject == "custom_token=yes,language=default"
+ # The token is bound by its digest and never written into the subject.
+ assert subject == tmdb_config_subject("x", None)
+ assert "sha256:" in subject and tmdb_config_subject("y", None) != subject
async def test_subject_says_no_custom_token_when_none_given(tmp_path):
@@ -146,7 +148,10 @@ async def test_subject_says_no_custom_token_when_none_given(tmp_path):
session._do_tmdb_config({})
_, subject, _, _ = issued[0]
- assert subject == "custom_token=no,language=default"
+ # Nothing given means both unchanged — distinct from clearing either.
+ assert subject == tmdb_config_subject(None, None)
+ assert subject != tmdb_config_subject("", None)
+ assert subject != tmdb_config_subject(None, "")
async def test_subject_reflects_a_configured_language(tmp_path):
@@ -158,7 +163,7 @@ async def test_subject_reflects_a_configured_language(tmp_path):
session._do_tmdb_config({"language": "fr-FR"})
_, subject, payload, _ = issued[0]
- assert subject == "custom_token=no,language=fr-FR"
+ assert subject == tmdb_config_subject(None, "fr-FR")
assert payload["language"] == "fr-FR"
diff --git a/packages/meshbay-node/tests/test_webrtc_transport.py b/packages/meshbay-node/tests/test_webrtc_transport.py
index 4542817..7a6f517 100644
--- a/packages/meshbay-node/tests/test_webrtc_transport.py
+++ b/packages/meshbay-node/tests/test_webrtc_transport.py
@@ -34,6 +34,7 @@ from meshbay_common.adminop import (
OP_INVITE_CREATE,
OP_INVITE_LINK_CREATE,
admin_transcript,
+ invite_create_subject,
)
from meshbay_common.crypto import (
generate_gek,
@@ -1335,7 +1336,8 @@ async def test_invite_then_join_delivers_the_gek(sk_node, sk_hub, gek, shared_di
challenge_msg = await asyncio.wait_for(q_admin.get(), timeout=5.0)
assert challenge_msg["type"] == MNP.ADMIN_CHALLENGE
assert challenge_msg["op"] == OP_INVITE_CREATE
- assert challenge_msg["subject"] == "user-002"
+ # The name the invitation records is signed with the account it is for.
+ assert challenge_msg["subject"] == invite_create_subject("user-002", "bob")
ch_admin.send(_pack({
"type": MNP.ADMIN_RESPONSE, "v": MNP_VERSION,