diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_desktop_keyring.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_desktop_keyring.py | 106 |
1 files changed, 99 insertions, 7 deletions
diff --git a/packages/meshbay-hub/tests/test_desktop_keyring.py b/packages/meshbay-hub/tests/test_desktop_keyring.py index 673a00e..963ee55 100644 --- a/packages/meshbay-hub/tests/test_desktop_keyring.py +++ b/packages/meshbay-hub/tests/test_desktop_keyring.py @@ -62,9 +62,39 @@ const v = JSON.parse(fs.readFileSync(input, 'utf8')); out.sealed_here = ring.sealBundle(v.userId, v.node).bundle; out.playlist_key = ring.playlistKey(v.userId); - // 2. a signature and an X25519 agreement that the other side can check. - const msg = Buffer.from('a transcript'); - out.sig = ring.sign(v.userId, v.node, msg.toString('base64')); + // 2. signatures by kind, built here from fields, for the reference to check; + // and an X25519 agreement that the other side can check. + const ts = Math.floor(Date.now() / 1000); + const nonceNode = Buffer.alloc(32, 7).toString('base64'); + const other = require('crypto').generateKeyPairSync('ed25519').publicKey + .export({ format: 'der', type: 'spki' }).subarray(12).toString('base64'); + const F = { + join: { nodePk: v.node, groupId: 'grp-1', userId: v.userId, nonceNode, ts }, + device_hello: { nodePk: v.node, groupId: 'grp-1', userId: v.userId, nonceNode, ts }, + device_request: { nodePk: v.node, userId: v.userId, codeHash: 'ab'.repeat(32), nonceNode, ts }, + device_add: { nodePk: v.node, userId: v.userId, pkEd: other, pkX: other, nonceNode, ts }, + device_revoke: { nodePk: v.node, userId: v.userId, pkEd: other, nonceNode, ts }, + chat: { groupId: 'grp-1', epoch: 3, nonce: Buffer.alloc(12, 1).toString('base64'), + ct: Buffer.from('ciphertext').toString('base64') }, + admin: { op: 'file_delete', nodePk: v.node, groupId: 'grp-1', subject: 'file-9', + nonce: Buffer.alloc(32, 2).toString('base64'), ts }, + }; + out.fields = F; out.signed = {}; + for (const [kind, f] of Object.entries(F)) { + out.signed[kind] = ring.signAs(v.userId, v.node, kind, f); + } + + const refusal = async (kind, f) => { + try { await ring.signAs(v.userId, v.node, kind, f); return null; } + catch (e) { return e.code || e.message; } + }; + out.refused = { + bytes: await refusal('raw', { bytes: 'YQ==' }), + other_node: await refusal('join', { ...F.join, nodePk: 'T3RoZXJOb2Rl' }), + other_account: await refusal('join', { ...F.join, userId: 'someone-else' }), + stale: await refusal('join', { ...F.join, ts: ts - 3600 }), + }; + const eph = require('crypto').generateKeyPairSync('x25519'); const ephPub = eph.publicKey.export({ format: 'der', type: 'spki' }).subarray(12); out.shared_here = ring.shared(v.userId, v.node, ephPub.toString('base64')); @@ -94,10 +124,16 @@ const v = JSON.parse(fs.readFileSync(input, 'utf8')); out.retired = (() => { try { ring.openBundle(v.userId, 'NODE-R', { bundleEnc: Buffer.from('MBK2' + 'x'.repeat(40)).toString('base64') }); } catch (e) { return e.code; } })(); + out.access_default = ring.browserAccess('someone-else'); + ring.setBrowserAccess(v.userId, false); + const refusedSeal = (fn) => { try { fn(); return null; } catch (e) { return e.message; } }; + out.sealing_while_access_off = { + bundle: refusedSeal(() => ring.sealBundle(v.userId, v.node)), + recovery: refusedSeal(() => ring.sealRecovery(v.userId, v.node, 'A'.repeat(56), v.user)), + }; ring.forgetSession(v.userId); out.after_sign_out = { session: ring.hasSession(v.userId), identity_kept: !!ring.identity(v.userId, v.node) }; - out.access_default = ring.browserAccess('someone-else'); ring.setBrowserAccess(v.userId, false); out.access_after_off = ring.browserAccess(v.userId); out.stored_json = JSON.stringify(store); @@ -155,10 +191,66 @@ def test_the_playlist_key_is_the_pages(out): _reference_master(), "meshbay:playlists:v2") -def test_signatures_and_agreements_check_out_with_the_public_keys(out): +def _reference_transcript(kind, f, pub): + from meshbay_common.adminop import admin_transcript + from meshbay_common.chatbox import signing_transcript + from meshbay_common.device import ( + device_add_transcript, + device_hello_transcript, + device_request_transcript, + device_revoke_transcript, + ) + from meshbay_common.join import join_transcript + nonce_node = base64.b64decode(f.get("nonceNode", "")) + ed, x = pub["pkEdB64"], pub["pkXB64"] + return { + "join": lambda: join_transcript(f["nodePk"], f["groupId"], f["userId"], ed, x, + nonce_node, f["ts"]), + "device_hello": lambda: device_hello_transcript(f["nodePk"], f["groupId"], + f["userId"], ed, nonce_node, f["ts"]), + "device_request": lambda: device_request_transcript( + f["nodePk"], f["userId"], ed, x, f["codeHash"], nonce_node, f["ts"]), + "device_add": lambda: device_add_transcript(f["nodePk"], f["userId"], f["pkEd"], + f["pkX"], nonce_node, f["ts"]), + "device_revoke": lambda: device_revoke_transcript(f["nodePk"], f["userId"], f["pkEd"], + nonce_node, 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]() + + +def test_every_kind_signs_the_specifications_bytes(out): + """ + The keyring builds the transcript itself from fields; what it signs must be + exactly what meshbay_common builds, or the node refuses every join, chat + line and admin operation from the desktop application. + """ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - Ed25519PublicKey.from_public_bytes(base64.b64decode(out["minted_pub"]["pkEdB64"])).verify( - base64.b64decode(out["sig"]), b"a transcript") + pub = out["minted_pub"] + key = Ed25519PublicKey.from_public_bytes(base64.b64decode(pub["pkEdB64"])) + assert set(out["signed"]) == {"join", "device_hello", "device_request", "device_add", + "device_revoke", "chat", "admin"} + for kind, sig in out["signed"].items(): + key.verify(base64.b64decode(sig), _reference_transcript(kind, out["fields"][kind], pub)) + + +def test_the_page_names_a_kind_and_never_the_bytes(out): + r = out["refused"] + assert "nothing is signed as" in r["bytes"] + assert "another node" in r["other_node"] + assert "another account" in r["other_account"] + assert "not now" in r["stale"] + + +def test_nothing_is_sealed_for_a_browser_while_browser_access_is_off(out): + for what, err in out["sealing_while_access_off"].items(): + assert err and "browser access is off" in err, what + + +def test_agreements_check_out_with_the_public_keys(out): assert out["shared_here"] == out["shared_there"] |