diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-01 16:05:16 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-01 16:05:16 +0200 |
| commit | 8a6294b0412a86f378c6e2e937c28de64a903c91 (patch) | |
| tree | 20d1977d9148a2c86c62925c5be7e571815c9057 /docs/poc-v1-fr.md | |
| parent | 854a9441ccb734c7fbb1e0ff8570b9ef659c09b0 (diff) | |
| download | meshbay-8a6294b0412a86f378c6e2e937c28de64a903c91.tar.gz | |
docs: move root docs into docs/ and archive superseded drafts
Move the remaining root-level .md files (except CLAUDE.md) into docs/:
devel-phases.md, devel-phases-next.md, first-review.md, second-review.md,
tmp-decisions.md. Update all inbound references in CLAUDE.md (now docs/-prefixed)
and strip the now-redundant docs/ prefix from links inside the moved files.
Consolidate the superseded material into docs/old-draft.md: architecture
drafts v1-v4, POC v1, and the Phase 1-12 development log, each under an
ARCHIVED banner with a preamble pointing at the current specs. Delete the
merged originals plus the unreferenced French translations (v1-fr, v2-fr,
poc-v1-fr). Repoint the surviving file-links in first-review.md,
second-review.md and meshbay-draft-v5.md at old-draft.md; prose "draft-v3 §x"
mentions are left as-is since the content now lives in the archive.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J74kj44q6REczub8XR3DRy
Diffstat (limited to 'docs/poc-v1-fr.md')
| -rw-r--r-- | docs/poc-v1-fr.md | 432 |
1 files changed, 0 insertions, 432 deletions
diff --git a/docs/poc-v1-fr.md b/docs/poc-v1-fr.md deleted file mode 100644 index 262a245..0000000 --- a/docs/poc-v1-fr.md +++ /dev/null @@ -1,432 +0,0 @@ -# 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. |