From 0378e8e0912a1a7e6cea4424e69d524e7afecbf8 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 30 Sep 2026 21:04:39 +0200 Subject: 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 --- .../meshbay-common/tests/test_js_python_parity.py | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) (limited to 'packages/meshbay-common/tests/test_js_python_parity.py') 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"] -- cgit v1.2.3