From 339cb427f886a0177014126bb684335837eff067 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 23 Sep 2026 17:14:26 +0200 Subject: feat: the node signs its handshake challenge (MNP 3.4) node_pk in handshake_challenge is now signed over the channel binding and both nonces, so a client can check the node key before a join rather than only at the ack. Both transports; the browser and the QUIC client refuse a wrong signature and treat an absent one as an older node. Co-Authored-By: Claude Opus 5.5 --- .../src/meshbay_node/transport/webrtc_server.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py') 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 a5e15df..809c5c5 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -99,6 +99,7 @@ from meshbay_common.handshake import ( ROLE_NODE, HandshakeError, authorize_token, + challenge_transcript, check_version, handshake_transcript, make_proof, @@ -962,8 +963,27 @@ class WebRTCPeerSession: # the two match, and a wrong value only makes our own verification # fail. It is never a substitute for the ack's proof and signature. "node_pk": self._node_pk_b64(), + # ...except that since 3.4 it is signed, so a client that already + # knows which key to expect can check it before it sends a code. + **self._challenge_sig(peer.group_id, self._channel_binding()), }) + def _challenge_sig(self, group_id: str, binding: bytes) -> dict: + """ + `{"sig": ...}` over the challenge transcript, or nothing (MNP 3.4). + + What makes `node_pk` above more than an announcement: a client about to + send an invitation code can check this node holds the key it was told + to expect, before the code leaves. No binding means no signature rather + than an unbound one — a signature that is not tied to the channel is one + somebody can relay, and the handshake proof refuses that case anyway. + """ + if not binding: + return {} + transcript = challenge_transcript( + group_id, self._nonce_client, self._gek_challenge, binding) + 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"): self._send({"type": "error", "detail": "No pending handshake challenge"}) -- cgit v1.2.3