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 | 45 |
1 files changed, 36 insertions, 9 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 370a8b4..0154319 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -45,11 +45,13 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import ( from meshbay_common import MNP_VERSION from meshbay_common.handshake import ( + MNP_MIN_SUPPORTED, NONCE_LEN, ROLE_CLIENT, ROLE_NODE, HandshakeError, authorize_token, + check_version, handshake_transcript, make_proof, verify_proof, @@ -87,6 +89,7 @@ from meshbay_common.device import ( device_code_hash, device_request_transcript, ) +from meshbay_common.groupbox import PURPOSE_ACK, seal from meshbay_common.join import ( JOIN_TTL, ROLE_MEMBER, @@ -609,6 +612,15 @@ class WebRTCPeerSession: group_id = msg.get("group_id", "") log.info("WebRTC handshake request: group=%s (peer=%s)", group_id[:8] if group_id else "none", self._peer_id) + # Before the token, and before anything is decided from it: a peer we + # cannot speak to is refused with a code it can act on, rather than + # served messages it will misread as missing fields (L2). + try: + check_version(msg.get("v", ""), msg.get("v_min", "")) + except HandshakeError as refusal: + self._send({"type": "error", "detail": str(refusal), + "code": refusal.code}) + return try: peer = authorize_token( msg.get("token", ""), @@ -655,6 +667,9 @@ class WebRTCPeerSession: self._send({ "type": MNP.HANDSHAKE_CHALLENGE, "v": MNP_VERSION, + # Our half of the range. The client refuses us on this rather than + # discovering the mismatch when a field it expected is not there. + "v_min": MNP_MIN_SUPPORTED, "nonce": base64.b64encode(self._gek_challenge).decode(), # Announced here because a first-time joiner needs it *before* the # ack: join_request signs a transcript naming this node, and someone @@ -729,13 +744,15 @@ class WebRTCPeerSession: gek, ROLE_NODE, self._group_id or "", self._nonce_client, self._gek_challenge or b"", binding) - ack = { - "type": MNP.HANDSHAKE_ACK, - "v": MNP_VERSION, - "node_pk": pk_to_b64(self._ctx["sk_node"].public_key()), - "proof": base64.b64encode(node_proof).decode(), - "sig": base64.b64encode( - self._ctx["sk_node"].sign(node_transcript)).decode(), + # Everything the client needs in order to *authenticate* us stays in clear — + # node_pk, proof and sig are what it checks before it would trust a + # decryption, so they cannot themselves be behind one. The configuration + # below is sealed under a GEK-derived subkey, which gives it an + # authentication tag from a key the hub does not hold. Until MNP 1.0 the + # signed transcript named no ack field at all, so is_node_admin, + # enabled_apps, video_root and the rest were authenticated by the DTLS + # channel and nothing else. + config = { "is_node_admin": self._is_node_admin(), # So the interface knows whether to offer uploading at all. Not a # permission — the node refuses regardless — but without it the @@ -790,10 +807,20 @@ class WebRTCPeerSession: }, } if node_user_id: - ack["node_user_id"] = node_user_id + config["node_user_id"] = node_user_id pk_x_b64 = self._ctx.get("pk_x25519_b64") if pk_x_b64: - ack["node_pk_x25519"] = pk_x_b64 + config["node_pk_x25519"] = pk_x_b64 + + ack = { + "type": MNP.HANDSHAKE_ACK, + "v": MNP_VERSION, + "node_pk": pk_to_b64(self._ctx["sk_node"].public_key()), + "proof": base64.b64encode(node_proof).decode(), + "sig": base64.b64encode( + self._ctx["sk_node"].sign(node_transcript)).decode(), + **seal(gek, PURPOSE_ACK, MNP.HANDSHAKE_ACK, self._group_id or "", config), + } self._send(ack) self._audit("handshake") |