summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_transport.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-13 04:10:14 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-13 04:10:14 +0200
commited9fb22ed703db38f9b07c00d17076f90aa4cbc8 (patch)
tree448af8bdc7734798607bb33735c2a8439c362c13 /packages/meshbay-node/tests/test_transport.py
parentee6573c57f721db8550e34e1c1c79c5922c62a4b (diff)
downloadmeshbay-ed9fb22ed703db38f9b07c00d17076f90aa4cbc8.tar.gz
fix(node)!: remove unauthenticated HTTP file API and TCP transport
Phase 11.5.A — findings C1 and C6 (see second-review.md). C1: the per-group HTTP file API bound 0.0.0.0 for every configured group, private ones included, and served two endpoints with no authentication at all: GET /index (full Mesh Group Index) and GET /file/{id} (raw plaintext file via FileResponse). Anyone able to reach the port — LAN, forwarded port, permissive IPv6 — read every private file. This bypassed the entire GEK-proof and node sovereignty layer. Deleted rather than patched: it duplicated MNP without any of its controls. C6: the TCP+TLS chunk server accepted a bare JWT with no GEK proof, leaving a second non-compliant handshake path. Deleted; QUIC remains and will be brought to parity with WebRTC by the unified handshake in 11.5.4. Transport decision recorded in transport/__init__.py: WebRTC/ICE is primary for browser and native clients (the only NAT traversal validated here — 2 ISPs, IPv4 STUN + IPv6, 4G CGNAT); QUIC is kept for LAN, port-forwarded and hub-less group:// access. punch_nat() is a direct-connection helper, not a traversal stack. Also removed server_ssl_context()/client_ssl_context() from tls_cert.py (no remaining callers) and a dead import of the former in quic_server.py. generate_self_signed_cert() stays: QUIC uses it, and the certificate hash is the intended channel-binding anchor for 11.5.6, since QUIC has no DTLS fingerprint to bind the GEK proof to. BREAKING CHANGE: node.toml keys `port` and `http_port` are gone. Regenerate config with `meshbay-node init`. Env var MESHBAY_PORT -> MESHBAY_QUIC_PORT. Tests: 198 passed (209 - 7 test_http_server - 4 test_transport). No other test changed status. Net -1300 lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_transport.py')
-rw-r--r--packages/meshbay-node/tests/test_transport.py222
1 files changed, 0 insertions, 222 deletions
diff --git a/packages/meshbay-node/tests/test_transport.py b/packages/meshbay-node/tests/test_transport.py
deleted file mode 100644
index 0e70d72..0000000
--- a/packages/meshbay-node/tests/test_transport.py
+++ /dev/null
@@ -1,222 +0,0 @@
-"""
-Integration test: ChunkServer ↔ ChunkClient over TLS.
-
-Starts a real TLS server on localhost, connects a client,
-fetches index and a chunk, verifies signature+hash+decryption.
-"""
-
-import asyncio
-import base64
-import os
-import time
-import jwt
-import pytest
-from pathlib import Path
-from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
-from cryptography.hazmat.primitives import serialization
-
-from meshbay_common.crypto import generate_gek, pk_to_b64
-from meshbay_node.indexer import DirectoryIndexer, GroupIndex
-from meshbay_node.transport.server import ChunkServer
-from meshbay_node.transport.client import ChunkClient
-
-
-@pytest.fixture
-def sk_node():
- return Ed25519PrivateKey.generate()
-
-@pytest.fixture
-def sk_hub():
- return Ed25519PrivateKey.generate()
-
-@pytest.fixture
-def gek():
- return generate_gek()
-
-@pytest.fixture
-def shared_dir(tmp_path):
- d = tmp_path / "shared"
- d.mkdir()
- (d / "test.mp4").write_bytes(os.urandom(2 * 1024 * 1024)) # 2 MB
- (d / "small.txt").write_bytes(b"hello meshbay " * 100)
- return d
-
-def make_jwt(sk_hub, pk_node_b64, user_id="user-001", ttl=3600, groups=None):
- sk_pem = sk_hub.private_bytes(
- serialization.Encoding.PEM,
- serialization.PrivateFormat.PKCS8,
- serialization.NoEncryption(),
- )
- now = int(time.time())
- return jwt.encode({
- "iss": "test-hub", "sub": user_id,
- "pk_user": pk_node_b64, "hub_id": "test-hub",
- "jti": "test-jti",
- "iat": now, "exp": now + ttl,
- "groups": groups or [],
- }, sk_pem, algorithm="EdDSA")
-
-
-@pytest.mark.asyncio
-async def test_chunk_server_client_roundtrip(
- sk_node, sk_hub, gek, shared_dir, tmp_path):
- """Full integration: server serves a chunk, client verifies and decrypts."""
-
- # Build index
- indexer = DirectoryIndexer(
- root=shared_dir, group_id="g", sk_node=sk_node, gek=gek)
- await indexer.initial_scan()
- assert indexer.index.count == 2
-
- # Hub PK for JWT verification
- hub_pk_pem = sk_hub.public_key().public_bytes(
- serialization.Encoding.PEM,
- serialization.PublicFormat.SubjectPublicKeyInfo)
-
- # TLS cert in tmp dir
- cert_path = tmp_path / "node.crt"
- key_path = tmp_path / "node.key"
-
- server = ChunkServer(
- sk_node=sk_node,
- hub_pk_pem=hub_pk_pem,
- gek=gek,
- shared_root=shared_dir,
- index=indexer.index,
- host="127.0.0.1",
- port=0, # OS picks a free port
- cert_path=cert_path,
- key_path=key_path,
- )
- await server.start()
- port = server._server.sockets[0].getsockname()[1]
-
- token = make_jwt(sk_hub, pk_to_b64(sk_node.public_key()))
-
- # Find the large test file in the index
- entry = next(e for e in indexer.index.entries if e.name == "test.mp4")
-
- async with ChunkClient(
- host="127.0.0.1",
- port=port,
- jwt_token=token,
- gek=gek,
- pk_node_b64=pk_to_b64(sk_node.public_key()),
- ) as client:
- # Fetch first chunk
- chunk0 = await client.fetch_chunk(entry.id, chunk_index=0)
- assert len(chunk0) == 1024 * 1024 # first 1MB of 2MB file
-
- # Fetch second chunk
- chunk1 = await client.fetch_chunk(entry.id, chunk_index=1)
- assert len(chunk1) == 1024 * 1024 # second 1MB
-
- # Reassembled file matches original
- original = (shared_dir / "test.mp4").read_bytes()
- assert chunk0 + chunk1 == original
-
- await server.stop()
-
-
-@pytest.mark.asyncio
-async def test_invalid_jwt_rejected(sk_node, sk_hub, gek, shared_dir, tmp_path):
- hub_pk_pem = sk_hub.public_key().public_bytes(
- serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
-
- indexer = DirectoryIndexer(root=shared_dir, group_id="g",
- sk_node=sk_node, gek=gek)
- await indexer.initial_scan()
-
- cert_path = tmp_path / "node.crt"
- key_path = tmp_path / "node.key"
-
- server = ChunkServer(
- sk_node=sk_node, hub_pk_pem=hub_pk_pem, gek=gek,
- shared_root=shared_dir, index=indexer.index,
- host="127.0.0.1", port=0,
- cert_path=cert_path, key_path=key_path,
- )
- await server.start()
- port = server._server.sockets[0].getsockname()[1]
-
- # Use a different hub key to sign the token
- sk_other_hub = Ed25519PrivateKey.generate()
- bad_token = make_jwt(sk_other_hub, pk_to_b64(sk_node.public_key()))
-
- with pytest.raises(Exception):
- async with ChunkClient(
- host="127.0.0.1", port=port,
- jwt_token=bad_token, gek=gek,
- pk_node_b64=pk_to_b64(sk_node.public_key()),
- ) as client:
- pass
-
- await server.stop()
-
-
-@pytest.mark.asyncio
-async def test_wrong_group_rejected(sk_node, sk_hub, gek, shared_dir, tmp_path):
- """TCP+TLS server rejects a client whose JWT groups don't include the requested group_id."""
- hub_pk_pem = sk_hub.public_key().public_bytes(
- serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
-
- indexer = DirectoryIndexer(root=shared_dir, group_id="g",
- sk_node=sk_node, gek=gek)
- await indexer.initial_scan()
-
- cert_path = tmp_path / "node.crt"
- key_path = tmp_path / "node.key"
-
- server = ChunkServer(
- sk_node=sk_node, hub_pk_pem=hub_pk_pem, gek=gek,
- shared_root=shared_dir, index=indexer.index,
- host="127.0.0.1", port=0,
- cert_path=cert_path, key_path=key_path,
- )
- await server.start()
- port = server._server.sockets[0].getsockname()[1]
-
- token = make_jwt(sk_hub, pk_to_b64(sk_node.public_key()), groups=["group-a"])
-
- with pytest.raises(ConnectionError, match="rejected"):
- async with ChunkClient(
- host="127.0.0.1", port=port,
- jwt_token=token, gek=gek,
- pk_node_b64=pk_to_b64(sk_node.public_key()),
- group_id="group-b",
- ) as client:
- pass
-
- await server.stop()
-
-
-@pytest.mark.asyncio
-async def test_fetch_index(sk_node, sk_hub, gek, shared_dir, tmp_path):
- hub_pk_pem = sk_hub.public_key().public_bytes(
- serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
- indexer = DirectoryIndexer(root=shared_dir, group_id="g",
- sk_node=sk_node, gek=gek)
- await indexer.initial_scan()
-
- cert_path = tmp_path / "node.crt"
- key_path = tmp_path / "node.key"
-
- server = ChunkServer(
- sk_node=sk_node, hub_pk_pem=hub_pk_pem, gek=gek,
- shared_root=shared_dir, index=indexer.index,
- host="127.0.0.1", port=0,
- cert_path=cert_path, key_path=key_path,
- )
- await server.start()
- port = server._server.sockets[0].getsockname()[1]
- token = make_jwt(sk_hub, pk_to_b64(sk_node.public_key()))
-
- async with ChunkClient(
- host="127.0.0.1", port=port, jwt_token=token,
- gek=gek, pk_node_b64=pk_to_b64(sk_node.public_key()),
- ) as client:
- wire = await client.fetch_index()
- recovered = GroupIndex.deserialize(wire, sk_node=sk_node, gek=gek)
- assert recovered.count == 2
-
- await server.stop()