diff options
Diffstat (limited to 'packages/meshbay-common/tests/test_js_python_parity.py')
| -rw-r--r-- | packages/meshbay-common/tests/test_js_python_parity.py | 65 |
1 files changed, 63 insertions, 2 deletions
diff --git a/packages/meshbay-common/tests/test_js_python_parity.py b/packages/meshbay-common/tests/test_js_python_parity.py index 9adac51..340ea3e 100644 --- a/packages/meshbay-common/tests/test_js_python_parity.py +++ b/packages/meshbay-common/tests/test_js_python_parity.py @@ -22,6 +22,7 @@ import pytest from meshbay_common.adminop import admin_transcript from meshbay_common.handshake import handshake_transcript, webrtc_binding +from meshbay_common.join import join_transcript CRYPTO_JS = (Path(__file__).resolve().parents[2] / "meshbay-hub" / "src" / "meshbay_hub" / "static" / "crypto.js") @@ -52,6 +53,18 @@ ADMIN_VECTORS = [ ("file_delete", "Tk9ERVBL", "café", "fichier é.mp4", "04" * 32, 1_700_000_002), ] +# (node_pk_b64, group_id, user_id, pk_ed b64, pk_x b64, nonce hex, ts) +JOIN_VECTORS = [ + # Operator pairing: group_id is empty and must stay distinguishable from a + # request that names a group. + ("Tk9ERVBL", "", "grenet", "QUFB", "QkJC", "01" * 32, 1_700_000_000), + ("Tk9ERVBL", "g" * 32, "grenet", "QUFB", "QkJC", "01" * 32, 1_700_000_000), + # Identical to the previous vector except that the two keys are swapped — + # they are adjacent fields, so this isolates the ordering. + ("Tk9ERVBL", "g" * 32, "grenet", "QkJC", "QUFB", "01" * 32, 1_700_000_000), + ("Tk9ERVBL", "café", "utilisateur-é", "QUFB", "QkJC", "03" * 32, 0), +] + _HARNESS = r""" const fs = require('fs'); @@ -62,7 +75,8 @@ globalThis.crypto = globalThis.crypto || {}; const src = fs.readFileSync(process.argv[2], 'utf8'); const load = new Function( - src + '\nreturn { handshakeTranscript, adminTranscript, webrtcBinding, b64encode };'); + src + '\nreturn { handshakeTranscript, adminTranscript, joinTranscript, ' + + 'webrtcBinding, b64encode };'); const M = load(); const hex = (s) => { @@ -74,7 +88,7 @@ const toHex = (u8) => Array.from(u8).map(b => b.toString(16).padStart(2, '0')).join(''); const input = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')); -const out = { handshake: [], admin: [] }; +const out = { handshake: [], admin: [], join: [] }; for (const v of input.handshake) { const binding = M.webrtcBinding(hex(v.offer_fp), hex(v.answer_fp)); @@ -87,6 +101,11 @@ for (const v of input.admin) { v.op, v.node_pk, v.group_id, v.subject, M.b64encode(hex(v.nonce)), v.ts))); } +for (const v of input.join) { + out.join.push(toHex(M.joinTranscript( + v.node_pk, v.group_id, v.user_id, v.pk_ed, v.pk_x, hex(v.nonce), v.ts))); +} + process.stdout.write(JSON.stringify(out)); """ @@ -110,6 +129,11 @@ def js_output(tmp_path_factory): "subject": s, "nonce": n, "ts": ts} for op, pk, g, s, n, ts in ADMIN_VECTORS ], + "join": [ + {"node_pk": pk, "group_id": g, "user_id": u, + "pk_ed": pe, "pk_x": px, "nonce": n, "ts": ts} + for pk, g, u, pe, px, n, ts in JOIN_VECTORS + ], })) proc = subprocess.run( @@ -164,6 +188,43 @@ def test_admin_transcript_parity(idx, vector, js_output): ) +@pytest.mark.parametrize("idx,vector", list(enumerate(JOIN_VECTORS))) +def test_join_transcript_parity(idx, vector, js_output): + """ + A mismatch here means no browser can pair with a node and no member can be + recognised — the node would reject every signature as invalid, and, as with + the other two, nothing else in the suite crosses this boundary. + """ + node_pk, group_id, user_id, pk_ed, pk_x, nonce, ts = vector + + expected = join_transcript( + node_pk_b64=node_pk, + group_id=group_id, + user_id=user_id, + pk_ed25519_b64=pk_ed, + pk_x25519_b64=pk_x, + nonce_node=bytes.fromhex(nonce), + ts=ts, + ) + assert js_output["join"][idx] == expected.hex(), ( + f"crypto.js and meshbay_common.join disagree for user={user_id!r} " + f"group={group_id!r}" + ) + + +def test_join_transcript_binds_the_two_keys_in_order(js_output): + """ + The X25519 key is trusted only because the Ed25519 identity signed it, so the + two must not be interchangeable: swapping them has to produce different bytes. + """ + assert js_output["join"][1] != js_output["join"][2] + + +def test_operator_pairing_is_distinguishable_from_a_group_join(js_output): + """An empty group_id (node-wide operator authority) must not collide.""" + assert js_output["join"][0] != js_output["join"][1] + + def test_length_prefixing_actually_disambiguates(js_output): """ The reason both sides length-prefix: two different field splits must not collide. |