aboutsummaryrefslogtreecommitdiffstats
path: root/docs/poc-v1-fr.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 /docs/poc-v1-fr.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 'docs/poc-v1-fr.md')
-rw-r--r--docs/poc-v1-fr.md432
1 files changed, 432 insertions, 0 deletions
diff --git a/docs/poc-v1-fr.md b/docs/poc-v1-fr.md
new file mode 100644
index 0000000..262a245
--- /dev/null
+++ b/docs/poc-v1-fr.md
@@ -0,0 +1,432 @@
+# MeshBay — POC v1 (FR)
+
+> Objectif : valider les concepts clés avant de s'engager dans une implémentation complète.
+> Périmètre : échange Hub/Node en Python, stack crypto, traversée NAT, transfert chiffré de chunk de fichier.
+> Tout en mémoire (pas de base de données), code minimal, TCP uniquement (pas de QUIC pour l'instant).
+
+---
+
+## Environnement
+
+### Distant — meshbay.org (Hub)
+- OVH VPS, Ubuntu 26.04 LTS, Python 3.14.4
+- IP fixe publique, ports 80 et 443 ouverts
+- Serveur vierge : aucun serveur web installé
+- Accès SSH : `ssh cbesson@meshbay.org`
+
+### Local — Fedora 44 (Node)
+- Laptop derrière NAT résidentiel SFR (vraisemblablement Restricted Cone NAT — UPnP supporté)
+- Python 3.13+ via paquets système
+- Utilisateur : `cbesson` (sudoer sans mot de passe)
+
+---
+
+## Dépendances Python
+
+```bash
+# Partagé (hub et node)
+cryptography>=43.0 # Ed25519, X25519, ChaCha20-Poly1305, Argon2id
+PyJWT>=2.9 # JWT avec support EdDSA (Ed25519)
+blake3>=1.0 # Hachage rapide du contenu
+
+# Hub uniquement (meshbay.org)
+fastapi>=0.115
+uvicorn[standard]>=0.30
+
+# Node uniquement (laptop Fedora)
+httpx>=0.28 # Client HTTP async pour les appels node→hub
+aioice>=0.9 # Requêtes STUN pour la découverte NAT
+miniupnpc>=2.2 # Ouverture de port UPnP sur la box SFR
+```
+
+Installation sur chaque machine :
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+pip install <paquets ci-dessus>
+```
+
+---
+
+## Configuration du hub sur meshbay.org
+
+Pour le POC, uvicorn tourne directement sur le port 80 via une redirection iptables (pas de Caddy/nginx pour l'instant — HTTPS ajouté avant la production).
+
+```bash
+# Sur meshbay.org
+# Redirection port 80 → 8000
+sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8000
+
+# Lancer le hub (depuis le répertoire poc, venv activé)
+uvicorn hub:app --host 127.0.0.1 --port 8000 --reload
+```
+
+> Note : HTTPS (via Caddy + Let's Encrypt) est obligatoire avant tout usage réel au-delà de ce POC.
+
+---
+
+## Vue d'ensemble des spikes
+
+| # | Nom | Où | Ce que ça valide | Durée |
+|---|---|---|---|---|
+| 1 | Primitives crypto | Local | La stack Python crypto couvre tous les besoins | ~1h |
+| 2 | Squelette hub | meshbay.org | API hub, émission JWT | ~2h |
+| 3 | Enregistrement node | Fedora | Handshake Hub-Node, vérification JWT offline | ~1h |
+| 4 | Traversée NAT | Les deux | UPnP box SFR + STUN, accessibilité P2P | ~2h |
+| 5 | Transfert chiffré | Les deux | Chiffrement GEK à la volée, chunk P2P | ~2h |
+
+---
+
+## Spike 1 — Primitives cryptographiques (local uniquement)
+
+**Objectif :** confirmer que `cryptography` (PyCA) couvre tous les besoins cryptographiques de MeshBay sans lacune ni surprise de performance.
+
+**Fichier :** `spike1_crypto.py`
+
+**Test 1 : Ed25519 — keypair hub, signature, vérification**
+```python
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+sk_hub = Ed25519PrivateKey.generate()
+pk_hub = sk_hub.public_key()
+msg = b"test payload"
+sig = sk_hub.sign(msg)
+pk_hub.verify(sig, msg) # lève une exception si invalide
+print("Ed25519 OK")
+```
+
+**Test 2 : X25519 — accord de clé pour l'enveloppement de la GEK**
+```python
+from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
+
+sk_a = X25519PrivateKey.generate()
+sk_b = X25519PrivateKey.generate()
+shared_a = sk_a.exchange(sk_b.public_key())
+shared_b = sk_b.exchange(sk_a.public_key())
+assert shared_a == shared_b
+print("X25519 OK")
+```
+
+**Test 3 : ChaCha20-Poly1305 sur un chunk de 1 Mo**
+```python
+from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
+import os, time
+
+gek = ChaCha20Poly1305.generate_key()
+cipher = ChaCha20Poly1305(gek)
+chunk = os.urandom(1024 * 1024) # 1 Mo
+
+t0 = time.perf_counter()
+nonce = os.urandom(12)
+ct = cipher.encrypt(nonce, chunk, None)
+pt = cipher.decrypt(nonce, ct, None)
+elapsed = time.perf_counter() - t0
+
+assert pt == chunk
+print(f"ChaCha20-Poly1305 1 Mo : {elapsed*1000:.1f} ms")
+```
+
+**Test 4 : Dérivation de clé de chunk par HKDF**
+```python
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+from cryptography.hazmat.primitives import hashes
+import blake3
+
+chunk_key = HKDF(
+ algorithm=hashes.SHA256(), length=32, salt=None,
+ info=b"file:" + blake3.blake3(chunk).digest() + b":chunk:0"
+).derive(gek)
+print(f"Clé HKDF : {chunk_key.hex()[:16]}...")
+```
+
+**Test 5 : Argon2id — dérivation de clé keystore**
+```python
+from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
+
+salt = os.urandom(16)
+t0 = time.perf_counter()
+kdf = Argon2id(salt=salt, length=32, iterations=3, lanes=4, memory_cost=65536)
+key = kdf.derive(b"motdepasse")
+print(f"Argon2id : {(time.perf_counter()-t0)*1000:.0f} ms")
+```
+
+**Test 6 : PyJWT avec Ed25519 (EdDSA)**
+```python
+import jwt
+from cryptography.hazmat.primitives import serialization
+
+sk_pem = sk_hub.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption()
+)
+pk_pem = pk_hub.public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo
+)
+payload = {"sub": "user_abc", "pk_user": "base64...", "exp": 9999999999}
+token = jwt.encode(payload, sk_pem, algorithm="EdDSA")
+decoded = jwt.decode(token, pk_pem, algorithms=["EdDSA"])
+assert decoded["sub"] == "user_abc"
+print("JWT EdDSA OK")
+```
+
+**Critères de succès :** tous les tests passent, ChaCha20 1 Mo < 20 ms, Argon2id ~1s.
+
+---
+
+## Spike 2 — Squelette du hub (meshbay.org)
+
+**Objectif :** hub FastAPI minimal avec stockage en mémoire, 6 endpoints.
+
+**Fichier :** `hub.py` (sur meshbay.org)
+
+### Génération du keypair hub (une seule fois)
+
+```python
+# gen_hub_keys.py — exécuter une seule fois sur meshbay.org
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from cryptography.hazmat.primitives import serialization
+
+sk = Ed25519PrivateKey.generate()
+with open("hub_private.pem", "wb") as f:
+ f.write(sk.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption()
+ ))
+with open("hub_public.pem", "wb") as f:
+ f.write(sk.public_key().public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo
+ ))
+print("Keypair hub généré.")
+```
+
+### Endpoints du hub
+
+```
+GET /v1/hub/pubkey → PEM de la clé publique Ed25519 du hub
+POST /v1/users/register → {username, password, pk_user_ed25519, pk_user_x25519} → {user_id}
+POST /v1/users/login → {username, password} → {access_token, refresh_token}
+POST /v1/users/token/refresh → {refresh_token} → {access_token}
+POST /v1/nodes/announce → (auth) {pk_node, endpoint_hint} → {node_id}
+GET /v1/nodes/{node_id} → (auth) {pk_node, endpoint_hint}
+```
+
+### Structure JWT (access token)
+
+```json
+{
+ "iss": "meshbay.org",
+ "sub": "<user_id>",
+ "pk_user": "<base64 Ed25519 publique>",
+ "hub_id": "meshbay.org",
+ "iat": 1720000000,
+ "exp": 1720003600
+}
+```
+
+Signé avec la clé Ed25519 privée du hub. Vérifiable par n'importe qui possédant la clé publique du hub — aucun appel hub requis.
+
+**Critères de succès :**
+- Hub démarre, tous les endpoints répondent correctement
+- `POST /v1/users/register` + `POST /v1/users/login` retourne un JWT valide
+- `jwt.decode()` avec la clé publique du hub passe sans erreur
+
+---
+
+## Spike 3 — Enregistrement du node (laptop Fedora)
+
+**Objectif :** le node génère son keypair, s'enregistre sur le hub, obtient un JWT, et le vérifie localement sans contacter le hub.
+
+**Fichier :** `node.py`
+
+**Séquence :**
+1. Récupérer la clé publique du hub (`GET /v1/hub/pubkey`) — mettre en cache
+2. Générer le keypair Ed25519 + X25519 du node
+3. Enregistrer l'utilisateur sur le hub
+4. Se connecter, recevoir l'access token (JWT)
+5. **Vérifier le JWT localement** avec la clé publique du hub — aucun appel réseau
+6. Annoncer le node au hub
+
+**Vérification JWT offline (point clé) :**
+```python
+# Aucun appel hub — juste la signature Ed25519
+decoded = jwt.decode(access_token, hub_pk_pem, algorithms=["EdDSA"])
+print(f"[node] JWT vérifié localement : sub={decoded['sub']}")
+```
+
+C'est la validation du concept fondamental : le hub est une autorité d'identité qui émet des credentials vérifiables hors ligne. Après le login, le hub n'est plus dans la boucle.
+
+**Critères de succès :**
+- Node s'enregistre, se connecte, reçoit un JWT
+- JWT décodé offline avec la seule clé publique du hub
+- Node annoncé ; `GET /v1/nodes/{node_id}` depuis le hub retourne le bon PK
+
+---
+
+## Spike 4 — Traversée NAT (les deux machines)
+
+**Objectif :** découvrir l'IP:port externe du node local via UPnP et STUN ; tester l'accessibilité depuis meshbay.org.
+
+**Fichier :** `spike4_nat.py` (laptop Fedora)
+
+### Partie A — UPnP (à tenter en premier, plus fiable sur box SFR)
+
+```python
+import miniupnpc, socket
+
+def try_upnp(internal_port=19000):
+ u = miniupnpc.UPnP()
+ u.discoverdelay = 200
+ if u.discover() == 0:
+ print("UPnP : aucune IGD trouvée")
+ return None
+
+ u.selectigd()
+ external_ip = u.externalipaddress()
+ local_ip = socket.gethostbyname(socket.gethostname())
+
+ if u.addportmapping(internal_port, 'TCP', local_ip, internal_port, 'MeshBay POC', ''):
+ print(f"UPnP : {external_ip}:{internal_port} → {local_ip}:{internal_port}")
+ return f"{external_ip}:{internal_port}"
+ print("UPnP : échec du mapping")
+ return None
+```
+
+### Partie B — Découverte STUN
+
+```python
+import asyncio, aioice
+
+async def stun_discover():
+ connection = aioice.Connection(
+ ice_controlling=True,
+ stun_server=("stun.cloudflare.com", 3478)
+ )
+ await connection.gather_candidates()
+
+ for candidate in connection.local_candidates:
+ if candidate.type == "srflx": # server-reflexive = adresse externe
+ print(f"STUN srflx : {candidate.host}:{candidate.port}")
+ return f"{candidate.host}:{candidate.port}"
+
+ print("STUN : aucun candidat srflx (NAT symétrique possible)")
+ return None
+```
+
+### Partie C — Test d'accessibilité depuis meshbay.org
+
+Le node annonce son `endpoint_hint` au hub. Depuis meshbay.org :
+
+```bash
+# Test TCP depuis meshbay.org
+python3 -c "
+import socket
+s = socket.create_connection(('<ip_externe>', <port>), timeout=5)
+print('ACCESSIBLE')
+s.close()
+"
+```
+
+Sur le laptop Fedora, un listener simple sur le port découvert :
+```python
+import socket
+s = socket.socket()
+s.bind(('', 19000))
+s.listen(1)
+print("En écoute sur 19000...")
+conn, addr = s.accept()
+print(f"Connexion depuis {addr}")
+conn.sendall(b"BONJOUR DU NODE\n")
+conn.close()
+```
+
+**Résultats attendus sur SFR résidentiel :**
+
+| Méthode | Résultat attendu | Niveau de confiance |
+|---|---|---|
+| UPnP | Fonctionne — La Box SFR supporte UPnP IGD | Élevé |
+| STUN srflx | Découvert — SFR est un cone NAT pour le résidentiel | Élevé |
+| TCP direct depuis meshbay.org | Fonctionne si UPnP a réussi | Élevé |
+| Hole punching seul | Dépend du type NAT découvert | Moyen |
+
+**Critères de succès :** au moins une méthode permet à meshbay.org d'atteindre directement le port du laptop Fedora.
+
+---
+
+## Spike 5 — Transfert chiffré de fichier (les deux machines)
+
+**Objectif :** le node sert un chunk de fichier chiffré via connexion TCP P2P directe ; le client déchiffre et vérifie.
+
+**Prérequis :** Spike 4 réussi — IP:port externe connu et accessible.
+
+### Côté node (laptop Fedora)
+
+Pipeline : lire le chunk → dériver la clé via HKDF(GEK, file_hash, chunk_index) → chiffrer ChaCha20-Poly1305 → signer Ed25519 → envoyer.
+
+**Points clés :**
+- Clé par chunk dérivée de la GEK (pas la GEK directement)
+- Chaque chunk signaturé avant envoi
+- La GEK n'est jamais envoyée en clair en production (envoyée en clair uniquement pour ce POC — voir note ci-dessous)
+
+### Côté client (meshbay.org)
+
+Pipeline : recevoir → vérifier signature Ed25519 → vérifier hash blake3 du ciphertext → déchiffrer ChaCha20-Poly1305 → obtenir les octets en clair.
+
+### Note sur la GEK dans le POC
+
+Pour ce POC, la GEK est transmise dans la réponse comme `gek_hint` pour des raisons de commodité. **En production, le client obtient la GEK depuis le bundle GEK chiffré du hub** (déchiffré côté client avec sa clé X25519 privée). Le mécanisme de distribution de la GEK est délibérément hors périmètre de ce POC.
+
+**Critères de succès :**
+- Le client reçoit le chunk depuis le node via TCP direct (sans hub dans le chemin)
+- La vérification de signature passe ✓
+- Le hash du ciphertext correspond ✓
+- Le déchiffrement produit les octets originaux ✓
+- `octets_originaux == octets_déchiffrés` ✓
+
+---
+
+## Ce que le POC valide (et ne valide pas)
+
+### Validé par ces spikes
+
+| Concept | Spike | Validation |
+|---|---|---|
+| Stack Python crypto suffisante | 1 | Toutes les primitives fonctionnent, performance acceptable |
+| Handshake Hub/Node via JWT | 2, 3 | JWT émis par le hub, vérifié offline par le node |
+| Protocole REST Hub-Node minimal | 2, 3 | Contrat API validé bout en bout |
+| Traversée NAT SFR via UPnP | 4 | Accessibilité P2P confirmée |
+| Découverte adresse externe STUN | 4 | Confirmée / fallback documenté |
+| Chiffrement par chunk à la volée | 5 | GEK + HKDF par chunk + ChaCha20 |
+| Signature et vérification de chunk | 5 | Ed25519 sign/verify avant déchiffrement |
+| Transfert de fichier P2P réel | 5 | Aucun hub dans le chemin des données |
+
+### Hors périmètre
+
+- Base de données (tout en mémoire)
+- HTTPS / TLS (HTTP pour le POC)
+- Transport QUIC (TCP simple)
+- Distribution du bundle GEK via hub (GEK transmise en clair pour le POC)
+- Gestion de groupes
+- Chat / Double Ratchet
+- Mesh Group Index
+- Fédération MHP
+- Client Android
+- Système de modules
+- Persistance entre les redémarrages
+
+---
+
+## Graphe de dépendance des spikes
+
+```
+Spike 1 (crypto)
+ └──→ Spike 2 (squelette hub)
+ └──→ Spike 3 (enregistrement node)
+ └──→ Spike 4 (traversée NAT)
+ └──→ Spike 5 (transfert chiffré)
+```
+
+Le Spike 1 est un prérequis pour tous les autres. Les Spikes 2 et 3 peuvent être menés en parallèle si deux personnes travaillent. Le Spike 4 peut commencer indépendamment dès que le Spike 3 est fonctionnel.