aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/tests/test_js_python_parity.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-common/tests/test_js_python_parity.py')
-rw-r--r--packages/meshbay-common/tests/test_js_python_parity.py65
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.