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>
16 KiB
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 :
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 :
- Identifiant de protocole :
"ZKP-HAM-v1"(ASCII) - Matrice d'adjacence de G (sérialisée, big-endian)
- Matrice d'adjacence de G' (sérialisée)
- Tous les commits dans l'ordre (i, j) lexicographique
- 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().
#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
#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
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
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
// 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)
// === 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
- test_completeness :
zkp_provepuiszkp_verify→ toujours ACCEPT - test_soundness : preuve avec cycle invalide → toujours REJECT
- test_anti_replay : deux preuves du même (G,H) → session_nonces différents
- test_tamper_commit : modifier un commit → REJECT
- test_tamper_challenge : modifier le challenge → REJECT (hash incohérent)
- test_tamper_cycle : modifier un nœud du cycle → REJECT
- 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_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
- Blum, M. (1986). How to prove a theorem so no one else can claim it. ICM.
- Fiat, A., Shamir, A. (1986). How to Prove Yourself. CRYPTO 1986. LNCS 263.
- Goldreich, O., Micali, S., Wigderson, A. (1991). Proofs that yield nothing but their validity. JACM.
- OEIS A000940 — Number of inequivalent Hamiltonian cycles in K_n under dihedral group.
- ANSSI — Référentiel d'exigences pour les mécanismes cryptographiques. (pour certification CSPN SecuBox)