diff options
Diffstat (limited to 'packages/meshbay-node/tests/test_device_linking.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_device_linking.py | 110 |
1 files changed, 101 insertions, 9 deletions
diff --git a/packages/meshbay-node/tests/test_device_linking.py b/packages/meshbay-node/tests/test_device_linking.py index 53387ca..344c252 100644 --- a/packages/meshbay-node/tests/test_device_linking.py +++ b/packages/meshbay-node/tests/test_device_linking.py @@ -30,6 +30,7 @@ from meshbay_common.device import ( device_add_transcript, device_code_hash, device_request_transcript, + device_revoke_transcript, ) from meshbay_common.join import ROLE_MEMBER from meshbay_common.protocol import MNP @@ -227,8 +228,8 @@ async def test_the_new_device_cannot_approve_itself(tmp_path, roster): sk_new, pk_new_ed, pk_new_x = _keys() session = await _session(tmp_path, roster) - await _file_request(session, sk_new, pk_new_ed, pk_new_x, generate_code()) - await _approve(session, sk_new, pk_new_ed, pk_new_x) + code_hash = await _file_request(session, sk_new, pk_new_ed, pk_new_x, generate_code()) + await _approve(session, sk_new, pk_new_ed, pk_new_x, code_hash=code_hash) assert _last(session)["type"] == "error" assert await roster.find_device("alice", pk_new_ed) is None @@ -240,10 +241,11 @@ async def test_a_stranger_cannot_approve(tmp_path, roster): await roster.pin_identity("alice", "alice", pk_old_ed, pk_old_x, "code") sk_bob, pk_bob_ed, pk_bob_x = _keys() await roster.pin_identity("bob", "bob", pk_bob_ed, pk_bob_x, "code") - _, pk_new_ed, pk_new_x = _keys() + sk_new, pk_new_ed, pk_new_x = _keys() session = await _session(tmp_path, roster) - await _approve(session, sk_bob, pk_new_ed, pk_new_x) + code_hash = await _file_request(session, sk_new, pk_new_ed, pk_new_x, generate_code()) + await _approve(session, sk_bob, pk_new_ed, pk_new_x, code_hash=code_hash) assert _last(session)["type"] == "error" assert await roster.find_device("alice", pk_new_ed) is None @@ -260,9 +262,10 @@ async def test_a_revoked_device_cannot_admit_its_replacement(tmp_path, roster): await roster.pin_identity("alice", "alice", pk_keep_ed, pk_keep_x, "device") await roster.revoke_device("alice", pk_lost_ed) - _, pk_new_ed, pk_new_x = _keys() + sk_new, pk_new_ed, pk_new_x = _keys() session = await _session(tmp_path, roster) - await _approve(session, sk_lost, pk_new_ed, pk_new_x) + code_hash = await _file_request(session, sk_new, pk_new_ed, pk_new_x, generate_code()) + await _approve(session, sk_lost, pk_new_ed, pk_new_x, code_hash=code_hash) assert _last(session)["type"] == "error" assert await roster.find_device("alice", pk_new_ed) is None @@ -416,10 +419,9 @@ async def test_your_last_device_cannot_be_revoked(tmp_path, roster): session = await _session(tmp_path, roster) ts = int(time.time()) - transcript = device_add_transcript( + transcript = device_revoke_transcript( node_pk_b64=session._node_pk_b64(), user_id="alice", - pk_ed25519_b64=pk_only_ed, pk_x25519_b64=pk_only_x, - nonce_node=NONCE, ts=ts) + pk_ed25519_b64=pk_only_ed, nonce_node=NONCE, ts=ts) await session._do_device_revoke({ "pk_ed25519": pk_only_ed, "ts": ts, "sig": base64.b64encode(sk_only.sign(transcript)).decode()}) @@ -438,3 +440,93 @@ async def test_unpinning_an_account_takes_every_device(tmp_path, roster): assert len(await roster.list_devices("alice")) == 3 await roster.unpin("alice") assert await roster.list_devices("alice") == [] + + +# ── An approval is an answer to a request, and nothing else ───────────────── + +async def test_a_countersignature_without_a_request_admits_nothing(tmp_path, roster): + """ + A pinned device's signature over keys nobody asked to add. Whoever obtained + it — a page that got a device to sign — must not be able to admit a key of + their choosing with it. + """ + sk_old, pk_old_ed, pk_old_x = _keys() + await roster.pin_identity("alice", "alice", pk_old_ed, pk_old_x, "code") + _, pk_new_ed, pk_new_x = _keys() + + session = await _session(tmp_path, roster) + await _approve(session, sk_old, pk_new_ed, pk_new_x) + + assert _last(session)["type"] == "error" + assert await roster.find_device("alice", pk_new_ed) is None + + +async def test_a_request_admits_only_the_keys_that_filed_it(tmp_path, roster): + """One device's pending request is not a ticket for another key.""" + sk_old, pk_old_ed, pk_old_x = _keys() + await roster.pin_identity("alice", "alice", pk_old_ed, pk_old_x, "code") + sk_asking, pk_asking_ed, pk_asking_x = _keys() + _, pk_other_ed, pk_other_x = _keys() + + session = await _session(tmp_path, roster) + code_hash = await _file_request(session, sk_asking, pk_asking_ed, pk_asking_x, + generate_code()) + await _approve(session, sk_old, pk_other_ed, pk_other_x, code_hash=code_hash) + + assert _last(session)["type"] == "error" + assert await roster.find_device("alice", pk_other_ed) is None + # And the request was not spent by the attempt. + await _approve(session, sk_old, pk_asking_ed, pk_asking_x, code_hash=code_hash) + assert _last(session)["type"] == MNP.DEVICE_ADD_ACK + + +async def test_a_retirement_signature_admits_nothing(tmp_path, roster): + """ + Retiring and admitting are signed under different prefixes: a signature + given to retire a key cannot be presented as the approval of that key. + """ + sk_old, pk_old_ed, pk_old_x = _keys() + await roster.pin_identity("alice", "alice", pk_old_ed, pk_old_x, "code") + sk_new, pk_new_ed, pk_new_x = _keys() + + session = await _session(tmp_path, roster) + code_hash = await _file_request(session, sk_new, pk_new_ed, pk_new_x, generate_code()) + ts = int(time.time()) + retire = device_revoke_transcript( + node_pk_b64=session._node_pk_b64(), user_id="alice", + pk_ed25519_b64=pk_new_ed, nonce_node=NONCE, ts=ts) + await session._do_device_add({ + "pk_ed25519": pk_new_ed, "pk_x25519": pk_new_x, "ts": ts, + "code_hash": code_hash, + "sig": base64.b64encode(sk_old.sign(retire)).decode(), + }) + + assert _last(session)["type"] == "error" + assert await roster.find_device("alice", pk_new_ed) is None + + +async def test_a_device_is_retired_with_the_retirement_signature(tmp_path, roster): + sk_a, pk_a_ed, pk_a_x = _keys() + _, pk_b_ed, pk_b_x = _keys() + await roster.pin_identity("alice", "alice", pk_a_ed, pk_a_x, "code") + await roster.pin_identity("alice", "alice", pk_b_ed, pk_b_x, "device") + session = await _session(tmp_path, roster) + + ts = int(time.time()) + admit = device_add_transcript( + node_pk_b64=session._node_pk_b64(), user_id="alice", + pk_ed25519_b64=pk_b_ed, pk_x25519_b64=pk_b_x, nonce_node=NONCE, ts=ts) + await session._do_device_revoke({ + "pk_ed25519": pk_b_ed, "ts": ts, + "sig": base64.b64encode(sk_a.sign(admit)).decode()}) + assert _last(session)["type"] == "error", "an admission signature retired a device" + assert await roster.find_device("alice", pk_b_ed) is not None + + retire = device_revoke_transcript( + node_pk_b64=session._node_pk_b64(), user_id="alice", + pk_ed25519_b64=pk_b_ed, nonce_node=NONCE, ts=ts) + await session._do_device_revoke({ + "pk_ed25519": pk_b_ed, "ts": ts, + "sig": base64.b64encode(sk_a.sign(retire)).decode()}) + assert _last(session)["type"] != "error", _last(session) + assert await roster.find_device("alice", pk_b_ed) is None |