aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-23 17:14:26 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-23 17:14:26 +0200
commit339cb427f886a0177014126bb684335837eff067 (patch)
tree5f79dc0df617be66287a06fc4f0c5dcc61ceb167 /packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
parentcd85808c13926c89a97987d320ac26391eae3267 (diff)
downloadmeshbay-339cb427f886a0177014126bb684335837eff067.tar.gz
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 <noreply@anthropic.com>
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.py20
1 files changed, 20 insertions, 0 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 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"})