aboutsummaryrefslogtreecommitdiffstats
path: root/poc/spike-results.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-09 03:52:58 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-09 03:52:58 +0200
commit271adc8504aad32075d75d06fd42023877a649ec (patch)
treefe8697761d635a5cac7e0693f2e588a38a7968b9 /poc/spike-results.md
downloadmeshbay-271adc8504aad32075d75d06fd42023877a649ec.tar.gz
chore: initialize monorepo structure for MeshBay
3-package layout: meshbay-common (shared crypto/protocol), meshbay-hub (FastAPI server), meshbay-node (local daemon). Includes validated POC spikes 1-6 in poc/, architecture drafts v1/v2 in docs/, and CLAUDE.md project conventions. All cryptographic primitives extracted from POC into meshbay_common/crypto.py (GEK wrap/unwrap, chunk key derivation, keystore encryption, chunk signing). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'poc/spike-results.md')
-rw-r--r--poc/spike-results.md266
1 files changed, 266 insertions, 0 deletions
diff --git a/poc/spike-results.md b/poc/spike-results.md
new file mode 100644
index 0000000..6e3cbb9
--- /dev/null
+++ b/poc/spike-results.md
@@ -0,0 +1,266 @@
+# MeshBay POC — Spike Results
+
+---
+
+## Spike 1 — Crypto Primitives
+**Date:** 2026-08-09
+**Machine:** Fedora 44 local (cbesson laptop)
+**Python:** 3.14.6
+**Libs:** cryptography 50.0.0, PyJWT 2.13.0, blake3 1.0.9
+
+### Results: 22/22 PASSED
+
+| Test | Result |
+|---|---|
+| Ed25519 sign + verify | ✓ signature 64 bytes |
+| Ed25519 tampered message rejected | ✓ |
+| Ed25519 PEM serialization | ✓ sk=119B pk=113B raw=32B |
+| X25519 two-party agreement | ✓ shared secret 32 bytes, both sides match |
+| X25519 different pairs ≠ same secret | ✓ |
+| ChaCha20-Poly1305 roundtrip (1 MB) | ✓ AEAD overhead=16B |
+| ChaCha20-Poly1305 performance | ✓ **avg 1.1ms/chunk, 1750 MB/s** |
+| ChaCha20-Poly1305 tampered ciphertext rejected | ✓ |
+| HKDF 3 distinct chunk keys | ✓ each 32 bytes |
+| HKDF deterministic derivation | ✓ |
+| blake3 1 MB performance | ✓ **0.3ms** |
+| blake3 deterministic | ✓ |
+| blake3 collision resistance (basic) | ✓ |
+| Argon2id key derivation | ✓ 78ms (⚠ see note) |
+| Argon2id salt uniqueness | ✓ |
+| Argon2id verify correct/wrong password | ✓ |
+| AES-256-GCM keystore roundtrip | ✓ 49B → 49B + 16B tag |
+| JWT EdDSA issue + verify offline | ✓ token 335 chars |
+| JWT tampered rejected | ✓ |
+| JWT wrong signing key rejected | ✓ |
+| JWT expired rejected | ✓ |
+| Full pipeline: encrypt+sign / verify+decrypt (1 MB) | ✓ **1.2ms / 1.3ms** |
+
+### Key Figures
+
+| Metric | Value | Note |
+|---|---|---|
+| ChaCha20-Poly1305 (1 MB, encrypt+decrypt) | 1.1 ms / 1750 MB/s | Encryption is not the bottleneck |
+| blake3 (1 MB) | 0.3 ms | Near-instant hashing |
+| Full pipeline (1 MB chunk) | 2.5 ms total | Well within all requirements |
+| Argon2id (iterations=3, memory=64MB) | 78 ms | ⚠ Too fast for production keystore |
+| JWT EdDSA token | 335 chars, offline verify | Hub not needed after login |
+
+### Notes / Actions
+
+- **Argon2id at 78ms is too weak for a production keystore.** Parameters need tuning to target 500ms–1s on the node hardware. Increase `memory_cost` (e.g. 262144 = 256MB) or `iterations`. To calibrate during implementation.
+- All crypto primitives confirmed available in `cryptography` 50.0.0 — no gaps.
+- PyJWT 2.13.0 EdDSA support works correctly with PEM keys.
+- blake3 PyPI package required (not in stdlib as of Python 3.14).
+
+---
+
+## Spike 2 — Hub Skeleton (meshbay.org)
+**Date:** 2026-08-09
+**Machine:** meshbay.org — Ubuntu 26.04 LTS, Python 3.14.4
+**Libs:** fastapi 0.115, uvicorn, cryptography 50.0.0, PyJWT 2.13.0
+**Deployment:** uvicorn sur port 80 via authbind (UFW actif — ports 22/80/443)
+
+### Results: 12/12 PASSED
+
+| Test | Result |
+|---|---|
+| GET /v1/hub/info | ✓ hub_id, versions, compteurs |
+| GET /v1/hub/pubkey | ✓ PEM 113B retourné |
+| POST /v1/users/register | ✓ user_id UUID retourné |
+| register doublon | ✓ → 409 Conflict |
+| POST /v1/users/login | ✓ access_token 364 chars + refresh_token |
+| login mauvais mot de passe | ✓ → 401 |
+| JWT vérifié OFFLINE (clé publique hub seulement) | ✓ sub, hub_id, pk_user corrects, expiry 3600s |
+| POST /v1/users/token/refresh | ✓ nouveau token valide |
+| POST /v1/nodes/announce (auth) | ✓ node_id UUID retourné |
+| GET /v1/nodes/{id} (auth) | ✓ pk_node et endpoint_hint corrects |
+| GET /v1/nodes/{id} sans auth | ✓ → 422 |
+| /v1/hub/info mise à jour (users=1, nodes=1) | ✓ |
+
+### Notes / Actions
+
+- **authbind** requis pour lier port 80 sans root sur Ubuntu avec UFW actif.
+ PREROUTING iptables + uvicorn sur 8000 ne fonctionne pas : UFW bloque le port 8000 en INPUT après le DNAT.
+- Hub joignable publiquement sur http://meshbay.org depuis le laptop Fedora.
+- JWT offline verify : concept clé validé — le node n'a **aucun besoin de contacter le hub** pour authentifier un client après la phase de login.
+- Hub en mémoire uniquement — redémarrer le hub efface users/nodes/tokens (attendu pour le POC).
+- **À faire avant production :** HTTPS (Caddy + Let's Encrypt), persistance DB (PostgreSQL).
+
+---
+
+## Spike 3 — Node Registration
+**Date:** 2026-08-09
+**Machine:** Fedora 44 local → meshbay.org
+**Python:** 3.14.6 local / 3.14.4 remote
+
+### Results: 8/8 PASSED
+
+| Test | Result |
+|---|---|
+| Génération keypair Ed25519 + X25519 | ✓ |
+| Fetch et cache clé publique hub (1 seul appel réseau) | ✓ 113B PEM |
+| Enregistrement utilisateur sur hub | ✓ user_id UUID |
+| Login → access token + refresh token | ✓ token 364 chars |
+| **JWT vérifié OFFLINE (clé publique hub uniquement)** | ✓ **884 µs — aucun appel hub** |
+| Annonce du node (endpoint_hint=None) | ✓ node_id UUID |
+| Récupération du node record depuis hub | ✓ pk_node et username corrects |
+| Token refresh → nouveau token vérifié offline | ✓ jti différent, même sub |
+
+### Notes / Actions
+
+- **Bug découvert et corrigé :** le hub n'incluait pas de champ `jti` (JWT ID) dans les tokens.
+ Sans `jti`, deux tokens émis dans la même seconde sont identiques (Ed25519 est déterministe sur un payload identique).
+ Fix : ajout d'un `uuid4()` en `jti` à chaque émission — chaque token est désormais unique même à la même seconde. Le `jti` permettra aussi la révocation individuelle de tokens en production.
+- **JWT offline verify à 884 µs** : concept fondamental validé — le node n'a jamais besoin de contacter le hub pour authentifier un client.
+- L'état du node (keypairs, tokens, node_id) est persisté dans `node_state.json` pour les spikes suivants.
+- `endpoint_hint=None` pour l'instant — sera rempli par le résultat du Spike 4 (STUN/UPnP).
+
+---
+
+## Spike 4 — NAT Traversal
+**Date:** 2026-08-09
+**Machine:** Fedora 44 (SFR résidentiel) → meshbay.org (OVH VPS)
+**Méthodes testées:** STUN (2 serveurs), détection type NAT, hole punching UDP bidirectionnel, UPnP
+
+### Results: PASSED (P2P UDP fonctionnel)
+
+| Test | Résultat |
+|---|---|
+| STUN via stun.cloudflare.com | ✓ 81.220.170.32:51250 |
+| STUN via stun.l.google.com | ✓ 81.220.170.32:51250 (même port) |
+| **Détection type NAT** | ✓ **Cone NAT** — même port externe pour les deux destinations |
+| **UDP bidirectionnel hole punch** | ✓ node → meshbay.org → echo reçu |
+| meshbay.org a vu le node comme | ✓ 81.220.170.32:51250 (confirme STUN) |
+| UPnP | ✗ désactivé sur la box SFR |
+| endpoint_hint hub | ✓ 81.220.170.32:51250 |
+
+### Findings clés
+
+- **Cone NAT confirmé** : le port externe (51250) est identique quelle que soit la destination (Cloudflare STUN ou Google STUN). Le hole punching UDP fonctionne donc sans TURN relay.
+- **UDP P2P opérationnel** : echo reçu de meshbay.org après probe sortant. Confirme que QUIC (UDP) peut fonctionner en P2P depuis cette configuration SFR résidentielle.
+- **UPnP désactivé** sur la box SFR testée — pas bloquant grâce au Cone NAT.
+- **Adresse externe stable** : 81.220.170.32 (IP publique SFR, pas de CGNAT).
+
+### Bugs découverts et corrigés
+
+- `seen_ext_addr` capturait l'IP source de l'écho (meshbay.org:19002) au lieu de notre adresse externe — confusion entre "qui m'a répondu" et "comment ils m'ont vu". Le log serveur (`RECEIVED from ('81.220.170.32', 51250)`) est la source de vérité.
+- `endpoint_hint` avait une parenthèse parasite due à la conversion de tuple. Corrigé en `node_state.json`.
+
+### Implication pour le design
+
+Le Mesh Relay (TURN) sera nécessaire uniquement pour les utilisateurs derrière **NAT symétrique** (typiquement : CGNAT mobile, certains FAI pro). Pour les connexions résidentielles standard (SFR, Orange, Free, etc.), le Cone NAT permet le hole punching direct → P2P sans relay.
+
+---
+
+## Spike 6 — GEK Distribution (X25519 + HKDF)
+**Date:** 2026-08-09
+**Machine:** Fedora 44 local → meshbay.org
+**Protocole :** ECIES-like : X25519 ephémère + HKDF + ChaCha20-Poly1305 + AAD
+
+### Results: 10/10 PASSED
+
+| Test | Résultat |
+|---|---|
+| Alice enregistrée sur hub | ✓ |
+| Bob enregistré sur hub (avec pk_x25519) | ✓ |
+| Alice crée groupe sur hub | ✓ |
+| Alice wraps GEK pour elle-même (bundle opaque sur hub) | ✓ 1.20ms |
+| Alice fetch pk_x25519 de Bob depuis hub | ✓ clé correcte |
+| Alice wraps GEK pour Bob (bundle opaque différent) | ✓ 0.48ms |
+| Bob récupère son bundle depuis hub | ✓ bundle intact |
+| **Bob unwrap GEK avec sa sk_x25519** | ✓ **0.59ms** |
+| **recovered_gek == original_gek** | ✓ **RÉSULTAT CLÉ** |
+| Bob déchiffre contenu Alice avec GEK récupérée | ✓ |
+| Mauvaise clé privée rejetée (AEAD auth tag) | ✓ |
+
+### Timings
+
+| Opération | Durée |
+|---|---|
+| wrap_gek (X25519 + HKDF + ChaCha20) | 0.48–1.20 ms |
+| unwrap_gek (X25519 + HKDF + ChaCha20) | 0.59 ms |
+
+### Protocole validé (ECIES-like)
+
+```
+Admin side (wrap):
+ sk_eph, pk_eph = X25519.generate()
+ shared = X25519(sk_eph, pk_recipient)
+ wrap_key = HKDF(shared, salt=pk_eph, info="meshbay:gek_wrap:v1")
+ wrapped = ChaCha20-Poly1305(wrap_key).encrypt(nonce, gek, aad=pk_recipient)
+ bundle = {pk_eph, nonce, wrapped} → hub (opaque)
+
+Member side (unwrap):
+ shared = X25519(sk_recipient, pk_eph)
+ wrap_key = HKDF(shared, salt=pk_eph, info="meshbay:gek_wrap:v1")
+ gek = ChaCha20-Poly1305(wrap_key).decrypt(nonce, wrapped, aad=pk_recipient)
+```
+
+### Propriétés de sécurité vérifiées
+
+- Le hub ne voit jamais la GEK en clair (bundle opaque de 48 bytes)
+- La clé éphémère est unique par bundle (même GEK, même destinataire → bundles différents)
+- L'AAD (`pk_recipient`) lie le bundle au destinataire → impossible de réutiliser pour un autre membre
+- Mauvaise clé privée → AEAD authentication tag échec (InvalidTag) → rejet immédiat
+
+### Bugs découverts
+
+- Double appel `unwrap_gek` dans le code initial (copie/colle résiduelle) → `InvalidTag` au premier appel
+- Keypairs de Bob non persistés entre les runs → incohérence hub vs local → `AssertionError`
+ Fix : `bob_state.json` pour rendre le spike idempotent
+
+---
+
+## Spike 5 — Encrypted File Transfer
+**Date:** 2026-08-09
+**Machine:** Fedora 44 (node, derrière NAT SFR) → meshbay.org (client, IP publique OVH)
+**Transport:** TCP sortant depuis le node (contournement NAT pour le POC — en production : QUIC + hole punching Spike 4)
+**Chunk:** 1 MB (chunk 0 d'un fichier test 5 MB)
+
+### Results: 4/4 PASSED
+
+| Test | Résultat |
+|---|---|
+| Ed25519 signature valid | ✓ |
+| blake3(ciphertext) matches | ✓ |
+| Déchiffrement ChaCha20-Poly1305 | ✓ 1024 KB → 1024 KB |
+| blake3(plaintext) matches — intégrité bout en bout | ✓ |
+
+### Timings
+
+| Mesure | Node (Fedora) | Client (meshbay.org) |
+|---|---|---|
+| Chiffrement + signature (1 MB) | **3.2 ms** | — |
+| Envoi TCP | 99 ms | — |
+| Réception TCP | — | 234 ms |
+| Vérification + déchiffrement | — | **3.9 ms** |
+| **Overhead crypto total** | **3.2 ms** | **3.9 ms** |
+| Débit réseau effectif | 13.5 MB/s envoi | **4.3 MB/s réception** |
+
+Le débit 4.3 MB/s (~34 Mbps) est la limite réseau OVH → SFR résidentiel, pas la limite crypto.
+L'overhead crypto (chiffrement + déchiffrement) est **< 10 ms pour 1 MB** — totalement négligeable.
+
+### Pipeline validé
+
+```
+Fichier disque (clair)
+ → lecture 1 MB chunk
+ → HKDF(GEK, file_hash, chunk_index) → clé 32B
+ → ChaCha20-Poly1305 encrypt (nonce aléatoire)
+ → blake3(ciphertext) → ct_hash
+ → Ed25519 sign(chunk_index || nonce || ct_hash)
+ → envoi JSON/TCP length-prefixed
+ → réception
+ → Ed25519 verify ✓
+ → blake3(ct) == ct_hash ✓
+ → ChaCha20-Poly1305 decrypt ✓
+ → blake3(plaintext) == pt_hash ✓
+```
+
+### Notes / Actions
+
+- `sys.exit(0)` dans un handler asyncio génère un log d'exception cosmétique — sans impact sur le résultat. À corriger (utiliser `server.close()` + event).
+- La GEK est incluse dans la réponse (`gek_b64`) pour le POC uniquement. En production : la GEK est distribuée via le bundle chiffré du hub (jamais en clair sur le réseau).
+- Le `file_hash` (blake3 du fichier complet) est utilisé dans l'info HKDF pour identifier le fichier. En production, il est dans le Mesh Group Index chiffré.
+- La compression (zstd avant chiffrement) n'est pas dans ce spike — à valider dans l'implémentation.
+- **En production** : même pipeline mais sur QUIC (UDP hole-punching Spike 4) au lieu de TCP.