diff options
Diffstat (limited to 'packages/meshbay-node/tests/test_device_linking.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_device_linking.py | 78 |
1 files changed, 71 insertions, 7 deletions
diff --git a/packages/meshbay-node/tests/test_device_linking.py b/packages/meshbay-node/tests/test_device_linking.py index 344c252..7f67b66 100644 --- a/packages/meshbay-node/tests/test_device_linking.py +++ b/packages/meshbay-node/tests/test_device_linking.py @@ -76,6 +76,7 @@ async def _session(tmp_path: Path, roster, user_id: str = "alice"): session._pinned_pk = "" session._uploads = {} session._nonce_node = NONCE + session._gek_challenge = None session._remote_ip = "" session.sent = [] session._send = session.sent.append @@ -85,19 +86,18 @@ async def _session(tmp_path: Path, roster, user_id: str = "alice"): async def test_device_messages_need_an_authenticated_session(tmp_path, roster): """ - Filing a device is not a pre-proof message, and must not become one. + Every device message but the request is answered on an authenticated session + only, and the request needs at least a challenge outstanding. - The pre-proof window exists for what a peer needs *in order to* prove - possession of the group key, and adding a device is not that: the request is - countersigned later by a device already pinned, so nothing is lost by - requiring the caller to finish its own handshake first. Left in the window it - would be a second thing a hub that forges a JWT could reach. + Looking up, approving, listing, revoking and saying hello are things a device + already pinned does, so it can finish its own handshake first and nothing is + lost. Filing is the exception (`test_a_new_device_files_before_the_proof`). Driven through the real dispatcher, because this is a property of the order of its branches and of nothing else. """ session = await _session(tmp_path, roster) - session._user_id = None # challenged, has not proved anything yet + session._user_id = None # has not proved anything yet _sk, pk_ed, pk_x = _keys() for mtype in ("device_add_request", "device_hello", "device_lookup", @@ -108,6 +108,70 @@ async def test_device_messages_need_an_authenticated_session(tmp_path, roster): assert _last(session) == {"type": "error", "detail": "Handshake required"}, mtype + session._gek_challenge = b"challenge outstanding" + for mtype in ("device_hello", "device_lookup", "device_add", "device_list", + "device_revoke"): + session.sent.clear() + session._dispatch_message({"type": mtype, "pk_ed25519": pk_ed, + "pk_x25519": pk_x}) + assert _last(session) == {"type": "error", + "detail": "Handshake required"}, mtype + + +async def test_a_new_device_files_before_the_proof(tmp_path, roster): + """ + A machine with no key on this node cannot prove the group key — the key is + wrapped for a pinned identity, and it has none — so the request that will + give it one is served in the pre-proof window, for the account the token + names. Filing admits nothing: the approval below is what does. + """ + import asyncio + 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) + session._user_id = None + session._username = None + session._pending_sub = "alice" + session._pending_username = "alice" + session._gek_challenge = b"challenge outstanding" + spawned = [] + session._spawn = lambda coro: spawned.append(asyncio.ensure_future(coro)) + session._audit_join = lambda *a, **k: None + + code = generate_code() + code_hash = device_code_hash(normalize_code(code), pk_new_ed, pk_new_x) + ts = int(time.time()) + transcript = device_request_transcript( + node_pk_b64=session._node_pk_b64(), user_id="alice", pk_ed25519_b64=pk_new_ed, + pk_x25519_b64=pk_new_x, code_hash=code_hash, nonce_node=NONCE, ts=ts) + session._dispatch_message({ + "type": "device_add_request", "pk_ed25519": pk_new_ed, "pk_x25519": pk_new_x, + "code_hash": code_hash, "ts": ts, + "sig": base64.b64encode(sk_new.sign(transcript)).decode()}) + await asyncio.gather(*spawned) + assert _last(session)["type"] == MNP.DEVICE_REQUEST_ACK + + # Inert until a pinned device countersigns. + assert await roster.find_device("alice", pk_new_ed) is None + pending = await roster.list_device_requests("alice") + assert [r["pk_ed25519"] for r in pending] == [pk_new_ed] + + +async def test_pending_device_requests_are_bounded_per_account(tmp_path, roster): + """Filing needs only a token, so the table it writes has a ceiling.""" + sk_old, pk_old_ed, pk_old_x = _keys() + await roster.pin_identity("alice", "alice", pk_old_ed, pk_old_x, "code") + filed = [] + for i in range(roster.MAX_PENDING_DEVICE_REQUESTS + 3): + _sk, pk_ed, pk_x = _keys() + await roster.file_device_request("alice", "alice", pk_ed, pk_x, f"hash-{i:02d}") + filed.append(f"hash-{i:02d}") + kept = {r["code_hash"] for r in await roster.list_device_requests("alice")} + # The oldest make room — filed within the same second, so by filing order. + assert kept == set(filed[-roster.MAX_PENDING_DEVICE_REQUESTS:]) + def _last(session): return session.sent[-1] if session.sent else {} |