diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 21:04:39 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 21:04:39 +0200 |
| commit | 0378e8e0912a1a7e6cea4424e69d524e7afecbf8 (patch) | |
| tree | 4ae94e32d6638b4c2cc1ae4f74cbe5d00c940636 /packages/meshbay-common/tests/test_js_python_parity.py | |
| parent | 0ed56d3a1b4f71cf622d3e27edc87a15ef33c185 (diff) | |
| download | meshbay-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.py | 101 |
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"] |