diff options
Diffstat (limited to 'packages/meshbay-node/tests/test_security_regressions.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_security_regressions.py | 58 |
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" + |