aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
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-hub/src/meshbay_hub/static/transport.js
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-hub/src/meshbay_hub/static/transport.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js36
1 files changed, 34 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 39e0ccf..c43422f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -883,10 +883,19 @@ class MeshBayTransport {
//
// nonce_node ties a join to this connection, so one cannot be lifted onto
// another. node_pk is announced here because a first-time member has no
- // GEK and so cannot complete the handshake that would prove it; it is
- // unverified at this point and checked against the ack below.
+ // GEK and so cannot complete the handshake that would prove it. From an
+ // older node it is unverified until the ack below checks it.
this._nonceNode = window.MeshBayCrypto.b64decode(reply.nonce);
this.nodePk = reply.node_pk || null;
+ // Since MNP 3.4 the node signs its challenge over this connection, so
+ // node_pk is proved here and not only at the ack — which comes after any
+ // join. A signature that does not verify is a peer lying about which node
+ // it is, and is refused. An absent one is an older node: `nodePkProved`
+ // stays false, and whatever needs the key proved before a code leaves
+ // (an invitation link names its node) reads that — never a version.
+ this.nodePkProved = await _challengeProvesNodeKey(
+ reply, groupId || '', this._nonceClient,
+ this._pc.localDescription.sdp, this._rawAnswerSdp);
// Our identity for THIS node: fetched from it, or created if this is a
// first join. Keys are per node, so there is nothing to carry between
@@ -3972,6 +3981,29 @@ function _hex(bytes) {
return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
}
+/**
+ * Whether `handshake_challenge` proves the key it announces (MNP 3.4).
+ *
+ * True when it carries a signature that verifies over this connection, false
+ * when it carries none — an older node, which proves its key only at the ack.
+ * A signature that does not verify is a peer lying about which node it is, and
+ * throws: that is a refusal, not a node that merely cannot say.
+ */
+async function _challengeProvesNodeKey(reply, groupId, nonceClient, offerSdp, answerSdp) {
+ if (!reply.sig) return false;
+ const C = window.MeshBayCrypto;
+ let ok = false;
+ try {
+ ok = Boolean(reply.node_pk) && await C.verifyNodeSignature(
+ reply.node_pk, reply.sig,
+ C.challengeTranscript(groupId, nonceClient, C.b64decode(reply.nonce),
+ C.webrtcBinding(_extractDtlsFingerprint(offerSdp),
+ _extractDtlsFingerprint(answerSdp))));
+ } catch { ok = false; }
+ if (!ok) throw new Error('Node challenge signature invalid — refusing connection');
+ return true;
+}
+
function _extractDtlsFingerprint(sdp) {
const match = sdp.match(/a=fingerprint:sha-256 ([0-9A-Fa-f:]+)/);
if (!match) return new Uint8Array(0);