diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py | 37 |
1 files changed, 34 insertions, 3 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py index f701072..7822186 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py @@ -137,7 +137,8 @@ class HandshakeMixin: return {"sig": base64.b64encode(self._ctx["sk_node"].sign(transcript)).decode()} def _do_handshake_response(self, msg: dict) -> None: - if not self._gek_challenge or not hasattr(self, "_pending_sub"): + if not self._gek_challenge or not hasattr(self, "_pending_sub") \ + or getattr(self, "_admitting", False): self._send({"type": "error", "detail": "No pending handshake challenge"}) return @@ -170,8 +171,38 @@ class HandshakeMixin: self._audit_auth_failed(group_id, "GEK HMAC mismatch") return - self._complete_handshake(gek, binding) - self._gek_challenge = None + # One answer at a time: the roster is asked asynchronously, and a + # second response arriving meanwhile must not be taken as a new one. + self._admitting = True + self._spawn(self._admit(gek, binding, group_id)) + + async def _admit(self, gek: bytes, binding: bytes, group_id: str) -> None: + """ + Open the session only for someone this node's roster admits. + + The group-key proof says the peer holds the key, and the token says the + hub counts the account a member. Neither is the node's own answer — + and the roster is supposed to be the authority (§6.1). Without this, a + member revoked here, or unpinned, but still a member on the hub, kept + full access with the key they already held: files, uploads, and — since + chat keys are handed to any session — the very epoch their removal had + just opened. An honest client never met the gap, because it asks for + the key through `join_request`, which does consult the roster; a + client that kept the key did not have to. + """ + try: + roster = self._ctx.get("roster") + if roster is not None and not await roster.is_authorized( + group_id, self._pending_sub): + self._send({"type": "error", + "detail": "This node has not admitted you to this group", + "code": "not_authorized_for_group"}) + self._audit_auth_failed(group_id, "not admitted by the roster") + return + self._complete_handshake(gek, binding) + finally: + self._gek_challenge = None + self._admitting = False def _complete_handshake(self, gek: bytes, binding: bytes) -> None: # Authenticated peers may send large frames (file uploads); unauthenticated |