aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/tests/test_js_python_parity.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-common/tests/test_js_python_parity.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-common/tests/test_js_python_parity.py')
-rw-r--r--packages/meshbay-common/tests/test_js_python_parity.py30
1 files changed, 27 insertions, 3 deletions
diff --git a/packages/meshbay-common/tests/test_js_python_parity.py b/packages/meshbay-common/tests/test_js_python_parity.py
index 3887414..5bf34aa 100644
--- a/packages/meshbay-common/tests/test_js_python_parity.py
+++ b/packages/meshbay-common/tests/test_js_python_parity.py
@@ -22,7 +22,11 @@ from pathlib import Path
import pytest
from meshbay_common.adminop import admin_transcript
-from meshbay_common.handshake import handshake_transcript, webrtc_binding
+from meshbay_common.handshake import (
+ challenge_transcript,
+ handshake_transcript,
+ webrtc_binding,
+)
from meshbay_common.join import join_transcript
CRYPTO_JS = (Path(__file__).resolve().parents[2]
@@ -76,7 +80,7 @@ globalThis.crypto = globalThis.crypto || {};
const src = fs.readFileSync(process.argv[2], 'utf8');
const load = new Function(
- src + '\nreturn { handshakeTranscript, adminTranscript, joinTranscript, '
+ src + '\nreturn { handshakeTranscript, challengeTranscript, adminTranscript, joinTranscript, '
+ 'webrtcBinding, b64encode };');
const M = load();
@@ -89,7 +93,7 @@ const toHex = (u8) =>
Array.from(u8).map(b => b.toString(16).padStart(2, '0')).join('');
const input = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
-const out = { handshake: [], admin: [], join: [] };
+const out = { handshake: [], challenge: [], admin: [], join: [] };
for (const v of input.handshake) {
const binding = M.webrtcBinding(hex(v.offer_fp), hex(v.answer_fp));
@@ -97,6 +101,12 @@ for (const v of input.handshake) {
v.role, v.group_id, hex(v.nonce_c), hex(v.nonce_s), binding)));
}
+for (const v of input.handshake) {
+ const binding = M.webrtcBinding(hex(v.offer_fp), hex(v.answer_fp));
+ out.challenge.push(toHex(M.challengeTranscript(
+ v.group_id, hex(v.nonce_c), hex(v.nonce_s), binding)));
+}
+
for (const v of input.admin) {
out.admin.push(toHex(M.adminTranscript(
v.op, v.node_pk, v.group_id, v.subject, M.b64encode(hex(v.nonce)), v.ts)));
@@ -167,6 +177,20 @@ def test_handshake_transcript_parity(idx, vector, js_output):
)
+@pytest.mark.parametrize("idx,vector", list(enumerate(HANDSHAKE_VECTORS)))
+def test_challenge_transcript_parity(idx, vector, js_output):
+ """
+ MNP 3.4. A mismatch means every browser refuses every node that signs its
+ challenge — the signature is checked, and a wrong one is a refusal.
+ """
+ _, group_id, nonce_c, nonce_s, offer_fp, answer_fp = vector
+ expected = challenge_transcript(
+ group_id, bytes.fromhex(nonce_c), bytes.fromhex(nonce_s),
+ webrtc_binding(bytes.fromhex(offer_fp), bytes.fromhex(answer_fp)))
+ assert js_output["challenge"][idx] == expected.hex(), (
+ f"crypto.js and meshbay_common.handshake disagree for group={group_id!r}")
+
+
@pytest.mark.parametrize("idx,vector", list(enumerate(ADMIN_VECTORS)))
def test_admin_transcript_parity(idx, vector, js_output):
"""