summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_hub_client.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-13 03:56:30 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-13 03:56:30 +0200
commitf0248975908ad670fa8a820f865bf22ea8d0172d (patch)
treef4af64d36cacaccb4f6d13436e001aeb57e861e3 /packages/meshbay-node/tests/test_hub_client.py
parent35130e5528a52161630fd1c93572e1b2b7cd911b (diff)
downloadmeshbay-f0248975908ad670fa8a820f865bf22ea8d0172d.tar.gz
feat: Phase 12 — P2P crypto material, password split, node Ed25519 auth
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_hub_client.py')
-rw-r--r--packages/meshbay-node/tests/test_hub_client.py76
1 files changed, 13 insertions, 63 deletions
diff --git a/packages/meshbay-node/tests/test_hub_client.py b/packages/meshbay-node/tests/test_hub_client.py
index fe8a2af..2975ee8 100644
--- a/packages/meshbay-node/tests/test_hub_client.py
+++ b/packages/meshbay-node/tests/test_hub_client.py
@@ -2,7 +2,6 @@
Tests for meshbay_node.hub_client — uses httpx.MockTransport to avoid network.
"""
-import base64
import json
import os
import time
@@ -16,7 +15,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives import serialization
-from meshbay_common.crypto import generate_gek, pk_to_b64, wrap_gek
+
from meshbay_node.hub_client import HubClient, HubConfig, HubSession
from meshbay_node.keystore import NodeKeys
@@ -51,16 +50,15 @@ def hub_config(tmp_path):
return HubConfig(
hub_url="http://fake-hub",
username="testuser",
- password="testpass99",
cache_dir=tmp_path,
)
-def make_token(sk_pem, user_id, pk_user_b64, hub_id="fake-hub", ttl=3600):
+def make_node_token(sk_pem, user_id, pk_user_b64, hub_id="fake-hub", ttl=3600):
now = int(time.time())
return jwt.encode({
"iss": hub_id, "sub": user_id, "pk_user": pk_user_b64,
- "hub_id": hub_id, "jti": "test-jti",
+ "hub_id": hub_id, "jti": "test-jti", "scope": "node",
"iat": now, "exp": now + ttl,
}, sk_pem, algorithm="EdDSA")
@@ -71,14 +69,14 @@ def make_token(sk_pem, user_id, pk_user_b64, hub_id="fake-hub", ttl=3600):
async def test_login_verifies_jwt_offline(hub_keys, node_keys, hub_config):
sk_hub, sk_hub_pem, pk_hub_pem = hub_keys
user_id = "user-uuid-001"
- token = make_token(sk_hub_pem, user_id, node_keys.pk_ed25519_b64)
+ token = make_node_token(sk_hub_pem, user_id, node_keys.pk_ed25519_b64)
def handler(request):
if request.url.path == "/v1/hub/pubkey":
return httpx.Response(200, json={"pk_hub_pem": pk_hub_pem.decode()})
- if request.url.path == "/v1/users/login":
+ if request.url.path == "/v1/nodes/auth":
return httpx.Response(200, json={
- "access_token": token, "refresh_token": "rt-abc", "expires_in": 3600})
+ "access_token": token, "token_type": "bearer", "expires_in": 3600})
return httpx.Response(404)
transport = httpx.MockTransport(handler)
@@ -96,18 +94,18 @@ async def test_login_verifies_jwt_offline(hub_keys, node_keys, hub_config):
@pytest.mark.asyncio
async def test_login_rejects_missing_jti(hub_keys, node_keys, hub_config):
sk_hub, sk_hub_pem, pk_hub_pem = hub_keys
- # Token without jti
bad_token = jwt.encode({
"iss": "fake-hub", "sub": "uid", "pk_user": node_keys.pk_ed25519_b64,
- "hub_id": "fake-hub", "iat": int(time.time()), "exp": int(time.time()) + 3600,
+ "hub_id": "fake-hub", "scope": "node",
+ "iat": int(time.time()), "exp": int(time.time()) + 3600,
}, sk_hub_pem, algorithm="EdDSA")
def handler(request):
if request.url.path == "/v1/hub/pubkey":
return httpx.Response(200, json={"pk_hub_pem": pk_hub_pem.decode()})
- if request.url.path == "/v1/users/login":
+ if request.url.path == "/v1/nodes/auth":
return httpx.Response(200, json={
- "access_token": bad_token, "refresh_token": "rt", "expires_in": 3600})
+ "access_token": bad_token, "token_type": "bearer", "expires_in": 3600})
return httpx.Response(404)
transport = httpx.MockTransport(handler)
@@ -119,33 +117,16 @@ async def test_login_rejects_missing_jti(hub_keys, node_keys, hub_config):
@pytest.mark.asyncio
-async def test_register_idempotent(hub_keys, node_keys, hub_config):
- def handler(request):
- if request.url.path == "/v1/users/register":
- return httpx.Response(409, json={"detail": "Username already taken"})
- return httpx.Response(404)
-
- transport = httpx.MockTransport(handler)
- client = HubClient(hub_config, node_keys)
- client._http = httpx.AsyncClient(transport=transport, base_url="http://fake-hub")
-
- # Should not raise on 409
- result = await client.register()
- assert result == ""
-
-
-@pytest.mark.asyncio
async def test_token_needs_refresh(hub_keys, node_keys, hub_config):
sk_hub, sk_hub_pem, pk_hub_pem = hub_keys
- # Token expiring in 60s (< TOKEN_REFRESH_MARGIN of 300s)
- short_token = make_token(sk_hub_pem, "uid", node_keys.pk_ed25519_b64, ttl=60)
+ short_token = make_node_token(sk_hub_pem, "uid", node_keys.pk_ed25519_b64, ttl=60)
def handler(request):
if request.url.path == "/v1/hub/pubkey":
return httpx.Response(200, json={"pk_hub_pem": pk_hub_pem.decode()})
- if request.url.path == "/v1/users/login":
+ if request.url.path == "/v1/nodes/auth":
return httpx.Response(200, json={
- "access_token": short_token, "refresh_token": "rt", "expires_in": 60})
+ "access_token": short_token, "token_type": "bearer", "expires_in": 60})
return httpx.Response(404)
transport = httpx.MockTransport(handler)
@@ -157,37 +138,6 @@ async def test_token_needs_refresh(hub_keys, node_keys, hub_config):
@pytest.mark.asyncio
-async def test_fetch_gek(hub_keys, node_keys, hub_config):
- """Admin wraps GEK for this node; client fetches and unwraps."""
- sk_hub, sk_hub_pem, pk_hub_pem = hub_keys
- gek = generate_gek()
-
- # Simulate admin wrapping GEK for this node
- pk_x_raw = base64.b64decode(node_keys.pk_x25519_b64)
- bundle = wrap_gek(gek, pk_x_raw)
-
- token = make_token(sk_hub_pem, "uid", node_keys.pk_ed25519_b64)
-
- def handler(request):
- if request.url.path == "/v1/hub/pubkey":
- return httpx.Response(200, json={"pk_hub_pem": pk_hub_pem.decode()})
- if request.url.path == "/v1/users/login":
- return httpx.Response(200, json={
- "access_token": token, "refresh_token": "rt", "expires_in": 3600})
- if "/v1/groups/" in request.url.path and request.url.path.endswith("/gek"):
- return httpx.Response(200, json=bundle)
- return httpx.Response(404)
-
- transport = httpx.MockTransport(handler)
- client = HubClient(hub_config, node_keys)
- client._http = httpx.AsyncClient(transport=transport, base_url="http://fake-hub")
-
- await client.login()
- recovered = await client.fetch_gek("group-abc")
- assert recovered == gek
-
-
-@pytest.mark.asyncio
async def test_hub_pk_cached(hub_keys, node_keys, hub_config, tmp_path):
_, _, pk_hub_pem = hub_keys
call_count = {"n": 0}