diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 64 |
1 files changed, 62 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 4e4a23f..dfabe9b 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -99,7 +99,12 @@ from meshbay_common.device import ( device_hello_transcript, device_request_transcript, ) -from meshbay_common.groupbox import PURPOSE_ACK, PURPOSE_CHAT_KEYS, seal +from meshbay_common.groupbox import ( + PURPOSE_ACK, + PURPOSE_CHAT_KEYS, + PURPOSE_ROSTER, + seal, +) from meshbay_common.join import ( JOIN_TTL, ROLE_MEMBER, @@ -571,6 +576,8 @@ class WebRTCPeerSession: self._do_chat_epoch(msg) elif mtype == MNP.CHAT_KEYS_REQ: self._spawn(self._do_chat_keys_req(msg)) + elif mtype == MNP.GROUP_ROSTER_REQ: + self._spawn(self._do_group_roster_req(msg)) elif mtype == MNP.MEDIA_META_REQ: self._spawn(self._do_media_meta_request(msg)) elif mtype == MNP.SEASON_META_REQ: @@ -1456,10 +1463,23 @@ class WebRTCPeerSession: "detail": "That request is no longer pending"}) return + # The countersignature is **kept**, with the two fields needed to rebuild + # what it signed. Until 2026-09-07 it was verified here and thrown away, + # leaving only `added_by_pk` — which says *which* key approved and + # proves nothing to anyone else. `device_add_transcript` binds + # `nonce_node`, this connection's handshake nonce, so a stored signature + # without it is still unverifiable; that is why all three go in. + # + # This is what lets another member check for themselves that this device + # belongs to an account whose earlier device they have already pinned, + # instead of taking the node's word (Tier 2, desktop-client-v1.md §4.8). await roster.pin_identity( user_id=self._user_id, username=self._username or "", pk_ed25519=pk_ed_b64, pk_x25519=pk_x_b64, via="device", - label=str(msg.get("label", ""))[:64], added_by_pk=signer) + label=str(msg.get("label", ""))[:64], added_by_pk=signer, + add_sig=str(msg.get("sig", "")), + add_nonce=base64.b64encode(self._nonce_node).decode(), + add_ts=ts) self._audit("device_added", f"{pk_ed_b64[:16]} by {signer[:16]}") log.info("Device added for %s: %s (approved by %s)", self._user_id[:8], pk_ed_b64[:16], signer[:16]) @@ -2522,6 +2542,46 @@ class WebRTCPeerSession: self._broadcast_to_group({"type": MNP.CHAT_EPOCH_ACK, "v": MNP_VERSION, "epoch": result["epoch"]}) + async def _do_group_roster_req(self, msg: dict) -> None: + """ + Who is in this group, and which device keys they hold. + + Answers **any member**, not only the operator — that is the whole point. + A member verifies for themselves that a message came from a device + belonging to the account it claims, instead of taking the node's + `sender_id` on trust. What makes that possible is relayed here: each + device's key, which already-pinned key countersigned it, and the + signature plus the nonce and timestamp needed to rebuild what was + signed. + + Sealed under a GEK-derived subkey, for the same reason the index is: it + is the group's membership, and a peer that has not completed the + handshake has no business reading it. + + What this deliberately does not do is *decide* anything. The node hands + over evidence; the client checks the chain and keeps its own pins. A + node that lies here is caught by a client that has seen the account + before, which is the property Tier 2 buys and the reason the node is not + asked to assert trust. + """ + gctx = self._group_ctx() + gek = gctx.get("gek") + roster = self._ctx.get("roster") + if not gek: + self._send({"type": "error", "detail": "Group encryption not initialized"}) + return + if roster is None: + self._send({"type": "error", "detail": "Roster not available"}) + return + + devices = await roster.group_devices(self._group_id or "") + payload = {"devices": devices, + "node_pk": self._node_pk_b64()} + sealed = seal(gek, PURPOSE_ROSTER, MNP.GROUP_ROSTER_RESP, + self._group_id or "", payload) + self._send({"type": MNP.GROUP_ROSTER_RESP, "v": MNP_VERSION, + "group_id": self._group_id or "", **sealed}) + async def _do_chat_keys_req(self, msg: dict) -> None: """ Hand this member every chat epoch key the group has, sealed. |