secubox-openwrt/package/secubox/zkp-hamiltonian/SPEC.md
CyberMind-FR 6553936886 feat(zkp-hamiltonian): Add Zero-Knowledge Proof library based on Hamiltonian Cycle
Implements NIZK (Non-Interactive Zero-Knowledge) proof protocol using
Blum's Hamiltonian Cycle construction with Fiat-Shamir transformation.

Features:
- Complete C99 library with SHA3-256 commitments (via OpenSSL)
- Graph generation with embedded trapdoor (Hamiltonian cycle)
- NIZK proof generation and verification
- Binary serialization for proofs, graphs, and cycles
- CLI tools: zkp_keygen, zkp_prover, zkp_verifier
- Comprehensive test suite (41 tests)

Security properties:
- Completeness: honest prover always convinces verifier
- Soundness: cheater fails with probability >= 1 - 2^(-128)
- Zero-Knowledge: verifier learns nothing about the secret cycle

Target: OpenWrt ARM (SecuBox authentication module)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-24 09:59:16 +01:00

486 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Spécification Technique — Protocole ZKP Hamiltonien
## Zero-Knowledge Proof basé sur le problème du Cycle Hamiltonien
### CyberMind.FR / SecuBox — Version 1.0
---
## 1. Vue d'ensemble
### 1.1 Objectif
Implémenter un protocole de **preuve à divulgation nulle de connaissance (ZKP)** basé sur le problème NP-complet du cycle hamiltonien (Blum 1986, étendu NIZK via Fiat-Shamir). Le Prouveur démontre qu'il connaît un cycle hamiltonien dans un graphe public **sans révéler le cycle lui-même**.
### 1.2 Propriétés garanties
| Propriété | Définition | Garantie |
|-----------|-----------|----------|
| **Complétude** | Un Prouveur honnête convainc toujours le Vérifieur | Probabilité 1 |
| **Solidité (Soundness)** | Un tricheur échoue avec haute probabilité | ≥ 1 - 2^{-λ} |
| **Divulgation nulle (ZK)** | Le Vérifieur n'apprend rien sur H | Simulable en temps polynomial |
### 1.3 Paramètres cibles
- **Niveau de sécurité** : λ = 128 bits
- **Taille du graphe** : n ∈ [50, 80] nœuds
- **Mode** : NIZK (Non-Interactive ZK) via transformation Fiat-Shamir
- **Cible matérielle** : OpenWrt sur ARM (routeurs MIPS/ARM Cortex-A7+)
- **Dépendances** : libsodium ou OpenSSL (SHA3-256), C99
---
## 2. Primitives cryptographiques
### 2.1 Fonction de commit
```
Commit : {0,1} × {0,1}^256 → {0,1}^256
Commit(bit, nonce) = SHA3-256(bit || nonce)
```
**Propriétés requises :**
- **Binding** : impossible de trouver (b, r) ≠ (b', r') tel que Commit(b,r) = Commit(b',r')
- **Hiding** : Commit(b, r) avec r uniforme est indistinguable de aléatoire
**Implémentation :**
```c
void commit(uint8_t bit, const uint8_t nonce[32], uint8_t out[32]);
// out = SHA3-256(bit_byte || nonce[0..31])
// bit_byte = 0x00 si bit=0, 0x01 si bit=1
```
### 2.2 Fonction de hash pour Fiat-Shamir
```
H_FS : {0,1}* → {0,1}^128
H_FS(G, G', {commits}, session_nonce) = SHA3-256(...)[:16]
```
Entrées concaténées dans l'ordre canonique :
1. Identifiant de protocole : `"ZKP-HAM-v1"` (ASCII)
2. Matrice d'adjacence de G (sérialisée, big-endian)
3. Matrice d'adjacence de G' (sérialisée)
4. Tous les commits dans l'ordre (i, j) lexicographique
5. Nonce de session (32 octets, aléatoire, anti-rejeu)
### 2.3 Génération aléatoire
**Obligatoire** : `getrandom()` (Linux ≥ 3.17) ou `/dev/urandom` — jamais `rand()`.
```c
#include <sys/random.h>
ssize_t getrandom(void *buf, size_t buflen, unsigned int flags);
```
---
## 3. Générateur de graphe à trapdoor
### 3.1 Algorithme de construction
Le graphe G est construit à partir d'un cycle hamiltonien H (la trapdoor) en ajoutant des arêtes leurres.
```
GENERATE_GRAPH(n, extra_edge_ratio) → (G, H)
1. Générer H :
a. Créer une permutation aléatoire uniforme des nœuds {0..n-1}
via Fisher-Yates avec getrandom()
b. H = [(π[0],π[1]), (π[1],π[2]), ..., (π[n-2],π[n-1]), (π[n-1],π[0])]
2. Initialiser G avec les arêtes de H
3. Ajouter des arêtes leurres :
- Nombre cible : extra = floor(n * extra_edge_ratio) // ratio ≈ 0.5..1.5
- Pour chaque arête leurre candidate (u, v) ∉ H, u < v :
- Vérifier que (u,v) n'est pas dans G
- Vérifier que l'ajout de (u,v) ne crée pas un sous-cycle de longueur < n
(optionnel pour niveau basique, recommandé pour niveau élevé)
- Ajouter (u,v) à G
4. Retourner (G, H)
```
**Invariants à vérifier :**
- G est connexe
- H est un cycle hamiltonien valide dans G (n arêtes, chaque nœud de degré ≥ 2)
- G est non-orienté (arête (u,v) ⟺ arête (v,u))
### 3.2 Paramètres recommandés
| Niveau sécurité | n | extra_edge_ratio | |arêtes| total |
|---|---|---|---|
| Développement | 20 | 0.5 | ~30 |
| Production 128b | 50 | 1.0 | ~75 |
| Production 256b | 70 | 1.2 | ~120 |
### 3.3 Représentation mémoire
```c
#define MAX_N 128
typedef struct {
uint8_t n; // nombre de nœuds
uint64_t adj[MAX_N]; // adj[i] : bitfield des voisins de i
// adj[i] & (1ULL << j) = 1 si arête (i,j)
} Graph;
typedef struct {
uint8_t n; // longueur du cycle = nombre de nœuds
uint8_t nodes[MAX_N]; // nodes[0..n-1] : séquence des nœuds du cycle
// arêtes : (nodes[i], nodes[(i+1)%n])
} HamiltonianCycle;
```
---
## 4. Protocole interactif (base)
### 4.1 Acteurs
- **Prouveur P** : connaît (G, H)
- **Vérifieur V** : connaît G uniquement
### 4.2 Déroulement d'un round
```
ROUND(P(G,H), V(G)) :
── COMMIT ──────────────────────────────────────────────────────
P :
1. Choisir π ∈ S_n uniformément (Fisher-Yates + getrandom)
2. Calculer G' = π(G) :
G'.adj[π[i]] |= (1ULL << π[j]) pour chaque arête (i,j) de G
3. Pour chaque paire (i,j) avec i < j, 0 ≤ i,j < n :
Choisir nonce[i][j] ∈ {0,1}^256 via getrandom
bit = (G'.adj[i] >> j) & 1 // 1 si arête, 0 sinon
commit[i][j] = SHA3-256(bit || nonce[i][j])
4. Envoyer {commit[i][j]} à V
── CHALLENGE ───────────────────────────────────────────────────
V :
5. Choisir b ∈ {0, 1} uniformément
6. Envoyer b à P
── RÉPONSE ─────────────────────────────────────────────────────
P :
Si b = 0 (challenge isomorphisme) :
7a. Envoyer (π, tous les nonces[i][j])
Si b = 1 (challenge cycle hamiltonien) :
7b. Calculer H' = π(H) dans G'
Pour chaque arête (u,v) de H' :
Envoyer (u, v, nonce[min(u,v)][max(u,v)])
── VÉRIFICATION ────────────────────────────────────────────────
V :
Si b = 0 :
8a. Recalculer G'' = π(G)
Pour chaque arête (i,j) de G'' :
Vérifier commit[min(i,j)][max(i,j)] = SHA3-256(0x01 || nonce[i][j])
Pour chaque non-arête :
Vérifier commit[min(i,j)][max(i,j)] = SHA3-256(0x00 || nonce[i][j])
ACCEPT si toutes les vérifications passent
Si b = 1 :
8b. Vérifier que H' forme un cycle hamiltonien valide :
- Exactement n arêtes
- Chaque nœud apparaît exactement une fois
- Le cycle est connexe
Pour chaque arête (u,v) de H' :
Vérifier commit[min(u,v)][max(u,v)] = SHA3-256(0x01 || nonce[u][v])
ACCEPT si tout est valide
```
### 4.3 Analyse de sécurité
**Complétude** : P honnête calcule toujours une réponse valide pour les deux challenges.
**Soundness** : Un tricheur (sans H) doit préparer à l'avance une réponse pour b=0 OU b=1, pas les deux simultanément. Probabilité de réussite par round = 1/2. Pour k rounds : 2^{-k}.
**Zero-Knowledge** : Le simulateur S (sans H) :
- Si b=0 : construit G' quelconque, révèle l'isomorphisme → valide
- Si b=1 : construit G' en posant un cycle arbitraire C, commit uniquement les arêtes de C → révèle C
Le Vérifieur ne peut distinguer transcription réelle de transcription simulée.
---
## 5. Protocole NIZK (Fiat-Shamir)
### 5.1 Transformation
Le challenge interactif `b` est remplacé par :
```
b = H_FS(G, G', {commits}, session_nonce)[0] & 0x01
```
Le Prouveur génère la preuve complète (commit + réponse) en une passe.
### 5.2 Structure de la preuve NIZK
```c
typedef struct {
// En-tête
uint8_t version; // 0x01
uint8_t n; // nombre de nœuds
uint8_t session_nonce[32]; // anti-rejeu
// Graphe G' (isomorphe de G sous π)
uint64_t gprime_adj[MAX_N]; // matrice d'adjacence de G'
// Commits de toutes les arêtes de G'
uint8_t commits[MAX_N][MAX_N][32]; // commits[i][j] pour i<j
// Challenge (déterministe via Fiat-Shamir)
uint8_t challenge; // 0 ou 1
// Réponse (selon challenge)
union {
struct { // si challenge = 0
uint8_t perm[MAX_N]; // permutation π
uint8_t nonces[MAX_N][MAX_N][32]; // tous les nonces
} iso_response;
struct { // si challenge = 1
uint8_t cycle_nodes[MAX_N]; // H' = cycle dans G'
uint8_t nonces[MAX_N][32]; // nonces des arêtes du cycle
} ham_response;
};
} NIZKProof;
```
### 5.3 Algorithme Prove
```
PROVE(G, H) → NIZKProof proof
1. Générer session_nonce ∈ {0,1}^256 via getrandom
2. Choisir π ∈ S_n (Fisher-Yates + getrandom)
3. Calculer G' = π(G)
4. Pour chaque (i,j), i<j :
Générer nonce[i][j] via getrandom
bit = arête présente dans G' ?
commit[i][j] = SHA3-256(bit || nonce[i][j])
5. Calculer b = H_FS(G, G', commits, session_nonce)[0] & 0x01
6. Construire réponse selon b
7. Retourner proof
```
### 5.4 Algorithme Verify
```
VERIFY(G, proof) → {ACCEPT, REJECT}
1. Vérifier proof.version == 0x01
2. Vérifier proof.n == G.n
3. Recalculer b = H_FS(G, proof.gprime_adj, proof.commits, proof.session_nonce)[0] & 0x01
4. Vérifier proof.challenge == b (sinon REJECT)
5. Si b = 0 :
Vérifier permutation + commits (challenge isomorphisme)
6. Si b = 1 :
Vérifier cycle hamiltonien + commits (challenge cycle)
7. Retourner ACCEPT ou REJECT
```
---
## 6. Vérifications de validité
### 6.1 Validation du cycle hamiltonien
```c
bool validate_hamiltonian_cycle(const Graph *G, const HamiltonianCycle *H) {
if (H->n != G->n) return false;
// 1. Tous les nœuds distincts
uint64_t seen = 0;
for (int i = 0; i < H->n; i++) {
uint8_t node = H->nodes[i];
if (node >= G->n) return false;
if (seen & (1ULL << node)) return false; // doublon
seen |= (1ULL << node);
}
if (seen != (1ULL << G->n) - 1) return false; // nœuds manquants
// 2. Chaque arête du cycle existe dans G
for (int i = 0; i < H->n; i++) {
uint8_t u = H->nodes[i];
uint8_t v = H->nodes[(i + 1) % H->n];
if (!(G->adj[u] & (1ULL << v))) return false;
}
return true;
}
```
### 6.2 Comparaison en temps constant
```c
// OBLIGATOIRE pour éviter les timing attacks
bool const_time_memcmp(const uint8_t *a, const uint8_t *b, size_t len) {
uint8_t diff = 0;
for (size_t i = 0; i < len; i++) {
diff |= a[i] ^ b[i];
}
return diff == 0;
}
// Ou utiliser : crypto_verify_32() de libsodium
```
---
## 7. Interface publique (API)
```c
// === Génération ===
// Génère un graphe à trapdoor avec son cycle hamiltonien
// Retourne 0 si succès, -1 si erreur
int zkp_generate_graph(uint8_t n, double extra_ratio,
Graph *out_graph, HamiltonianCycle *out_cycle);
// === Protocole ===
// Génère une preuve NIZK (Prouveur)
// Retourne 0 si succès
int zkp_prove(const Graph *G, const HamiltonianCycle *H,
NIZKProof *out_proof);
// Vérifie une preuve NIZK (Vérifieur)
// Retourne 1 si ACCEPT, 0 si REJECT, -1 si erreur
int zkp_verify(const Graph *G, const NIZKProof *proof);
// === Sérialisation ===
// Sérialise/désérialise la preuve pour transport réseau
int zkp_proof_serialize(const NIZKProof *proof, uint8_t *buf, size_t *len);
int zkp_proof_deserialize(const uint8_t *buf, size_t len, NIZKProof *proof);
// Sérialise/désérialise le graphe public
int zkp_graph_serialize(const Graph *G, uint8_t *buf, size_t *len);
int zkp_graph_deserialize(const uint8_t *buf, size_t len, Graph *G);
// === Utilitaires ===
// Affiche lisiblement (debug)
void zkp_graph_print(const Graph *G);
void zkp_cycle_print(const HamiltonianCycle *H);
void zkp_proof_print(const NIZKProof *proof);
```
---
## 8. Structure du projet
```
zkp-hamiltonian/
├── CMakeLists.txt
├── README.md
├── include/
│ ├── zkp_hamiltonian.h // API publique complète
│ ├── zkp_types.h // Types (Graph, HamiltonianCycle, NIZKProof)
│ ├── zkp_crypto.h // Primitives crypto (commit, hash_fs)
│ └── zkp_graph.h // Opérations sur graphes
├── src/
│ ├── zkp_graph.c // Générateur + opérations graphe
│ ├── zkp_crypto.c // SHA3-256, commits, Fiat-Shamir
│ ├── zkp_prove.c // Algorithme Prove
│ ├── zkp_verify.c // Algorithme Verify
│ └── zkp_serialize.c // Sérialisation binaire
├── tests/
│ ├── test_graph.c // Tests du générateur
│ ├── test_crypto.c // Tests des primitives
│ ├── test_protocol.c // Tests complétude + soundness
│ ├── test_nizk.c // Tests Fiat-Shamir
│ └── test_vectors.c // Vecteurs de test fixes
├── tools/
│ ├── zkp_keygen.c // CLI : génère et sauvegarde (G, H)
│ ├── zkp_prover.c // CLI : génère une preuve
│ └── zkp_verifier.c // CLI : vérifie une preuve
└── openwrt/
├── Makefile // Package OpenWrt
└── files/
└── etc/secubox/zkp.conf
```
---
## 9. Vecteurs de test
### 9.1 Graphe minimal (n=4, développement)
```
G (n=4) :
Arêtes : (0,1), (1,2), (2,3), (3,0), (0,2) // 4 cycle + 1 leurre
adj[0] = 0b1101 = {1, 2, 3}
adj[1] = 0b0101 = {0, 2}
adj[2] = 0b0111 = {0, 1, 3}
adj[3] = 0b0101 = {0, 2} // erreur → adj[3]={0,2} mais arête (2,3) et (3,0)
Cycle hamiltonien H : [0, 1, 2, 3] (retour 3→0)
Commit test :
bit=1, nonce=0x00...00 (32 zéros)
SHA3-256(0x01 || 0x00...00) = [valeur fixe à calculer et inclure]
```
### 9.2 Tests automatisés requis
1. **test_completeness** : `zkp_prove` puis `zkp_verify` → toujours ACCEPT
2. **test_soundness** : preuve avec cycle invalide → toujours REJECT
3. **test_anti_replay** : deux preuves du même (G,H) → session_nonces différents
4. **test_tamper_commit** : modifier un commit → REJECT
5. **test_tamper_challenge** : modifier le challenge → REJECT (hash incohérent)
6. **test_tamper_cycle** : modifier un nœud du cycle → REJECT
7. **benchmark_n50** : mesurer temps prove/verify et RAM pour n=50
---
## 10. Considérations OpenWrt / embarqué
### 10.1 Contraintes mémoire
Pour n=50 :
- `commits[50][50][32]` = 80 000 octets ≈ 78 Ko (alloué dynamiquement)
- `nonces[50][50][32]` = 80 000 octets (libérés après preuve)
- Stack : prévoir 8 Ko minimum pour les fonctions récursives
### 10.2 CMakeLists.txt (cross-compilation OpenWrt)
```cmake
cmake_minimum_required(VERSION 3.16)
project(zkp_hamiltonian C)
set(CMAKE_C_STANDARD 99)
# Options
option(USE_LIBSODIUM "Use libsodium for crypto" ON)
option(BUILD_TOOLS "Build CLI tools" ON)
option(BUILD_TESTS "Build test suite" ON)
# Cible embarquée : optimiser la taille
if(OPENWRT_BUILD)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os -ffunction-sections -fdata-sections")
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--gc-sections")
endif()
```
### 10.3 Dépendances
```
libsodium >= 1.0.18 // SHA3-256 (crypto_hash_sha256 ou SHA3 via EVP)
OU
openssl >= 1.1.1 // EVP_DigestInit avec EVP_sha3_256()
```
Pour OpenWrt : `libsodium` est disponible dans les feeds standard.
---
## 11. Références
1. Blum, M. (1986). *How to prove a theorem so no one else can claim it.* ICM.
2. Fiat, A., Shamir, A. (1986). *How to Prove Yourself.* CRYPTO 1986. LNCS 263.
3. Goldreich, O., Micali, S., Wigderson, A. (1991). *Proofs that yield nothing but their validity.* JACM.
4. OEIS A000940 — *Number of inequivalent Hamiltonian cycles in K_n under dihedral group.*
5. ANSSI — *Référentiel d'exigences pour les mécanismes cryptographiques.* (pour certification CSPN SecuBox)