diff options
Diffstat (limited to 'poc')
| -rw-r--r-- | poc/spike1_crypto.py | 18 | ||||
| -rw-r--r-- | poc/spike2_hub.py | 11 | ||||
| -rw-r--r-- | poc/spike3_node.py | 20 | ||||
| -rw-r--r-- | poc/spike4_nat.py | 33 | ||||
| -rw-r--r-- | poc/spike5_client.py | 19 | ||||
| -rw-r--r-- | poc/spike5_node.py | 19 | ||||
| -rw-r--r-- | poc/spike6_gek.py | 27 |
7 files changed, 97 insertions, 50 deletions
diff --git a/poc/spike1_crypto.py b/poc/spike1_crypto.py index 335758c..4006506 100644 --- a/poc/spike1_crypto.py +++ b/poc/spike1_crypto.py @@ -4,17 +4,20 @@ MeshBay — Spike 1: Crypto Primitives Validates the full cryptographic stack needed for MeshBay. """ -import os, time, base64, sys +import base64 +import os +import sys +import time +import blake3 +import jwt +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.kdf.argon2 import Argon2id -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -import blake3 -import jwt +from cryptography.hazmat.primitives.kdf.hkdf import HKDF PASS = "✓" FAIL = "✗" @@ -240,7 +243,8 @@ print("\n=== Test 7: AES-256-GCM — Keystore encryption ===") def test_aes_gcm_keystore(): # Derive an AES key from Argon2id (as done for keystore unlock) salt = os.urandom(16) - aes_key = Argon2id(salt=salt, length=32, iterations=3, lanes=4, memory_cost=65536).derive(b"password") + aes_key = Argon2id(salt=salt, length=32, iterations=3, lanes=4, + memory_cost=65536).derive(b"password") # Encrypt a mock keystore blob keystore_data = b'{"sk_user": "base64...", "sk_group": "base64..."}' diff --git a/poc/spike2_hub.py b/poc/spike2_hub.py index f8c1b08..a0ebd6f 100644 --- a/poc/spike2_hub.py +++ b/poc/spike2_hub.py @@ -4,10 +4,15 @@ Minimal FastAPI hub: user registration, JWT issuance, node announcement. In-memory storage only — not persistent across restarts. """ -from fastapi import FastAPI, HTTPException, Depends, Header -from pydantic import BaseModel +import base64 +import os +import time +import uuid + +import jwt from cryptography.hazmat.primitives.kdf.argon2 import Argon2id -import jwt, uuid, os, time, base64 +from fastapi import Depends, FastAPI, Header, HTTPException +from pydantic import BaseModel app = FastAPI(title="MeshBay Hub POC", version="0.1.0") diff --git a/poc/spike3_node.py b/poc/spike3_node.py index c0ddde0..562239f 100644 --- a/poc/spike3_node.py +++ b/poc/spike3_node.py @@ -12,17 +12,23 @@ Sequence: 8. Simulate token refresh """ -import asyncio, httpx, base64, json, os, time +import asyncio +import base64 +import json +import time from pathlib import Path + +import httpx +import jwt +from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey -from cryptography.hazmat.primitives import serialization -import jwt HUB_URL = "http://meshbay.org" STATE_FILE = Path("node_state.json") # persists keys and tokens between runs -PASS = "✓"; FAIL = "✗" +PASS = "✓" +FAIL = "✗" # ── Key helpers ─────────────────────────────────────────────────────────────── @@ -52,7 +58,7 @@ def _load_or_generate_keys(state: dict) -> tuple: sk_x = X25519PrivateKey.generate() state["sk_ed25519_b64"] = _sk_to_b64(sk_ed) state["sk_x25519_b64"] = _sk_to_b64(sk_x) - print(f" Generated new keypair") + print(" Generated new keypair") return sk_ed, sk_x # ── Main ────────────────────────────────────────────────────────────────────── @@ -101,7 +107,7 @@ async def main(): state["user_id"] = r.json()["user_id"] print(f" {PASS} Registered — user_id={state['user_id'][:8]}...") elif r.status_code == 409: - print(f" Already registered (409) — continuing with existing account") + print(" Already registered (409) — continuing with existing account") else: raise RuntimeError(f"Register failed: {r.status_code} {r.text}") @@ -149,7 +155,7 @@ async def main(): node_id = r.json()["node_id"] state["node_id"] = node_id print(f" {PASS} Node announced — node_id={node_id[:8]}...") - print(f" endpoint_hint : None (STUN/UPnP discovery in Spike 4)") + print(" endpoint_hint : None (STUN/UPnP discovery in Spike 4)") # ── Step 7: Retrieve node record ────────────────────────────────────── print("\n[ 7 ] Retrieve node record from hub") diff --git a/poc/spike4_nat.py b/poc/spike4_nat.py index 44bcdca..ee7485f 100644 --- a/poc/spike4_nat.py +++ b/poc/spike4_nat.py @@ -12,10 +12,18 @@ Tests: E. Update hub endpoint_hint """ -import asyncio, socket, struct, os, json, httpx, time, subprocess +import asyncio +import json +import os +import socket +import struct from pathlib import Path -PASS = "✓"; FAIL = "✗"; SKIP = "–" +import httpx + +PASS = "✓" +FAIL = "✗" +SKIP = "–" LOCAL_PORT = 19000 MESHBAY_IP = "164.132.246.44" # meshbay.org resolved @@ -148,7 +156,7 @@ async def udp_hole_punch_test(local_port: int) -> tuple[bool, str|None, str|None try: await asyncio.wait_for(recv_event.wait(), timeout=8) - except asyncio.TimeoutError: + except TimeoutError: pass finally: transport.close() @@ -192,7 +200,7 @@ async def main(): print(f" {FAIL} {nat_type}") print(f" Cloudflare STUN : {stun_ip1}:{stun_port1}") print(f" Google STUN : {stun_ip2}:{stun_port2}") - print(f" → Hole punching unreliable; TURN relay required") + print(" → Hole punching unreliable; TURN relay required") elif stun_ip1: nat_type = "Unknown (only one STUN server responded)" print(f" {SKIP} {nat_type}") @@ -202,7 +210,7 @@ async def main(): print(f" {FAIL} {nat_type}") # ── Phase C: Bidirectional UDP hole punching ─────────────────────────────── - print(f"\n[ C ] Bidirectional UDP hole punch (node → meshbay.org → echo back)") + print("\n[ C ] Bidirectional UDP hole punch (node → meshbay.org → echo back)") print(f" Starting UDP echo server on meshbay.org:{MESHBAY_ECHO_PORT} ...") udp_ok = False seen_ext_addr = None @@ -223,7 +231,8 @@ async def main(): # May differ from STUN if symmetric NAT actual_ext = seen_ext_addr.replace("('", "").replace("'", "").replace(", ", ":") if endpoint_hint and actual_ext != endpoint_hint: - print(f" ⚠ STUN addr {endpoint_hint} ≠ actual {actual_ext} (symmetric NAT confirmed)") + print(f" ⚠ STUN addr {endpoint_hint} ≠ actual {actual_ext} " + f"(symmetric NAT confirmed)") endpoint_hint = actual_ext else: print(f" {FAIL} No echo received (timeout)") @@ -234,7 +243,7 @@ async def main(): print(f" {FAIL} Error: {e}") # ── Phase D: UPnP ───────────────────────────────────────────────────────── - print(f"\n[ D ] UPnP port mapping") + print("\n[ D ] UPnP port mapping") upnp_ip, upnp_port = try_upnp(LOCAL_PORT) if upnp_ip: print(f" {PASS} Mapped {upnp_ip}:{upnp_port}") @@ -243,7 +252,7 @@ async def main(): print(f" {FAIL} UPnP not available on this router") # ── Phase E: Update hub ──────────────────────────────────────────────────── - print(f"\n[ E ] Update endpoint_hint on hub") + print("\n[ E ] Update endpoint_hint on hub") if endpoint_hint and state.get("username"): async with httpx.AsyncClient(timeout=10) as client: r = await client.post(f"{HUB_URL}/v1/users/login", json={ @@ -270,10 +279,10 @@ async def main(): print(f" UPnP : {PASS + ' works' if upnp_ip else FAIL + ' disabled'}") print(f" endpoint_hint : {endpoint_hint or 'none'}") if udp_ok: - print(f"\n P2P UDP is functional. QUIC transport will work.") - print(f" Spike 4 COMPLETE.") + print("\n P2P UDP is functional. QUIC transport will work.") + print(" Spike 4 COMPLETE.") else: - print(f"\n P2P blocked — TURN relay needed for this configuration.") - print(f" Spike 4 COMPLETE (with finding: relay required).") + print("\n P2P blocked — TURN relay needed for this configuration.") + print(" Spike 4 COMPLETE (with finding: relay required).") asyncio.run(main()) diff --git a/poc/spike5_client.py b/poc/spike5_client.py index 11b3ac0..cc79ebf 100644 --- a/poc/spike5_client.py +++ b/poc/spike5_client.py @@ -12,16 +12,23 @@ Acts as the "file requester": - Reports timing and results """ -import asyncio, json, base64, struct, time, sys +import asyncio +import base64 +import json +import struct +import sys +import time + +import blake3 +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives import hashes -import blake3 PORT = 19003 CHUNK_INDEX = 0 -PASS = "✓"; FAIL = "✗" +PASS = "✓" +FAIL = "✗" # ── Wire helpers ─────────────────────────────────────────────────────────────── @@ -115,7 +122,7 @@ async def handle_node(reader, writer): total_ms = recv_ms + verify_ms print(f"\n {'='*50}") - print(f" Timings:") + print(" Timings:") print(f" Network receive : {recv_ms:.0f} ms") print(f" Verify+decrypt : {verify_ms:.1f} ms") print(f" Total : {total_ms:.0f} ms") @@ -133,7 +140,7 @@ async def main(): async with server: try: await asyncio.wait_for(server.serve_forever(), timeout=30) - except asyncio.TimeoutError: + except TimeoutError: print(f" {FAIL} Timeout — node did not connect within 30s") sys.exit(1) diff --git a/poc/spike5_node.py b/poc/spike5_node.py index 6104e00..cded922 100644 --- a/poc/spike5_node.py +++ b/poc/spike5_node.py @@ -14,20 +14,27 @@ Note: node initiates the TCP connection (reversed for POC — in production, the QUIC client connects to the node using the hole-punching from Spike 4). """ -import asyncio, json, base64, struct, os, time +import asyncio +import base64 +import json +import os +import struct +import time from pathlib import Path + +import blake3 +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives import hashes, serialization -import blake3 MESHBAY_HOST = "meshbay.org" MESHBAY_PORT = 19003 CHUNK_SIZE = 1024 * 1024 # 1 MB TEST_FILE = Path("testfile.bin") STATE_FILE = Path("node_state.json") -PASS = "✓"; FAIL = "✗" +PASS = "✓" +FAIL = "✗" # ── Wire helpers ─────────────────────────────────────────────────────────────── @@ -125,7 +132,7 @@ async def main(): print(f" {PASS} Test file: {TEST_FILE} ({file_size/1024/1024:.1f} MB)") # Pre-compute file hash (used in HKDF info string — identifies the file) - print(f" Computing file hash...") + print(" Computing file hash...") t0 = time.perf_counter() file_hash = blake3.blake3(TEST_FILE.read_bytes()).digest() print(f" {PASS} File hash: {file_hash.hex()[:16]}... ({(time.perf_counter()-t0)*1000:.0f}ms)") @@ -166,7 +173,7 @@ async def main(): print(f" {PASS} Sent {payload_kb:.1f} KB in {send_ms:.0f}ms ({throughput:.1f} MB/s)") print(f"\n {'='*50}") - print(f" Timings (node side):") + print(" Timings (node side):") print(f" Encrypt + sign : {enc_ms:.1f} ms") print(f" TCP send : {send_ms:.0f} ms") print(f" Total : {total_ms:.0f} ms") diff --git a/poc/spike6_gek.py b/poc/spike6_gek.py index 89ab06c..e58823b 100644 --- a/poc/spike6_gek.py +++ b/poc/spike6_gek.py @@ -16,18 +16,25 @@ Validates the full GEK lifecycle: The hub stores opaque encrypted blobs — it never sees the GEK in cleartext. """ -import asyncio, httpx, base64, os, json, time +import asyncio +import base64 +import json +import os +import time from pathlib import Path + +import blake3 +import httpx +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives import hashes, serialization -import blake3 HUB_URL = "http://meshbay.org" STATE_FILE = Path("node_state.json") -PASS = "✓"; FAIL = "✗" +PASS = "✓" +FAIL = "✗" # ── Key helpers ──────────────────────────────────────────────────────────────── @@ -194,7 +201,9 @@ async def main(): bundle_alice = wrap_gek(gek_raw, pk_alice_x_raw) print(f" {PASS} Wrapped in {(time.perf_counter()-t0)*1000:.2f}ms") print(f" pk_eph : {bundle_alice['pk_eph_b64'][:24]}...") - print(f" wrapped : {bundle_alice['wrapped_b64'][:24]}... ({len(base64.b64decode(bundle_alice['wrapped_b64']))}B)") + wrapped = bundle_alice["wrapped_b64"] + print(f" wrapped : {wrapped[:24]}... " + f"({len(base64.b64decode(wrapped))}B)") r = await c.post( f"{HUB_URL}/v1/groups/{group_id}/members/{state.get('username','node_cbesson')}/gek", @@ -218,7 +227,7 @@ async def main(): wrap_ms = (time.perf_counter()-t0)*1000 print(f" {PASS} Wrapped in {wrap_ms:.2f}ms") print(f" pk_eph : {bundle_bob['pk_eph_b64'][:24]}...") - print(f" (different from Alice's bundle — ephemeral key is unique)") + print(" (different from Alice's bundle — ephemeral key is unique)") r = await c.post( f"{HUB_URL}/v1/groups/{group_id}/members/bob_member/gek", @@ -301,10 +310,10 @@ async def main(): print(f" {PASS} Wrong private key correctly rejected (AEAD auth failed)") print("\n" + "="*55) - print(f"Spike 6 COMPLETE — GEK distribution validated.") + print("Spike 6 COMPLETE — GEK distribution validated.") print(f" wrap_gek : {wrap_ms:.2f} ms") print(f" unwrap_gek : {unwrap_ms:.2f} ms") - print(f" Hub role : stores opaque bundle, never sees GEK in clear") - print(f" Security : wrong key rejected by AEAD authentication tag") + print(" Hub role : stores opaque bundle, never sees GEK in clear") + print(" Security : wrong key rejected by AEAD authentication tag") asyncio.run(main()) |