diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_transport_contracts.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_transport_contracts.py | 75 |
1 files changed, 75 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py index 6011ddb..fee80bb 100644 --- a/packages/meshbay-hub/tests/test_transport_contracts.py +++ b/packages/meshbay-hub/tests/test_transport_contracts.py @@ -317,3 +317,78 @@ def test_uploads_are_tracked_per_file(transport): """Acks interleave when two files are in flight.""" assert "this._uploaders = new Map()" in transport assert "this._uploaders.set(file.name" in transport + + +# ── MNP 1.0: the sealed handshake ack ──────────────────────────────────────── +# +# The index half is measured for real in `test_index_seal_client.py`. The ack is +# opened inside `connect()`, three messages into a WebRTC negotiation, so these +# read the source — and the ordering they pin is the whole security argument, not +# an implementation detail. + +def _handshake_block(transport: str) -> str: + start = transport.index("if (reply.type === 'handshake_challenge') {") + return transport[start:transport.index(" return ack;", start)] + + +def test_the_ack_is_verified_before_it_is_decrypted(transport): + """ + Verify, then decrypt. Opening the payload first would mean acting on data + from a peer we have not yet authenticated — which is the exact shape of C3, + where `node_pk` was never checked and a peer that had hijacked signaling + could serve a forged index and a forged `is_node_admin`. + """ + block = _handshake_block(transport) + proof = block.index("Node failed to prove GEK possession") + signature = block.index("Node signature invalid") + pinned = block.index("_checkNodePin(") + opened = block.index("openGroup(") + assert proof < opened, "the payload is opened before the GEK proof is checked" + assert signature < opened, "the payload is opened before the signature is checked" + assert pinned < opened, "the payload is opened before the node is pinned" + + +def test_an_ack_that_does_not_open_refuses_the_connection(transport): + """ + Never a default. An `enabled_apps` that failed to open would otherwise reach + the client's documented fallback — show every registered app — which is a + confident wrong answer, indistinguishable from an operator's real choice. + """ + block = _handshake_block(transport) + opened = block[block.index("let config;"):block.index("return ack;") + if "return ack;" in block else len(block)] + assert "throw new Error(" in opened, "a failed decrypt is swallowed" + assert "handshake_ack" in opened, "the failure does not name the message" + for fallback in ("|| {}", "?? {}", "catch { }", "config = {}"): + assert fallback not in opened, ( + f"the ack falls back to {fallback} instead of refusing") + + +def test_the_handshake_declares_a_version_range(transport): + """ + L2: `v` used to be written by everyone and read by nobody, so a mismatch + surfaced as a missing field rather than a refusal. Both halves of the range + ride the handshake, and the node's half is checked before anything below it + in `connect()` runs. + """ + block = transport[transport.index("type: 'handshake',"):] + block = block[:block.index("});")] + assert "v: MNP_V," in block and "v_min: MNP_V_MIN," in block + + challenge = _handshake_block(transport) + assert challenge.index("_checkNodeVersion(") < challenge.index("openGroup("), ( + "the node's version is checked after its messages are relied on") + + +def test_the_index_is_never_reported_from_a_failed_decrypt(transport): + """ + The consumer callbacks may only be reached from inside the opened path — a + `catch` that called `_onIndexSync` with an empty message would show "this + group has no files", which is a state a real group can be in. + """ + body = transport[transport.index("async _applyIndexMessage("):] + body = body[:body.index("\n /**", 1)] + assert "openGroup(" in body + assert "catch" not in body, ( + "_applyIndexMessage swallows its own failure instead of letting " + "_queueIndexMessage end the session") |