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 --- .../meshbay-node/src/meshbay_node/transport/quic_client.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'packages/meshbay-node/src/meshbay_node/transport/quic_client.py') diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_client.py b/packages/meshbay-node/src/meshbay_node/transport/quic_client.py index 273f225..8f3fb74 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/quic_client.py +++ b/packages/meshbay-node/src/meshbay_node/transport/quic_client.py @@ -26,6 +26,7 @@ from meshbay_common.handshake import ( NONCE_LEN, ROLE_CLIENT, ROLE_NODE, + challenge_transcript, check_version, handshake_transcript, make_proof, @@ -211,6 +212,17 @@ class QuicChunkClient: "session — refusing to handshake without channel binding") binding = quic_binding(self._peer_cert_der) + # MNP 3.4: a signed challenge proves the node key before we send anything + # else. A wrong signature is refused; an absent one is an older node. + if reply.get("sig"): + try: + Ed25519PublicKey.from_public_bytes( + base64.b64decode(reply.get("node_pk", "")) + ).verify(base64.b64decode(reply["sig"]), challenge_transcript( + self._group_id, nonce_c, nonce_s, binding)) + except Exception as exc: + raise ConnectionError(f"Node challenge signature invalid: {exc}") from exc + self._proto._send(self._ctrl_stream, { "type": MNP.HANDSHAKE_RESPONSE, "v": MNP_VERSION, -- cgit v1.2.3