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/quic_server.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/transport/quic_server.py') diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py index 036574b..96cd752 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py @@ -41,6 +41,7 @@ from meshbay_common.handshake import ( ROLE_NODE, HandshakeError, authorize_token, + challenge_transcript, check_version, handshake_transcript, make_proof, @@ -313,11 +314,23 @@ class _MNPServerProtocol(QuicConnectionProtocol): # Decoded but NOT authenticated: authentication is the GEK proof below. self._pending = peer self._gek_challenge = os.urandom(NONCE_LEN) + # The same announcement and signature as WebRTC's challenge (MNP 3.4), + # so the two transports stay one handshake. The binding is the node's + # certificate, which is known here as it is at the proof. + sig = {} + cert = self._ctx.get("server_cert_der") + if cert: + transcript = challenge_transcript( + peer.group_id, self._nonce_client, self._gek_challenge, + quic_binding(cert)) + sig = {"sig": base64.b64encode(self._ctx["sk_node"].sign(transcript)).decode()} self._send(stream_id, { - "type": MNP.HANDSHAKE_CHALLENGE, - "v": MNP_VERSION, - "v_min": MNP_MIN_SUPPORTED, - "nonce": base64.b64encode(self._gek_challenge).decode(), + "type": MNP.HANDSHAKE_CHALLENGE, + "v": MNP_VERSION, + "v_min": MNP_MIN_SUPPORTED, + "nonce": base64.b64encode(self._gek_challenge).decode(), + "node_pk": pk_to_b64(self._ctx["sk_node"].public_key()), + **sig, }) def _do_handshake_response_sync(self, stream_id: int, msg: dict) -> None: -- cgit v1.2.3