Prouver que vous connaissez un secret sans jamais le révéler semble relever du tour de magie. C’est pourtant exactement ce que fait une preuve à divulgation nulle de connaissance (zero-knowledge proof, ou ZKP). Le concept date de 1985, formalisé par Shafi Goldwasser, Silvio Micali et Charles Rackoff, mais il n’a jamais été aussi présent qu’en 2026 : vérification d’âge en ligne, identité numérique européenne, preuves de solvabilité sans exposer un solde bancaire. Ce tutoriel vous fait construire deux versions d’une preuve ZK, d’abord à la main en JavaScript pur pour comprendre les maths, puis avec des outils de production (circom et snarkjs) pour générer un vrai zk-SNARK vérifiable sur une blockchain. Comptez environ 60 minutes et 12 étapes pour arriver à une preuve qui fonctionne de bout en bout sur votre machine.

Qu’est-ce qu’une preuve à divulgation nulle de connaissance ?

Une preuve à divulgation nulle de connaissance permet à un prouveur de convaincre un vérificateur qu’une affirmation est vraie, sans transmettre aucune information au-delà du fait que l’affirmation est vraie. Trois propriétés définissent ce mécanisme. La complétude garantit que si l’affirmation est vraie et que les deux parties suivent le protocole, le vérificateur finit toujours par être convaincu. La correction (soundness) garantit l’inverse : un prouveur malhonnête ne peut pas convaincre le vérificateur d’une affirmation fausse, sauf avec une probabilité négligeable. La troisième propriété, celle qui donne son nom au concept, garantit que le vérificateur n’apprend rien de plus que la véracité de l’affirmation.

Concrètement, un prouveur peut démontrer qu’il connaît le mot de passe d’un compte sans le taper, qu’il a plus de 18 ans sans révéler sa date de naissance, ou qu’une transaction respecte les règles d’un protocole sans exposer les montants. Le débat européen autour de la vérification d’âge en ligne a remis ces techniques sur le devant de la scène ces derniers mois, mais l’usage des ZKP dépasse largement ce cas d’école : identité numérique, conformité réglementaire, calculs délégués à un tiers non fiable (rollups de couche 2 en blockchain), authentification sans mot de passe transmis. Deux familles d’implémentations dominent le paysage actuel : les protocoles interactifs classiques comme celui de Schnorr, et les zk-SNARK non interactifs (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge), qui produisent une preuve compacte vérifiable en une seule fois, sans échange supplémentaire entre les deux parties.

L’exemple pédagogique le plus cité dans la littérature reste celui du coloriage de graphe : un prouveur affirme connaître une façon de colorier les sommets d’un graphe avec trois couleurs, sans que deux sommets reliés ne partagent la même couleur, sans jamais montrer le coloriage complet au vérificateur. En répétant l’échange un grand nombre de fois avec des permutations de couleurs différentes à chaque tour, la probabilité qu’un prouveur trichant passe tous les tests sans connaître de solution valide devient négligeable. Ce même principe de répétition statistique se retrouve, sous une forme bien plus optimisée, dans les zk-SNARK modernes construits avec circom.

Où les ZKP sont utilisés concrètement en 2026

Les preuves à divulgation nulle de connaissance ne sont plus une curiosité de laboratoire. Dans le contexte européen, le sujet le plus discuté reste la vérification d’âge en ligne : plusieurs projets pilotes testent des schémas ZKP pour permettre à un utilisateur de prouver qu’il a atteint un âge minimum sans transmettre sa date de naissance exacte à une plateforme tierce, un sujet suivi de près par les régulateurs qui veulent concilier protection des mineurs et minimisation des données personnelles. Ce cas d’usage illustre bien la promesse du ZKP, mais aussi ses limites pratiques, notamment la nécessité de lier la preuve à un document d’identité fiable en amont.

Le secteur de l’identité numérique européenne s’appuie également sur ces techniques pour permettre à un citoyen de prouver un attribut (majorité, nationalité, diplôme) sans révéler l’intégralité d’un document officiel. Du côté de la blockchain, les zk-rollups utilisent des zk-SNARK pour prouver, en une seule transaction compacte, que des milliers d’opérations effectuées hors chaîne respectent bien les règles du protocole, ce qui réduit les coûts et la charge du réseau principal. Les plateformes d’échange de cryptomonnaies s’en servent aussi pour des preuves de réserve, afin de démontrer qu’elles détiennent bien les fonds de leurs clients sans exposer le détail de chaque compte individuel. Dans la finance traditionnelle, des cas d’usage émergent autour de la conformité réglementaire, où une institution doit prouver qu’une transaction respecte un ensemble de règles (plafond, origine des fonds) sans divulguer les données sous-jacentes à chaque partie de la chaîne.

Pourquoi coder un ZKP soi-même en 2026

La plupart des développeurs utilisent des ZKP sans jamais toucher aux maths sous-jacentes, via des bibliothèques d’identité ou des SDK blockchain. Comprendre le mécanisme change la donne dès qu’il faut auditer un circuit, diagnostiquer un bug de preuve invalide, ou simplement décider si un ZKP est la bonne solution à un problème donné. Ce tutoriel vise cet objectif précis : vous donner assez de pratique pour lire un circuit circom sans paniquer, et assez de recul mathématique pour repérer une erreur de conception avant qu’elle finisse en production.

Deux chemins existent pour apprendre. Le premier consiste à lire des papiers de recherche denses en notation mathématique. Le second, celui suivi ici, consiste à coder d’abord un protocole simple à la main, puis à basculer vers un outil de production une fois l’intuition acquise. La preuve de Schnorr, construite en 1989, reste le meilleur point d’entrée : elle tient dans une trentaine de lignes de JavaScript et illustre les trois propriétés citées plus haut sans avoir besoin de comprendre les courbes elliptiques ni les polynômes.

Prérequis : outils, versions et matériel

Avant de commencer, installez les outils suivants. Les versions indiquées sont celles disponibles au moment de la rédaction (2 septembre 2026), mais vérifiez toujours la version active avant de démarrer un projet réel.

OutilVersion utilisée dans ce tutorielRôle
Node.js24.20.0 LTS (“Krypton”)Exécution du protocole de Schnorr et des scripts snarkjs
npmfournie avec Node.js 24 LTSGestion des paquets du projet
circomv2.2.3Compilateur de circuits arithmétiques
snarkjs0.7.6Génération et vérification des preuves Groth16
circomlib2.0.5Bibliothèque de composants circom (comparateurs, hachage)
Rust / Cargodernière version stableCompilation du binaire circom depuis les sources

Comptez environ 2 Go d’espace disque libre pour les fichiers de la cérémonie Powers of Tau utilisés à l’étape 8, et une machine avec au moins 4 Go de RAM disponible. Un terminal Linux, macOS ou WSL2 sous Windows convient. Aucune carte graphique n’est nécessaire pour ce tutoriel : les circuits utilisés restent assez petits pour tourner sur un processeur classique en quelques secondes.

Étape 1 : installer Node.js et initialiser le projet

Installez Node.js 24 LTS via votre gestionnaire de versions préféré (nvm, fnm, ou le paquet officiel), puis créez un dossier de projet dédié.

nvm install 24
nvm use 24
node -v
# v24.20.0

mkdir zkp-tutoriel && cd zkp-tutoriel
npm init -y
npm install snarkjs circomlib

La commande npm init -y génère un package.json minimal. L’installation de snarkjs et circomlib comme dépendances npm classiques suffit pour la partie zk-SNARK du tutoriel. Le compilateur circom, lui, s’installe séparément car il s’agit d’un binaire natif écrit en Rust, pas d’un paquet npm.

Étape 2 : installer le compilateur circom

Deux options existent : télécharger un binaire précompilé depuis les releases GitHub du projet, ou compiler depuis les sources avec Cargo si Rust est déjà installé sur votre machine. La seconde option garantit la version la plus récente.

git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom

circom --version
# circom compiler 2.2.3

Si la commande circom reste introuvable après l’installation, le binaire cargo n’est probablement pas dans votre PATH. Ajoutez export PATH=”$HOME/.cargo/bin:$PATH” à votre fichier de configuration shell (.bashrc, .zshrc) et rechargez le terminal.

Étape 3 : comprendre le protocole de Schnorr

Avant d’écrire le moindre circuit, construisons une preuve ZK à la main. Le protocole de Schnorr permet à un prouveur de démontrer qu’il connaît un exposant secret x tel que y = g^x mod p, sans révéler x. Trois messages suffisent. Le prouveur choisit un nombre aléatoire r, calcule un engagement t = g^r mod p et l’envoie au vérificateur. Le vérificateur répond avec un défi c choisi aléatoirement. Le prouveur calcule alors une réponse s = r + c·x et l’envoie. Le vérificateur accepte la preuve si g^s mod p est égal à t · y^c mod p.

L’astuce mathématique tient en une ligne : g^s = g^(r + c·x) = g^r · g^(c·x) = t · y^c. Cette égalité se vérifie sans jamais isoler x. Le vérificateur ne voit que t, c et s, trois valeurs qui ne suffisent pas à remonter au secret, tant que r change à chaque exécution. C’est précisément cette dernière condition qui cause l’essentiel des failles d’implémentation réelles, comme on le verra dans la section sur les pièges courants.

Étape 4 : coder la preuve de Schnorr en JavaScript

Passons à l’implémentation. Ce code utilise des BigInt natifs de Node.js pour l’arithmétique modulaire. Les valeurs p et g choisies ici sont volontairement petites pour que les calculs restent lisibles à l’écran : elles n’offrent aucune sécurité réelle et ne doivent jamais servir en dehors d’un contexte pédagogique.

// schnorr.js
function modPow(base, exp, mod) {
  base = ((base % mod) + mod) % mod;
  let result = 1n;
  while (exp > 0n) {
    if (exp & 1n) result = (result * base) % mod;
    base = (base * base) % mod;
    exp >>= 1n;
  }
  return result;
}

// Parametres publics (demonstration uniquement, jamais en production)
const p = 2147483647n; // nombre premier de Mersenne (2^31 - 1)
const g = 7n;           // generateur

function proverSetup(secretX) {
  const y = modPow(g, secretX, p); // cle publique
  return { y };
}

function proverCommit() {
  const r = BigInt(Math.floor(Math.random() * 1_000_000) + 1);
  const t = modPow(g, r, p);
  return { r, t };
}

function proverRespond(r, secretX, challenge) {
  return r + challenge * secretX; // s = r + c*x
}

function verify(t, y, challenge, s) {
  const left = modPow(g, s, p);
  const right = (t * modPow(y, challenge, p)) % p;
  return left === right;
}

// Demonstration complete
const secretX = 424242n;
const { y } = proverSetup(secretX);
const { r, t } = proverCommit();
const challenge = BigInt(Math.floor(Math.random() * 1000) + 1);
const s = proverRespond(r, secretX, challenge);

console.log("Preuve valide :", verify(t, y, challenge, s));

Exécutez node schnorr.js. La sortie attendue est Preuve valide : true. Si vous modifiez la ligne du calcul de s pour y ajouter une erreur volontaire (par exemple s = r + challenge), la vérification doit renvoyer false : c’est le comportement attendu de la propriété de correction.

Étape 5 : tester la robustesse et casser volontairement la preuve

Un bon exercice pour ancrer la compréhension consiste à casser volontairement l’implémentation. Réutilisez le même r pour deux défis différents et observez que le secret x devient calculable par un attaquant qui intercepte les deux échanges. Avec deux réponses s1 = r + c1·x et s2 = r + c2·x, la soustraction élimine r : x = (s1 – s2) / (c1 – c2). C’est exactement la faille qui a touché plusieurs implémentations de signatures ECDSA par le passé, où un générateur de nombres aléatoires défaillant réutilisait un nonce.

// Demonstration de l'attaque par reutilisation de nonce
const c1 = 111n, c2 = 222n;
const s1 = r + c1 * secretX;
const s2 = r + c2 * secretX;
const recoveredX = (s1 - s2) / (c1 - c2);
console.log("Secret retrouve :", recoveredX === secretX);

Cette manipulation illustre pourquoi les bibliothèques de production génèrent r à partir d’un générateur cryptographiquement sûr (crypto.randomBytes en Node.js) et pourquoi certains protocoles modernes dérivent r de façon déterministe à partir du message et de la clé privée (RFC 6979), justement pour éliminer tout risque de réutilisation accidentelle.

Étape 6 : écrire votre premier circuit avec circom

Le protocole de Schnorr prouve la connaissance d’un secret, mais il ne prouve rien sur une condition arbitraire comme “mon âge dépasse 18 ans”. Pour ce type d’affirmation, l’approche moderne consiste à exprimer la condition sous forme de circuit arithmétique, puis à générer un zk-SNARK qui prouve que le circuit s’exécute correctement sans révéler les entrées privées. Créez un fichier AgeCheck.circom.

pragma circom 2.1.6;

include "circomlib/circuits/comparators.circom";

template AgeCheck() {
    signal input age;      // entree privee : jamais revelee
    signal output isAdult; // sortie publique : 1 ou 0

    component geq = GreaterEqThan(8); // comparaison sur 8 bits (age < 256)
    geq.in[0] <== age;
    geq.in[1] <== 18;
    isAdult <== geq.out;
}

component main = AgeCheck();

Dans circom, tout signal déclaré dans le corps du template reste privé par défaut. Seuls les signaux explicitement marqués comme sortie (output) ou listés dans un tableau public lors de la déclaration du composant principal deviennent visibles du vérificateur. Ici, age ne sortira jamais du circuit : seul le résultat booléen isAdult sera public.

Étape 7 : compiler le circuit et générer le témoin

Compilez le circuit pour produire trois artefacts : le fichier R1CS (la représentation arithmétique du circuit), un module WebAssembly capable de calculer le témoin, et un fichier symbolique utile au débogage.

mkdir build
circom AgeCheck.circom --r1cs --wasm --sym -o build -l node_modules

# Creez le fichier d'entree avec votre valeur privee
echo '{"age": 25}' > input.json

# Calculez le temoin (witness) a partir de l'entree privee
node build/AgeCheck_js/generate_witness.js \
  build/AgeCheck_js/AgeCheck.wasm input.json witness.wtns

Le témoin est un fichier binaire contenant toutes les valeurs intermédiaires du circuit pour cette entrée précise, y compris la valeur privée age. Il ne doit jamais quitter la machine du prouveur : c'est à partir de lui, et non de l'entrée brute, que la preuve sera générée à l'étape suivante.

Étape 8 : la cérémonie Powers of Tau et le trusted setup

Groth16, le système de preuve utilisé par défaut avec circom et snarkjs, nécessite un paramétrage de confiance (trusted setup) avant de pouvoir générer des preuves. Ce paramétrage se déroule en deux phases. La phase 1, appelée Powers of Tau, est générique et réutilisable pour n'importe quel circuit : de nombreux projets publient déjà des fichiers issus de cérémonies publiques à plusieurs contributeurs, ce qui évite de la relancer à chaque fois. La phase 2 est spécifique à votre circuit.

# Phase 1 : generique, reutilisable entre circuits
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_final.ptau \
  --name="contribution locale" -v
snarkjs powersoftau prepare phase2 pot12_final.ptau pot12_prepared.ptau -v

# Phase 2 : specifique au circuit AgeCheck
snarkjs groth16 setup build/AgeCheck.r1cs pot12_prepared.ptau age_0000.zkey
snarkjs zkey contribute age_0000.zkey age_final.zkey \
  --name="contributeur final" -v
snarkjs zkey export verificationkey age_final.zkey verification_key.json

L'argument 12 dans la commande powersoftau new fixe la taille maximale du circuit pris en charge (2^12 contraintes), largement suffisant pour un circuit aussi simple qu'AgeCheck. Pour un projet en production, n'utilisez jamais un fichier Powers of Tau généré par une seule personne sur une seule machine : téléchargez plutôt un fichier issu d'une cérémonie publique à contributeurs multiples, comme celle maintenue par le collectif Privacy & Scaling Explorations, afin qu'aucun participant isolé ne puisse reconstruire les paramètres secrets (le "toxic waste" de la cérémonie).

Étape 9 : générer la preuve avec snarkjs

Le témoin et la clé de preuve (zkey) en main, la génération de la preuve elle-même est la partie la plus rapide du processus.

snarkjs groth16 prove age_final.zkey witness.wtns proof.json public.json

Deux fichiers sont produits. proof.json contient la preuve compacte (trois points de courbe elliptique pour Groth16, quelques centaines d'octets au total). public.json contient uniquement les signaux publics du circuit, ici la valeur 1 correspondant à isAdult. La valeur privée age n'apparaît dans aucun des deux fichiers.

Étape 10 : vérifier la preuve côté serveur

La vérification se fait avec la clé de vérification exportée à l'étape 8, sans jamais avoir besoin du témoin ni de la valeur privée.

snarkjs groth16 verify verification_key.json public.json proof.json
# [INFO]  snarkJS: OK!

Le message [INFO] snarkJS: OK! confirme que le circuit a bien été exécuté avec une entrée satisfaisant la contrainte age ≥ 18, sans jamais dévoiler l'âge exact. Modifiez input.json avec une valeur inférieure à 18, régénérez le témoin et la preuve, et observez que public.json contient alors 0 pour isAdult : le circuit reste cohérent, seule la conclusion change.

Étape 11 : générer un vérificateur Solidity pour la blockchain

L'un des intérêts pratiques des zk-SNARK Groth16 est la faible taille et la faible complexité de la vérification, ce qui les rend adaptés à une exécution on-chain. snarkjs peut générer directement un contrat Solidity capable de vérifier la preuve.

snarkjs zkey export solidityverifier age_final.zkey verifier.sol
snarkjs zkey export soliditycalldata public.json proof.json > calldata.txt

Le fichier verifier.sol contient un contrat autonome avec une fonction verifyProof qui accepte les mêmes arguments que ceux exportés dans calldata.txt. Déployez-le avec Hardhat ou Foundry sur un réseau de test avant toute mise en production, et vérifiez la version du compilateur solc indiquée en en-tête du fichier généré : elle doit correspondre à celle configurée dans votre projet.

Étape 12 : assembler le projet complet de bout en bout

Voici l'arborescence complète du projet une fois toutes les étapes exécutées, avec un script npm qui enchaîne l'ensemble des commandes.

zkp-tutoriel/
├── schnorr.js
├── AgeCheck.circom
├── input.json
├── package.json
├── node_modules/circomlib/
├── build/
│   ├── AgeCheck.r1cs
│   ├── AgeCheck.sym
│   └── AgeCheck_js/
│       ├── AgeCheck.wasm
│       └── generate_witness.js
├── pot12_prepared.ptau
├── age_final.zkey
├── verification_key.json
├── witness.wtns
├── proof.json
├── public.json
└── verifier.sol

Ajoutez un script dans package.json pour reproduire tout le flux en une seule commande, utile pour une intégration continue ou pour re-tester rapidement après une modification du circuit.

{
  "scripts": {
    "witness": "node build/AgeCheck_js/generate_witness.js build/AgeCheck_js/AgeCheck.wasm input.json witness.wtns",
    "prove": "snarkjs groth16 prove age_final.zkey witness.wtns proof.json public.json",
    "verify": "snarkjs groth16 verify verification_key.json public.json proof.json",
    "e2e": "npm run witness && npm run prove && npm run verify"
  }
}

Aller plus loin : combiner plusieurs conditions dans un circuit

Le circuit AgeCheck ne vérifie qu'une seule condition. Dans un cas réel, il est fréquent de devoir prouver plusieurs affirmations à la fois, par exemple qu'un utilisateur a plus de 18 ans et moins de 100 ans, pour filtrer des entrées absurdes ou frauduleuses. circom permet de combiner des composants avec des portes logiques classiques, ici une porte ET (AND) appliquée aux deux résultats de comparaison.

pragma circom 2.1.6;

include "circomlib/circuits/comparators.circom";
include "circomlib/circuits/gates.circom";

template AgeRangeCheck() {
    signal input age;        // entree privee
    signal output isValidAdult;

    component geq = GreaterEqThan(8);
    geq.in[0] <== age;
    geq.in[1] <== 18;

    component leq = LessEqThan(8);
    leq.in[0] <== age;
    leq.in[1] <== 100;

    component andGate = AND();
    andGate.a <== geq.out;
    andGate.b <== leq.out;

    isValidAdult <== andGate.out;
}

component main = AgeRangeCheck();

Recompilez avec la même commande circom qu'à l'étape 7, régénérez un témoin avec une valeur d'âge de test, puis relancez la cérémonie de phase 2 pour ce nouveau circuit : le fichier R1CS ayant changé, l'ancienne clé de preuve ne sera plus compatible. C'est un bon exercice pour vérifier que vous avez bien intégré le piège numéro six de la section suivante, celui de la clé de vérification obsolète après modification du circuit.

Exemples de sortie attendue

Voici à quoi ressemble une exécution réussie de bout en bout, du calcul du témoin à la vérification finale.

$ npm run e2e

> [email protected] witness
> node build/AgeCheck_js/generate_witness.js build/AgeCheck_js/AgeCheck.wasm input.json witness.wtns

> [email protected] prove
> snarkjs groth16 prove age_final.zkey witness.wtns proof.json public.json

> [email protected] verify
> snarkjs groth16 verify verification_key.json public.json proof.json
[INFO]  snarkJS: OK!

Le contenu de public.json pour une entrée age = 25 ressemble à ceci : un tableau contenant uniquement la valeur "1", correspondant au signal de sortie isAdult. Aucune trace de la valeur 25 n'apparaît nulle part dans ce fichier, ni dans proof.json.

// public.json
["1"]

// proof.json (extrait, tronque)
{
  "pi_a": ["10495...", "88213...", "1"],
  "pi_b": [["...", "..."], ["...", "..."], ["1", "0"]],
  "pi_c": ["...", "...", "1"],
  "protocol": "groth16",
  "curve": "bn128"
}

Pièges courants à éviter

La majorité des incidents observés sur des projets ZKP réels ne viennent pas d'une faille dans les mathématiques du protocole, mais d'une erreur d'implémentation banale, souvent la même que celle qui touche les schémas de signature classiques depuis des décennies. Voici les six erreurs les plus fréquentes rencontrées en atelier, du protocole fait main jusqu'au circuit de production.

  • Réutiliser des paramètres jouets en production. Le nombre premier p de 31 bits utilisé dans l'exemple Schnorr est cassable en quelques secondes par un calcul de logarithme discret. Les vraies implémentations utilisent des courbes elliptiques comme secp256k1 ou Curve25519, avec des groupes d'ordre bien supérieur à 2^250.
  • Réutiliser un nonce r entre deux preuves. Comme démontré à l'étape 5, deux réponses partageant le même r permettent de retrouver le secret par simple soustraction. Générez toujours r avec crypto.randomBytes, jamais avec Math.random.
  • Oublier qu'un signal circom est privé par défaut. Une erreur fréquente consiste à croire qu'un signal doit être explicitement marqué "privé" pour rester caché. C'est l'inverse : tout ce qui n'est pas déclaré output ou listé en public dans le composant principal reste invisible du vérificateur.
  • Mélanger les versions de circom. La syntaxe des tableaux de signaux et certains opérateurs ont changé entre circom 2.0.x et les versions 2.1.x/2.2.x. Un pragma circom 2.1.6 en tête de fichier avec un compilateur 2.0.x plus ancien provoque des erreurs de compilation confuses.
  • Sous-estimer la cérémonie Powers of Tau. Un seul contributeur honnête suffit en théorie à garantir la sécurité de la cérémonie, mais un projet destiné à un usage réel ne doit jamais dépendre d'un unique participant local. Utilisez un fichier issu d'une cérémonie publique à contributions multiples.
  • Vérifier avec une clé obsolète après modification du circuit. Toute modification, même mineure, du fichier .circom change le fichier R1CS et invalide silencieusement l'ancienne verification_key.json. Régénérez systématiquement la clé de vérification après une recompilation.

Dépannage : erreurs fréquentes et solutions

Ce tableau récapitule les erreurs les plus souvent signalées par les développeurs qui suivent ce type de flux circom et snarkjs pour la première fois, avec la cause probable et la correction à appliquer. Gardez-le sous la main lors de vos premiers essais : la quasi-totalité de ces erreurs se règle en moins d'une minute une fois la cause identifiée.

Erreur rencontréeCause probableSolution
circom: command not foundLe binaire cargo n'est pas dans le PATHAjouter $HOME/.cargo/bin au PATH et recharger le shell
Error: Missing input signalLe nom du champ dans input.json ne correspond pas au signal déclaré dans le circuitVérifier l'orthographe exacte du signal (age, pas Age)
Assert Failed pendant groth16 setupLe fichier .ptau n'a pas été préparé pour la phase 2Exécuter snarkjs powersoftau prepare phase2 avant le setup
La vérification renvoie false alors que l'entrée semble correctepublic.json désynchronisé après une recompilation du circuitRégénérer le témoin, la preuve et public.json ensemble
Erreurs BigInt inattendues dans schnorr.jsL'opérateur % en JavaScript retourne des valeurs négatives pour un BigInt négatifUtiliser la formule ((x % m) + m) % m comme dans modPow
too many constraints ou circuit trop lent à compilerLargeur de bits surdimensionnée dans un composant comme GreaterEqThanRéduire la largeur au strict nécessaire (8 bits suffisent pour un âge)
Le contrat verifier.sol ne compile pasVersion de solc du projet incompatible avec le pragma généré par snarkjsAligner la version solc du hardhat.config.js sur celle du pragma généré
Génération du témoin très lente ou saturation mémoireCalcul du witness en WASM pur sur un circuit de grande tailleUtiliser le binaire C++ généré (option --c à la compilation) pour les circuits volumineux

Conseils avancés : PLONK, Halo2 et preuve côté navigateur

Groth16 reste le choix par défaut de circom et snarkjs pour sa preuve compacte et sa vérification bon marché, mais il impose un inconvénient de taille : chaque circuit nécessite sa propre cérémonie de phase 2. Changez une seule ligne du circuit et il faut relancer tout le processus. PLONK, également pris en charge par snarkjs, utilise un paramétrage universel : une seule cérémonie sert pour n'importe quel circuit jusqu'à une taille maximale donnée, au prix d'une preuve légèrement plus grande. Halo2, la génération suivante popularisée par l'écosystème Zcash, élimine complètement le trusted setup grâce à un argument d'engagement polynomial basé sur des courbes de type Pasta, en échange d'une complexité d'implémentation plus élevée.

Système de preuveTrusted setupTaille de la preuveCas d'usage typique
Groth16Par circuit (phase 2 dédiée)La plus compacteVérification on-chain à coût minimal
PLONKUniversel (une cérémonie réutilisable)IntermédiaireCircuits qui évoluent souvent en développement
Halo2AucunPlus grande, compressible par récursionSystèmes sans autorité de confiance unique

Autre piste à explorer une fois ce tutoriel terminé : la génération de preuve directement dans un navigateur. snarkjs expose une API compatible WebAssembly qui permet de calculer le témoin et la preuve côté client, sans jamais transmettre la valeur privée (comme l'âge exact) à un serveur. C'est le modèle retenu par plusieurs projets d'identité numérique qui cherchent à minimiser la donnée personnelle transmise. La contrepartie est un temps de calcul plus long sur des appareils mobiles peu puissants, à tester systématiquement avant tout déploiement grand public.

Sur le plan de la résistance aux futurs ordinateurs quantiques, notez que les zk-SNARK basés sur les courbes elliptiques (comme Groth16 sur bn128) ne sont pas conçus pour résister à un adversaire quantique, à la différence des schémas de signature à base de hachage comme SLH-DSA (SPHINCS+), finalisé par le NIST sous la référence FIPS 205 en août 2024. Les zk-STARK, une autre famille de preuves non traitée dans ce tutoriel, reposent uniquement sur des fonctions de hachage et offrent une meilleure marge de sécurité face à cette menace à long terme, au prix de preuves nettement plus volumineuses.

Un dernier point mérite votre attention avant de porter ce type de circuit en production : l'audit. Un circuit circom bogué peut sembler fonctionner parfaitement lors des tests tout en laissant passer des preuves invalides une fois exposé à des entrées malveillantes, notamment via des sous-contraintes manquantes qui laissent un signal sous-déterminé. Des outils d'analyse statique comme circomspect permettent de détecter une partie de ces défauts avant la mise en production, mais rien ne remplace une revue de code indépendante menée par une équipe spécialisée en cryptographie appliquée, en particulier avant tout déploiement gérant de vrais fonds ou des données personnelles réelles. Documentez aussi la provenance exacte de votre fichier Powers of Tau et de chaque contribution à la phase 2 : en cas d'audit réglementaire ou de litige, pouvoir retracer la chaîne de confiance du paramétrage devient un argument de conformité à part entière.

Ressources officielles pour aller plus loin

Le code source du compilateur circom est publié sur le dépôt GitHub officiel iden3/circom, tout comme snarkjs, la bibliothèque JavaScript utilisée dans ce tutoriel pour générer et vérifier les preuves Groth16. Le papier académique original décrivant le système Groth16 reste consultable sur l'archive Cryptology ePrint de Jens Groth (2016). Pour un projet destiné à un usage réel, les fichiers de cérémonie Powers of Tau à contributeurs multiples sont publiés par le collectif Privacy & Scaling Explorations. Enfin, le calendrier de support de Node.js, utile pour planifier une montée de version en production, est disponible sur la page officielle des releases Node.js.

Questions fréquentes

Qu'est-ce qu'une preuve à divulgation nulle de connaissance en une phrase ?

C'est une méthode cryptographique qui permet de prouver qu'une affirmation est vraie sans révéler aucune information supplémentaire, comme prouver que l'on a plus de 18 ans sans donner sa date de naissance.

Un ZKP reste-t-il sûr si le prouveur essaie de tricher ?

Oui, c'est le rôle de la propriété de correction (soundness). Un prouveur malhonnête ne peut convaincre le vérificateur d'une affirmation fausse qu'avec une probabilité négligeable, calculée mathématiquement en fonction des paramètres du protocole ou de la taille du corps fini utilisé par le circuit.

Faut-il maîtriser les courbes elliptiques pour utiliser circom ?

Non. circom et circomlib fournissent des composants prêts à l'emploi (comparateurs, portes logiques, fonctions de hachage) qui masquent la majorité des maths sous-jacentes. Une bonne compréhension de l'arithmétique modulaire suffit pour écrire des circuits simples comme celui de ce tutoriel.

Combien de temps prend une cérémonie Powers of Tau en production ?

Cela dépend entièrement du nombre de contributeurs recherchés et de la taille du circuit visé. Une cérémonie publique avec plusieurs dizaines de participants peut s'étaler sur plusieurs semaines. C'est justement pour éviter ce délai que la plupart des équipes réutilisent un fichier Powers of Tau déjà publié plutôt que d'en lancer un nouveau.

Peut-on générer une preuve directement dans un navigateur ?

Oui, snarkjs fournit une version WebAssembly utilisable côté client. Le calcul est plus lent que sur un serveur, mais cette approche évite de transmettre la donnée privée à qui que ce soit, ce qui en fait une option intéressante pour les usages liés à l'identité ou à la vérification d'âge.

Quelle est la différence entre un zk-SNARK et un zk-STARK ?

Un zk-SNARK comme Groth16 ou PLONK repose sur des courbes elliptiques et produit des preuves très compactes, mais nécessite généralement un trusted setup. Un zk-STARK repose uniquement sur des fonctions de hachage, ne nécessite aucun trusted setup et résiste mieux en théorie à un futur ordinateur quantique, au prix de preuves plus volumineuses et plus coûteuses à vérifier on-chain.

Le circuit d'exemple (âge ≥ 18) est-il utilisable tel quel en production ?

Non. Ce circuit illustre le principe mais omet des éléments indispensables en production, comme la liaison de la preuve à une identité vérifiée en amont (un document officiel signé, par exemple) pour empêcher un prouveur de simplement inventer un âge arbitraire. Un déploiement réel combine généralement le ZKP avec une source d'attestation de confiance.

Où trouver des fichiers Powers of Tau déjà générés ?

Plusieurs cérémonies publiques à contributeurs multiples publient leurs fichiers en accès libre, notamment celle maintenue par le collectif Privacy & Scaling Explorations. Réutiliser un de ces fichiers est la pratique recommandée plutôt que de générer un nouveau paramétrage seul sur sa machine.

Le code de ce tutoriel fonctionne-t-il tel quel sur Windows ?

Toutes les commandes présentées ici s'exécutent sans modification sous Linux, macOS et sous WSL2 (Windows Subsystem for Linux) sur Windows. Une installation directe de circom en PowerShell natif reste possible via le binaire précompilé, mais la compilation depuis les sources avec Cargo est nettement plus simple à mener dans un environnement Linux ou WSL2, notamment pour la gestion du PATH et des permissions d'exécution du binaire.