summaryrefslogtreecommitdiffstats
path: root/docs/poc-v1-fr.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-01 16:05:16 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-01 16:05:16 +0200
commit8a6294b0412a86f378c6e2e937c28de64a903c91 (patch)
tree20d1977d9148a2c86c62925c5be7e571815c9057 /docs/poc-v1-fr.md
parent854a9441ccb734c7fbb1e0ff8570b9ef659c09b0 (diff)
downloadmeshbay-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.md432
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.