aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/tests/test_js_python_parity.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-30 21:04:39 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-30 21:04:39 +0200
commit0378e8e0912a1a7e6cea4424e69d524e7afecbf8 (patch)
tree4ae94e32d6638b4c2cc1ae4f74cbe5d00c940636 /packages/meshbay-common/tests/test_js_python_parity.py
parent0ed56d3a1b4f71cf622d3e27edc87a15ef33c185 (diff)
downloadmeshbay-0378e8e0912a1a7e6cea4424e69d524e7afecbf8.tar.gz
fix: an identity signs a named kind, and a device approval answers a request
The desktop main process builds every transcript itself from fields (transcripts.js) and signs no raw bytes; the page's identity has the same contract (crypto.js transcriptFor). The keyring seals no bundle while browser access is off. On the node, device_add must redeem a pending request filed by the same keys, and device_revoke is signed under its own prefix (meshbay:device_revoke:v1), so a retirement signature admits nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-common/tests/test_js_python_parity.py')
-rw-r--r--packages/meshbay-common/tests/test_js_python_parity.py101
1 files changed, 101 insertions, 0 deletions
diff --git a/packages/meshbay-common/tests/test_js_python_parity.py b/packages/meshbay-common/tests/test_js_python_parity.py
index 5bf34aa..f75d60a 100644
--- a/packages/meshbay-common/tests/test_js_python_parity.py
+++ b/packages/meshbay-common/tests/test_js_python_parity.py
@@ -625,3 +625,104 @@ def test_a_message_does_not_open_under_another_devices_key(chatbox_js):
open_message(CHAT_EPOCH_KEY, group_id, epoch, vectors[1]["device_b64"],
bytes.fromhex(vectors[0]["nonce"]),
bytes.fromhex(vectors[0]["ct"]))
+
+
+# ── Every kind an identity signs, through `transcriptFor` ───────────────────
+#
+# The page signs with an identity only by kind (`transcriptFor`), and the
+# desktop application builds the same bytes in its main process. The device
+# transcripts had no parity check of their own; the retirement one differs
+# from the admission one only by its prefix, which is the whole point of it.
+
+_KINDS_HARNESS = r"""
+const fs = require('fs');
+globalThis.window = {};
+const src = fs.readFileSync(process.argv[2], 'utf8');
+const M = new Function(src + '\nreturn { transcriptFor };')();
+const input = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
+const toHex = (u8) => Array.from(u8).map(b => b.toString(16).padStart(2, '0')).join('');
+const out = {};
+for (const [kind, f] of Object.entries(input.fields)) {
+ out[kind] = toHex(M.transcriptFor(kind, f, input.own));
+}
+try { M.transcriptFor('raw', {}, input.own); out.raw = 'signed'; }
+catch (e) { out.raw = String(e.message); }
+process.stdout.write(JSON.stringify(out));
+"""
+
+_OWN = {"pkEdB64": base64.b64encode(b"E" * 32).decode(),
+ "pkXB64": base64.b64encode(b"X" * 32).decode()}
+_OTHER = base64.b64encode(b"O" * 32).decode()
+_NONCE = base64.b64encode(b"\x07" * 32).decode()
+_KIND_FIELDS = {
+ "join": {"nodePk": "Tk9ERVBL", "groupId": "g" * 32, "userId": "grenet",
+ "nonceNode": _NONCE, "ts": 1_700_000_000},
+ "device_hello": {"nodePk": "Tk9ERVBL", "groupId": "g" * 32, "userId": "grenet",
+ "nonceNode": _NONCE, "ts": 1_700_000_000},
+ "device_request": {"nodePk": "Tk9ERVBL", "userId": "grenet", "codeHash": "ab" * 32,
+ "nonceNode": _NONCE, "ts": 1_700_000_000},
+ "device_add": {"nodePk": "Tk9ERVBL", "userId": "grenet", "pkEd": _OTHER,
+ "pkX": _OTHER, "nonceNode": _NONCE, "ts": 1_700_000_000},
+ "device_revoke": {"nodePk": "Tk9ERVBL", "userId": "grenet", "pkEd": _OTHER,
+ "nonceNode": _NONCE, "ts": 1_700_000_000},
+ "chat": {"groupId": "g" * 32, "epoch": 4,
+ "nonce": base64.b64encode(b"\x01" * 12).decode(),
+ "ct": base64.b64encode(b"ciphertext").decode()},
+ "admin": {"op": "root_add", "nodePk": "Tk9ERVBL", "groupId": "g" * 32,
+ "subject": '{"path":"/café"}', "nonce": _NONCE, "ts": 1_700_000_000},
+}
+
+
+@pytest.fixture(scope="module")
+def kinds_js(tmp_path_factory):
+ d = tmp_path_factory.mktemp("kinds")
+ (d / "harness.js").write_text(_KINDS_HARNESS)
+ (d / "input.json").write_text(json.dumps({"fields": _KIND_FIELDS, "own": _OWN}))
+ proc = subprocess.run(["node", str(d / "harness.js"), str(CRYPTO_JS), str(d / "input.json")],
+ capture_output=True, text=True, timeout=60)
+ if proc.returncode != 0:
+ pytest.fail(f"node harness failed:\n{proc.stderr}")
+ return json.loads(proc.stdout)
+
+
+def _python_kind(kind):
+ from meshbay_common.chatbox import signing_transcript
+ from meshbay_common.device import (
+ device_add_transcript,
+ device_hello_transcript,
+ device_request_transcript,
+ device_revoke_transcript,
+ )
+ f = _KIND_FIELDS[kind]
+ nonce = base64.b64decode(f.get("nonceNode", ""))
+ ed, x = _OWN["pkEdB64"], _OWN["pkXB64"]
+ return {
+ "join": lambda: join_transcript(f["nodePk"], f["groupId"], f["userId"], ed, x,
+ nonce, f["ts"]),
+ "device_hello": lambda: device_hello_transcript(f["nodePk"], f["groupId"], f["userId"],
+ ed, nonce, f["ts"]),
+ "device_request": lambda: device_request_transcript(f["nodePk"], f["userId"], ed, x,
+ f["codeHash"], nonce, f["ts"]),
+ "device_add": lambda: device_add_transcript(f["nodePk"], f["userId"], f["pkEd"],
+ f["pkX"], nonce, f["ts"]),
+ "device_revoke": lambda: device_revoke_transcript(f["nodePk"], f["userId"], f["pkEd"],
+ nonce, f["ts"]),
+ "chat": lambda: signing_transcript(f["groupId"], f["epoch"], base64.b64decode(ed),
+ base64.b64decode(f["nonce"]),
+ base64.b64decode(f["ct"])),
+ "admin": lambda: admin_transcript(f["op"], f["nodePk"], f["groupId"], f["subject"],
+ base64.b64decode(f["nonce"]), f["ts"]),
+ }[kind]()
+
+
+@pytest.mark.parametrize("kind", sorted(_KIND_FIELDS))
+def test_every_signed_kind_is_byte_identical(kind, kinds_js):
+ assert kinds_js[kind] == _python_kind(kind).hex(), kind
+
+
+def test_a_retirement_is_not_an_admission(kinds_js):
+ assert kinds_js["device_revoke"] != kinds_js["device_add"]
+
+
+def test_an_unknown_kind_is_not_signed(kinds_js):
+ assert "nothing is signed as" in kinds_js["raw"]