From f0248975908ad670fa8a820f865bf22ea8d0172d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 13 Aug 2026 03:56:30 +0200 Subject: feat: Phase 12 — P2P crypto material, password split, node Ed25519 auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Baseline commit capturing in-progress Phase 12 work that was already present in the working tree (uncommitted) before the Phase 11.5 security remediation begins. Committed as-is, without review or modification, so that remediation changes arrive as a separable diff. Contents: BundleStore (P2P GEK + keypair bundles), password split (auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin challenge-response, node local admin UI rewrite, browser key persistence. Not authored in this session — captured to establish a baseline. Co-Authored-By: Claude Opus 5 --- packages/meshbay-node/tests/test_daemon.py | 44 ++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) (limited to 'packages/meshbay-node/tests/test_daemon.py') diff --git a/packages/meshbay-node/tests/test_daemon.py b/packages/meshbay-node/tests/test_daemon.py index faf12e3..1c5a07e 100644 --- a/packages/meshbay-node/tests/test_daemon.py +++ b/packages/meshbay-node/tests/test_daemon.py @@ -7,11 +7,13 @@ Hub interaction is mocked. """ import asyncio +import base64 import os import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from unittest.mock import AsyncMock, MagicMock, patch from meshbay_common.crypto import generate_gek @@ -20,6 +22,20 @@ from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer +def _mock_keystore_keys(sk_ed): + """Create a mock keystore with real Ed25519 + X25519 key material.""" + sk_x = X25519PrivateKey.generate() + pk_x_raw = sk_x.public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw) + + mock_keys = MagicMock() + mock_keys.sk_ed25519 = sk_ed + mock_keys.pk_ed25519_b64 = "test" + mock_keys.sk_x25519 = sk_x + mock_keys.pk_x25519_b64 = base64.b64encode(pk_x_raw).decode() + return mock_keys + + @pytest.fixture def sk_hub(): return Ed25519PrivateKey.generate() @@ -48,7 +64,7 @@ def shared_dir(tmp_path): @pytest.fixture def node_config(tmp_path, shared_dir): return Config( - hub=HubConfig(url="http://localhost:9999", username="testuser", password="testpass"), + hub=HubConfig(url="http://localhost:9999", username="testuser"), node=NodeConfig(port=29000, quic_port=29010, http_port=29001, ui_port=28000), groups=[GroupConfig( id="g" * 32, @@ -70,10 +86,7 @@ async def test_daemon_creates_chat_store(tmp_path, node_config, gek, hub_pk_pem) daemon = NodeDaemon(node_config) sk_node = Ed25519PrivateKey.generate() - mock_keys = MagicMock() - mock_keys.sk_ed25519 = sk_node - mock_keys.pk_ed25519_b64 = "test" - mock_keys.pk_x25519_b64 = "test" + mock_keys = _mock_keystore_keys(sk_node) mock_session = MagicMock() mock_session.node_id = "node123" @@ -85,7 +98,6 @@ async def test_daemon_creates_chat_store(tmp_path, node_config, gek, hub_pk_pem) hub_instance = AsyncMock() hub_instance.startup = AsyncMock(return_value=mock_session) - hub_instance.fetch_gek = AsyncMock(return_value=gek) hub_instance.maintain_ws = AsyncMock() hub_instance.send_ws = AsyncMock() hub_instance._ws = None @@ -139,7 +151,7 @@ async def test_daemon_creates_chat_store(tmp_path, node_config, gek, hub_pk_pem) async def test_daemon_no_groups_exits(tmp_path): """Daemon with no valid groups exits cleanly.""" config = Config( - hub=HubConfig(url="http://localhost:9999", username="testuser", password="testpass"), + hub=HubConfig(url="http://localhost:9999", username="testuser"), node=NodeConfig(), groups=[GroupConfig(id="", name="empty", shared_dir="")], keystore=KeystoreConfig(path=tmp_path / "keystore.enc"), @@ -148,17 +160,22 @@ async def test_daemon_no_groups_exits(tmp_path): daemon = NodeDaemon(config) sk_node = Ed25519PrivateKey.generate() - mock_keys = MagicMock() - mock_keys.sk_ed25519 = sk_node - mock_keys.pk_ed25519_b64 = "test" + mock_keys = _mock_keystore_keys(sk_node) mock_session = MagicMock() mock_session.node_id = "node123" mock_session.user_id = "user123" mock_session.hub_pk_pem = b"pem" + mock_server = AsyncMock() + mock_server.serve = AsyncMock() + with patch("meshbay_node.daemon.load_or_create_keystore", return_value=mock_keys), \ - patch("meshbay_node.daemon.HubClient") as MockHub: + patch("meshbay_node.daemon.HubClient") as MockHub, \ + patch("meshbay_node.daemon.uvicorn") as mock_uvicorn: + + mock_uvicorn.Config = MagicMock() + mock_uvicorn.Server = MagicMock(return_value=mock_server) hub_instance = AsyncMock() hub_instance.startup = AsyncMock(return_value=mock_session) @@ -169,7 +186,6 @@ async def test_daemon_no_groups_exits(tmp_path): await daemon.run() - assert daemon._state["status"] == "starting" assert len(daemon._chat_stores) == 0 @@ -177,7 +193,7 @@ async def test_daemon_no_groups_exits(tmp_path): async def test_daemon_index_change_pushes_to_peers(tmp_path, shared_dir, gek, hub_pk_pem): """Index change callback pushes updated index to WebRTC peers.""" config = Config( - hub=HubConfig(url="http://localhost:9999", username="testuser", password="testpass"), + hub=HubConfig(url="http://localhost:9999", username="testuser"), node=NodeConfig(port=29000, quic_port=29010, http_port=29001, ui_port=28000), groups=[GroupConfig( id="a" * 32, @@ -228,7 +244,7 @@ async def test_daemon_index_change_skips_other_group_peers( ): """Index change only pushes to peers in the same group.""" config = Config( - hub=HubConfig(url="http://localhost:9999", username="testuser", password="testpass"), + hub=HubConfig(url="http://localhost:9999", username="testuser"), node=NodeConfig(), groups=[], keystore=KeystoreConfig(path=tmp_path / "keystore.enc"), -- cgit v1.2.3