diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 15:48:51 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 15:48:51 +0200 |
| commit | 8926f163dad9d32dc06c3a142658a4e11d9c12c1 (patch) | |
| tree | 4d36d1c18154cb46e6e80c59ef6c607972caa81e /packages/meshbay-hub/tests | |
| parent | 8d96cf2314b45e0737f932998b5422c27a2ae72e (diff) | |
| download | meshbay-8926f163dad9d32dc06c3a142658a4e11d9c12c1.tar.gz | |
refactor(hub): the transport holds an identity, never a private key
Two public keys, sign() and shared(); the apps take transport.signFn. What
holds the keys (this page, or the desktop main process) is the identity's
business alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/harness/chat_send_probe.py | 5 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_identity_seam.py | 49 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_rewrap_fanout.py | 2 |
3 files changed, 53 insertions, 3 deletions
diff --git a/packages/meshbay-hub/tests/harness/chat_send_probe.py b/packages/meshbay-hub/tests/harness/chat_send_probe.py index 7569628..e5cfc8b 100644 --- a/packages/meshbay-hub/tests/harness/chat_send_probe.py +++ b/packages/meshbay-hub/tests/harness/chat_send_probe.py @@ -171,7 +171,8 @@ function makeTransport(name, chatReply) { tp._groupId = '__GROUP_ID__'; tp._gekRaw = hex('__GEK_HEX__'); tp.chatEpoch = 1; - tp._sessionKeys = { skEdB64: SK_ED_B64 }; + tp._identity = { pkEdB64: DEVICE_PK_B64, + sign: (bytes) => window.MeshBayKeys.signBytes(SK_ED_B64, bytes) }; tp.devicePk = DEVICE_PK_B64; tp._send = (obj) => { log.push('sent ' + obj.type); @@ -314,7 +315,7 @@ async function runScenario(name, chatReply, duringSession) { // connect() drops it before it touches the network. Nothing about this // step is simulated, and the clear is not poked in by the test. await tp.connect('node-1', 'token', tp._groupId, tp._gekRaw, - tp._sessionKeys, null, 'me', 'user-me').then( + tp._identity, null, 'me', 'user-me').then( () => log.push('reconnect: connect() unexpectedly succeeded'), (e) => log.push('reconnect: connect() stopped at signaling, as expected: ' + (e && e.message || e))); diff --git a/packages/meshbay-hub/tests/test_identity_seam.py b/packages/meshbay-hub/tests/test_identity_seam.py new file mode 100644 index 0000000..15db6d8 --- /dev/null +++ b/packages/meshbay-hub/tests/test_identity_seam.py @@ -0,0 +1,49 @@ +""" +Nothing in the interface reads an identity private key except the identity. + +The transport sees an identity on a node as two public keys, a signature and an +X25519 agreement (`transport.js`, `_identityFromKeys`). In a browser that object +wraps keys held in the page; in the desktop application the keys stay in the +main process and the same object asks it to sign. That only holds if no other +code reaches past the object for a key — which is what every admin op, device +link, chat message and app used to do, twenty times over, and is what this +test refuses. +""" + +import re + +from spa_source import STATIC + +# Where a private key may be named: minted and sealed (keyderive.js), wrapped +# into an identity (transport.js), re-sealed during a passphrase change in a +# browser (transport-rewrap.js). +ALLOWED = {"keyderive.js", "transport.js", "transport-rewrap.js"} + + +def test_only_the_identity_touches_a_private_key(): + offenders = [] + for path in STATIC.glob("*.js"): + if path.name in ALLOWED: + continue + text = path.read_text(encoding="utf-8") + if re.search(r"skEdB64|skXB64|sessionKeys|signBytes\(", text): + offenders.append(path.name) + assert not offenders, f"these read a private key directly: {offenders}" + + +def test_in_the_transport_only_the_identity_factory_signs_with_a_raw_key(): + text = (STATIC / "transport.js").read_text(encoding="utf-8") + factory = text[text.index("async function _identityFromKeys"):] + factory = factory[:factory.index("\n}\n")] + rest = text.replace(factory, "") + assert "signBytes(" in factory + assert "signBytes(" not in rest, "the transport signs with a raw key outside the identity" + assert "skXB64" not in rest.replace("id.skXB64", ""), \ + "the transport reads the X25519 private key outside the identity" + + +def test_a_group_key_is_unwrapped_through_the_identity(): + crypto = (STATIC / "crypto.js").read_text(encoding="utf-8") + unwrap = crypto[crypto.index("async function unwrapGEK"):] + unwrap = unwrap[:unwrap.index("\n}\n")] + assert "shared(pkEphRaw)" in unwrap and "pkcs8" not in unwrap diff --git a/packages/meshbay-hub/tests/test_rewrap_fanout.py b/packages/meshbay-hub/tests/test_rewrap_fanout.py index 79a5bf5..9e93140 100644 --- a/packages/meshbay-hub/tests/test_rewrap_fanout.py +++ b/packages/meshbay-hub/tests/test_rewrap_fanout.py @@ -74,7 +74,7 @@ T.prototype.connect = async function (nodeId, _t, _g, _gek, _sk, bundleKey) { rewrapOnlySeen.push(this._rewrapOnly === true); const s = NODES[nodeId] || {}; if (s.throws) throw new Error(s.throws); - this._sessionKeys = s.sessionKeys || null; + this._identity = s.sessionKeys ? { raw: s.sessionKeys } : null; this._newNodeBundle = s.newNodeBundle || null; return { ok: true }; }; |