aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_webrtc_transport.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_webrtc_transport.py')
-rw-r--r--packages/meshbay-node/tests/test_webrtc_transport.py149
1 files changed, 149 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_webrtc_transport.py b/packages/meshbay-node/tests/test_webrtc_transport.py
index 0d2aa3a..c0dc6d1 100644
--- a/packages/meshbay-node/tests/test_webrtc_transport.py
+++ b/packages/meshbay-node/tests/test_webrtc_transport.py
@@ -29,7 +29,9 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
from meshbay_common import MNP_VERSION
from meshbay_common.adminop import (
OP_FILE_DELETE,
+ OP_INVITE_CANCEL,
OP_INVITE_CREATE,
+ OP_INVITE_LINK_CREATE,
admin_transcript,
)
from meshbay_common.crypto import (
@@ -1406,6 +1408,153 @@ async def test_invite_then_join_delivers_the_gek(sk_node, sk_hub, gek, shared_di
await transport.close_all()
+async def _bearer_join(transport, sk_hub, user_id, peer_id, code, x25519_keypair,
+ expect_node_pk):
+ """A newcomer's first connection with a link code, as the browser makes it:
+ the challenge must prove the node key the link named before the code goes."""
+ sk_x_raw, pk_x_raw = x25519_keypair
+ sk_ed = Ed25519PrivateKey.generate()
+ pc, ch, q = await _open_channel(transport, peer_id)
+ nonce_c = os.urandom(NONCE_LEN)
+ ch.send(_pack({
+ "type": MNP.HANDSHAKE, "v": MNP_VERSION,
+ "token": _token(sk_hub, user_id, peer_id, TEST_GROUP),
+ "group_id": TEST_GROUP, "nonce": base64.b64encode(nonce_c).decode(),
+ }))
+ challenge = await asyncio.wait_for(q.get(), timeout=5.0)
+ nonce_s = base64.b64decode(challenge["nonce"])
+ assert challenge["node_pk"] == expect_node_pk
+ Ed25519PublicKey.from_public_bytes(base64.b64decode(expect_node_pk)).verify(
+ base64.b64decode(challenge["sig"]),
+ challenge_transcript(TEST_GROUP, nonce_c, nonce_s, webrtc_binding(
+ _extract_dtls_fp(pc.localDescription.sdp),
+ _extract_dtls_fp(pc.remoteDescription.sdp))))
+
+ pk_ed_b64 = pk_to_b64(sk_ed.public_key())
+ pk_x_b64 = base64.b64encode(pk_x_raw).decode()
+ ts = int(time.time())
+ ch.send(_pack({
+ "type": MNP.JOIN_REQUEST, "v": MNP_VERSION, "group_id": TEST_GROUP,
+ "pk_ed25519": pk_ed_b64, "pk_x25519": pk_x_b64, "code": code, "ts": ts,
+ "sig": base64.b64encode(sk_ed.sign(join_transcript(
+ node_pk_b64=challenge["node_pk"], group_id=TEST_GROUP, user_id=user_id,
+ pk_ed25519_b64=pk_ed_b64, pk_x25519_b64=pk_x_b64,
+ nonce_node=nonce_s, ts=ts))).decode(),
+ }))
+ result = await asyncio.wait_for(q.get(), timeout=5.0)
+ return pc, result, sk_x_raw, pk_x_raw
+
+
+@pytest.mark.asyncio
+async def test_a_link_is_issued_signed_redeemed_once_and_cancellable(
+ sk_node, sk_hub, gek, shared_dir, tmp_path, x25519_keypair):
+ """
+ Invitation links over a real DataChannel (docs/invite-links.md §3.4).
+
+ The operator signs `invite_link_create` naming the outcome, `link:<group>`;
+ the first account to bring the code is admitted and handed the key; the
+ second is refused; a link not yet used can be taken back, by its handle,
+ and is then refused too. Nothing is registered on the hub: there is no
+ account to register until somebody redeems it.
+ """
+ indexer = DirectoryIndexer(roots=one_root(shared_dir), group_id="g", sk_node=sk_node, gek=gek)
+ await indexer.initial_scan()
+ roster = Roster(db_path=tmp_path / "roster.db")
+ await roster.open()
+ transport = WebRTCTransport(
+ sk_node=sk_node, hub_pk_pem=_hub_pk_pem(sk_hub), gek=gek,
+ roots=one_root(shared_dir), index=indexer.index, stun_servers=[],
+ )
+ transport._ctx["roster"] = roster
+ transport._ctx["has_admin_authority"] = True
+ transport._ctx["groups"] = {
+ TEST_GROUP: {"gek": gek, "roots": one_root(shared_dir), "index": indexer.index},
+ }
+ hub = _InviteHub()
+ transport._ctx["daemon_state"] = {
+ "roster": roster, "groups_ctx": transport._ctx["groups"], "hub": hub,
+ }
+ sk_admin = Ed25519PrivateKey.generate()
+ await roster.pin_identity("user-001", "grenet", pk_to_b64(sk_admin.public_key()),
+ "AA==", "code")
+ await roster.set_member("", "user-001", ROLE_OPERATOR, "active", "local-cli")
+ pc_admin, ch_admin, q_admin = await _setup_peer(transport, sk_hub, gek, "peer-admin")
+ node_pk = pk_to_b64(sk_node.public_key())
+ peers = [pc_admin]
+
+ async def signed(request: dict, op: str, subject: str) -> dict:
+ ch_admin.send(_pack({"v": MNP_VERSION, **request}))
+ challenge = await asyncio.wait_for(q_admin.get(), timeout=5.0)
+ assert challenge["type"] == MNP.ADMIN_CHALLENGE, challenge
+ assert (challenge["op"], challenge["subject"]) == (op, subject)
+ ch_admin.send(_pack({
+ "type": MNP.ADMIN_RESPONSE, "v": MNP_VERSION, "op_id": challenge["op_id"],
+ "signature": base64.b64encode(
+ sk_admin.sign(_transcript_from(challenge))).decode(),
+ }))
+ return await asyncio.wait_for(q_admin.get(), timeout=5.0)
+
+ try:
+ link = await signed({"type": MNP.INVITE_LINK_CREATE, "group_id": TEST_GROUP},
+ OP_INVITE_LINK_CREATE, f"link:{TEST_GROUP}")
+ assert link["type"] == MNP.INVITE_LINK_RESULT, link
+ assert len(link["code"]) == 9 and len(link["invite_id"]) == 32
+ assert hub.added == [], "a link registers nobody on the hub"
+
+ pc, result, sk_x_raw, pk_x_raw = await _bearer_join(
+ transport, sk_hub, "user-003", "peer-first", link["code"], x25519_keypair,
+ node_pk)
+ peers.append(pc)
+ assert result["ok"] is True and result["gek"] is True, result
+ assert unwrap_gek_aes(result, sk_x_raw, pk_x_raw) == gek
+
+ pc, result, _, _ = await _bearer_join(
+ transport, sk_hub, "user-004", "peer-second", link["code"], x25519_keypair,
+ node_pk)
+ peers.append(pc)
+ assert result.get("reason") == "code_invalid"
+
+ spare = await signed({"type": MNP.INVITE_LINK_CREATE, "group_id": TEST_GROUP},
+ OP_INVITE_LINK_CREATE, f"link:{TEST_GROUP}")
+ done = await signed({"type": MNP.INVITE_CANCEL, "invite_id": spare["invite_id"]},
+ OP_INVITE_CANCEL, spare["invite_id"])
+ assert done.get("detail") == "invite_cancelled", done
+ pc, result, _, _ = await _bearer_join(
+ transport, sk_hub, "user-005", "peer-late", spare["code"], x25519_keypair,
+ node_pk)
+ peers.append(pc)
+ assert result.get("reason") == "code_invalid"
+ finally:
+ await roster.close()
+ for pc in peers:
+ await pc.close()
+ await transport.close_all()
+
+
+@pytest.mark.asyncio
+async def test_a_link_needs_the_operator(sk_node, sk_hub, gek, shared_dir, tmp_path):
+ """A member who is not the operator is refused before any challenge is issued."""
+ indexer = DirectoryIndexer(roots=one_root(shared_dir), group_id="g", sk_node=sk_node, gek=gek)
+ await indexer.initial_scan()
+ roster = Roster(db_path=tmp_path / "roster.db")
+ await roster.open()
+ transport = WebRTCTransport(
+ sk_node=sk_node, hub_pk_pem=_hub_pk_pem(sk_hub), gek=gek,
+ roots=one_root(shared_dir), index=indexer.index, stun_servers=[],
+ )
+ transport._ctx["roster"] = roster
+ transport._ctx["has_admin_authority"] = False
+ pc, ch, q = await _setup_peer(transport, sk_hub, gek, "peer-member")
+ try:
+ ch.send(_pack({"type": MNP.INVITE_LINK_CREATE, "v": MNP_VERSION}))
+ reply = await asyncio.wait_for(q.get(), timeout=5.0)
+ assert reply["type"] == "error" and "operator" in reply["detail"]
+ assert await roster.list_invites() == []
+ finally:
+ await roster.close()
+ await pc.close()
+ await transport.close_all()
+
@pytest.mark.asyncio
async def test_gek_bundle_fetch_during_handshake(sk_node, sk_hub, gek, shared_dir,
tmp_path, x25519_keypair):