summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_security_regressions.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_security_regressions.py')
-rw-r--r--packages/meshbay-node/tests/test_security_regressions.py58
1 files changed, 32 insertions, 26 deletions
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py
index 9299bf4..6bb680c 100644
--- a/packages/meshbay-node/tests/test_security_regressions.py
+++ b/packages/meshbay-node/tests/test_security_regressions.py
@@ -216,16 +216,35 @@ def test_daemon_sets_no_global_chat_store(tmp_path):
# ── H2: node admin UI escaping ───────────────────────────────────────────────
-def test_gek_bundle_store_requires_admin_challenge(tmp_path):
+def test_no_member_can_hand_the_node_key_material(tmp_path):
"""
- C5b: gek_bundle_store used to write whatever any authenticated member sent.
- It must now answer with a challenge and store nothing until a valid
- node-operator signature arrives.
+ C5b, strengthened by the invite redesign (docs/invite-pairing-v1.md).
+
+ This test used to assert that `gek_bundle_store` answered with an admin
+ challenge and stored nothing without an operator signature. The message is now
+ gone entirely: the node holds the GEK and wraps it itself, so no member ever
+ submits key material, authorized or not. Deleting the path is a stronger
+ guarantee than gating it, which is why the assertion changed rather than the
+ behaviour regressing.
"""
+ from meshbay_common.protocol import MNP as _MNP
+
+ assert not hasattr(_MNP, "GEK_BUNDLE_STORE"), (
+ "the member-supplied bundle message is back — the node must never accept "
+ "key material over MNP (C5b)"
+ )
+
+ source = (Path(__file__).parent.parent
+ / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text()
+ assert "_do_gek_bundle_store" not in source
+ assert "_admin_exec_bundle_store" not in source
+
+
+def test_unknown_message_stores_nothing(tmp_path):
+ """A peer sending the retired message must not reach any storage path."""
session = _session(tmp_path, "ordinary-member")
session._group_id = None
session._admin_ops = {}
- session._ctx["admin_pk_ed25519"] = Ed25519PrivateKey.generate().public_key()
stored = []
@@ -234,27 +253,13 @@ def test_gek_bundle_store_requires_admin_challenge(tmp_path):
stored.append(args)
session._ctx["bundle_store"] = _Store()
- session._do_gek_bundle_store({
+ session._handle_message({
+ "type": "gek_bundle_store",
"user_id": "victim", "group_id": "g" * 32,
"pk_eph_b64": "AA==", "nonce_b64": "AA==", "wrapped_b64": "AA==",
})
- assert stored == [], "bundle written without operator authorization (C5b)"
- assert any(m.get("type") == "admin_challenge" for m in session.sent)
-
-
-def test_gek_bundle_store_refused_without_pinned_admin_key(tmp_path):
- """C5b: deny by default — no pinned key means no privileged operation."""
- session = _session(tmp_path, "ordinary-member")
- session._group_id = None
- session._admin_ops = {}
- session._ctx["bundle_store"] = object()
-
- session._do_gek_bundle_store({
- "user_id": "victim", "group_id": "g" * 32,
- "pk_eph_b64": "AA==", "nonce_b64": "AA==", "wrapped_b64": "AA==",
- })
- assert any(m.get("type") == "error" for m in session.sent)
+ assert stored == [], "a retired message type still reached the bundle store"
def test_gek_auto_activation_is_gone():
@@ -287,7 +292,7 @@ def test_admin_transcript_is_domain_separated():
@pytest.mark.parametrize("field,value", [
- ("op", "gek_bundle_store"),
+ ("op", "invite_create"),
("subject", "file-2"),
("node_pk_b64", "OTHERNODE"),
("group_id", "h" * 32),
@@ -320,15 +325,15 @@ def test_admin_signature_does_not_transfer_between_operations(tmp_path):
H5: the concrete attack. A signature collected to delete a file must not
authorize storing a GEK bundle.
"""
- from meshbay_common.adminop import OP_FILE_DELETE, OP_GEK_BUNDLE_STORE
+ from meshbay_common.adminop import OP_FILE_DELETE, OP_INVITE_CREATE
sk_admin = Ed25519PrivateKey.generate()
delete_transcript = _transcript(op=OP_FILE_DELETE)
signature = sk_admin.sign(delete_transcript)
- store_transcript = _transcript(op=OP_GEK_BUNDLE_STORE)
+ invite_transcript = _transcript(op=OP_INVITE_CREATE)
with pytest.raises(Exception):
- sk_admin.public_key().verify(signature, store_transcript)
+ sk_admin.public_key().verify(signature, invite_transcript)
def test_admin_challenge_expires(tmp_path):
@@ -573,3 +578,4 @@ def test_admin_ui_escapes_filenames(tmp_path):
assert payload not in html, "filename rendered unescaped — stored XSS (H2)"
assert "<img" in html, "filename should appear escaped"
+