Les ordinateurs quantiques progressent à un rythme que la communauté cryptographique n’anticipait pas avant 2030. En août 2024, le NIST a publié trois standards finalisés, FIPS 203, FIPS 204 et FIPS 205, marquant la fin d’une décennie de sélection. En juin 2026, ces standards sont devenus la nouvelle référence pour toute infrastructure traitant des données à longue durée de vie. Ce tutoriel vous montre comment implémenter la cryptographie post-quantique dans Node.js en 12 étapes, depuis l’installation des dépendances jusqu’au déploiement d’un échange de clés hybride X25519 + ML-KEM-768.
Pourquoi la cryptographie post-quantique est urgente en 2026
L’algorithme de Shor, exécuté sur un ordinateur quantique disposant de suffisamment de qubits logiques, casse RSA-2048 et les courbes elliptiques en temps polynomial. La menace n’est pas théorique : en janvier 2025, Microsoft a annoncé son processeur quantique Majorana 1 capable de manipuler des qubits topologiques stables. IBM a dépassé le cap des 1 000 qubits avec son processeur Condor. Les experts estiment désormais qu’un ordinateur quantique cryptographiquement pertinent (CRQC) pourrait émerger entre 2030 et 2035.
La stratégie d’attaque dite « harvest now, decrypt later » est déjà en œuvre. Des acteurs étatiques collectent dès aujourd’hui des flux TLS chiffrés avec l’intention de les déchiffrer quand les CRQC seront disponibles. Les données médicales, les clés d’infrastructure, les secrets industriels transmis en 2026 restent vulnérables si vous n’avez pas migré. L’ANSSI, dans ses recommandations cryptographiques de 2026, préconise une approche hybride : combiner un algorithme classique (X25519 ou RSA) avec un algorithme post-quantique pendant la période de transition.
Le NIST a finalisé trois standards en août 2024 :
| Standard NIST | Algorithme | Ancien nom | Usage | Date de finalisation |
|---|---|---|---|---|
| FIPS 203 | ML-KEM | CRYSTALS-Kyber | Échange de clés (KEM) | Août 2024 |
| FIPS 204 | ML-DSA | CRYSTALS-Dilithium | Signature numérique | Août 2024 |
| FIPS 205 | SLH-DSA | SPHINCS+ | Signature (stateless hash) | Août 2024 |
Dans ce tutoriel, vous allez implémenter ML-KEM (échange de clés) et ML-DSA (signatures) dans Node.js, puis construire un échange de clés hybride compatible avec les recommandations IETF en cours.
Prérequis et versions
Avant de commencer, vérifiez que votre environnement correspond aux versions suivantes :
| Outil | Version minimale | Version recommandée | Commande de vérification |
|---|---|---|---|
| Node.js | 20.0.0 LTS | 22.x LTS (actuelle) | node --version |
| npm | 10.0.0 | 10.x | npm --version |
| @noble/post-quantum | 0.2.0 | dernière version stable | npm show @noble/post-quantum version |
| mlkem | 3.0.0 | dernière version stable | npm show mlkem version |
| OS | Linux / macOS / Windows 10+ | Ubuntu 22.04 / macOS 14 | uname -a |
Ce tutoriel utilise principalement @noble/post-quantum, une implémentation pure JavaScript sans dépendances C natives, idéale pour les environnements serverless et les déploiements conteneurisés. Pour les cas nécessitant les performances maximales ou l’interopérabilité avec OpenSSL, nous verrons également l’option liboqs-node dans les étapes avancées.
Étape 1 : Initialiser le projet Node.js
Créez un répertoire dédié et initialisez votre projet avec les modules ES activés. La cryptographie post-quantique bénéficie du type: "module" pour utiliser les imports natifs et éviter les problèmes de compatibilité CommonJS avec les librairies modernes.
mkdir pq-crypto-demo && cd pq-crypto-demo
npm init -y
# Activer les modules ES natifs
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.type='module'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2))"
Vérifiez que votre package.json contient bien "type": "module". Cette configuration évite les conflits entre CommonJS (require()) et les modules ES (import) que plusieurs paquets de cryptographie post-quantique utilisent.
Étape 2 : Installer les dépendances post-quantiques
Installez la librairie @noble/post-quantum de Paul Miller, auteur de plusieurs implémentations cryptographiques de référence pour l’écosystème JavaScript. Cette librairie implémente ML-KEM et ML-DSA en JavaScript pur, sans WebAssembly ni bindings natifs, ce qui simplifie les audits de sécurité et les déploiements multi-architectures.
# Librairie principale : ML-KEM + ML-DSA en JS pur
npm install @noble/post-quantum
# Optionnel : ML-KEM seul, implémentation alternative conforme FIPS 203
npm install mlkem
# Vérification de l'intégrité des paquets
npm audit
Après installation, vérifiez que le module se charge correctement :
node --input-type=module <<'EOF'
import { ml_kem768 } from '@noble/post-quantum/ml-kem';
console.log('ML-KEM-768 chargé avec succès');
EOF
# Sortie attendue : ML-KEM-768 chargé avec succès
Piège courant n°1 : Si vous obtenez ERR_REQUIRE_ESM, c'est que votre fichier utilise require() au lieu d'import. @noble/post-quantum est un module ES pur depuis la version 0.2.0. Renommez votre fichier en .mjs ou ajoutez "type": "module" dans package.json.
Étape 3 : Comprendre les niveaux de sécurité ML-KEM
ML-KEM (FIPS 203) propose trois niveaux de sécurité correspondant à des niveaux de résistance quantique différents. Le choix du niveau affecte directement la taille des clés et les performances. Avant d'écrire la moindre ligne de code, comprenez ces chiffres : ils déterminent le surcoût que vos clients et serveurs subiront à chaque connexion.
| Variante | Niveau NIST | Clé publique | Clé secrète | Texte chiffré | Équivalent classique |
|---|---|---|---|---|---|
| ML-KEM-512 | Niveau 1 | 800 octets | 1 632 octets | 768 octets | AES-128 |
| ML-KEM-768 | Niveau 3 | 1 184 octets | 2 400 octets | 1 088 octets | AES-192 |
| ML-KEM-1024 | Niveau 5 | 1 568 octets | 3 168 octets | 1 568 octets | AES-256 |
Pour comparaison, une clé publique RSA-2048 fait 256 octets et une clé ECDH X25519 fait seulement 32 octets. ML-KEM-768 est environ 37 fois plus volumineux que X25519, mais reste marginal pour des connexions TLS (quelques kilooctets supplémentaires par handshake). ML-KEM-768 est le choix recommandé pour la grande majorité des usages en 2026 : il correspond au niveau de sécurité AES-192 contre les attaquants quantiques.
L'échange de clés hybride X25519 + ML-KEM-768 est actuellement adopté par Chrome (depuis la version 131), Cloudflare et plusieurs navigateurs majeurs. Il garantit qu'un attaquant doit casser les deux algorithmes simultanément, protégeant vos sessions même si l'un d'eux s'avère vulnérable.
Étape 4 : Implémenter ML-KEM-768 (échange de clés)
ML-KEM est un mécanisme d'encapsulation de clés (KEM). À la différence de Diffie-Hellman, il ne produit pas de secret partagé par échange symétrique : l'émetteur génère un secret et l'encapsule dans un texte chiffré que seul le détenteur de la clé secrète peut décapsuler. Ce modèle est parfaitement adapté à TLS 1.3 où le client encapsule un secret pour le serveur.
// kem-demo.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem';
// === CÔTÉ SERVEUR : génération de la paire de clés ===
const { publicKey, secretKey } = ml_kem768.keygen();
console.log('Clé publique (octets):', publicKey.length); // 1184
console.log('Clé secrète (octets) :', secretKey.length); // 2400
// === CÔTÉ CLIENT : encapsulation ===
// Le client reçoit la clé publique du serveur et génère un secret partagé
const { cipherText, sharedSecret: clientSecret } = ml_kem768.encapsulate(publicKey);
console.log('Texte chiffré (octets):', cipherText.length); // 1088
console.log('Secret client (octets):', clientSecret.length); // 32
// === CÔTÉ SERVEUR : décapsulation ===
// Le serveur utilise sa clé secrète pour retrouver le même secret partagé
const serverSecret = ml_kem768.decapsulate(cipherText, secretKey);
// Vérification : les deux secrets doivent être identiques
const match = Buffer.compare(
Buffer.from(clientSecret),
Buffer.from(serverSecret)
) === 0;
console.log('Secret partagé identique:', match);
console.log('Secret partagé (hex):', Buffer.from(serverSecret).toString('hex').slice(0, 32) + '...');
Exécutez ce script avec node kem-demo.mjs. La sortie attendue :
Clé publique (octets): 1184
Clé secrète (octets) : 2400
Texte chiffré (octets): 1088
Secret client (octets): 32
Secret partagé identique: true
Secret partagé (hex): a3f2c891d4e7b56f902c1a8d34e5f678...
Piège courant n°2 : Ne confondez pas le texte chiffré (ciphertext) ML-KEM avec un message chiffré. Le ciphertext ML-KEM est uniquement le vecteur d'encapsulation du secret partagé. Pour chiffrer des données réelles, utilisez le secret partagé (32 octets) comme clé pour AES-256-GCM, comme montré à l'étape suivante.
Étape 5 : Chiffrement des données avec le secret ML-KEM
Le secret partagé ML-KEM (32 octets) sert de clé symétrique pour chiffrer les données réelles. La combinaison ML-KEM + AES-256-GCM est le schéma recommandé : sécurité post-quantique pour l'échange de clés, performance optimale pour le chiffrement des données. AES-256-GCM fournit à la fois confidentialité et intégrité grâce à son tag d'authentification.
// hybrid-encrypt.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem';
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
async function encrypt(recipientPublicKey, plaintext) {
// 1. Encapsuler un secret avec la clé publique ML-KEM du destinataire
const { cipherText: kemCiphertext, sharedSecret } = ml_kem768.encapsulate(recipientPublicKey);
// 2. Chiffrer le message avec AES-256-GCM en utilisant le secret ML-KEM
const iv = randomBytes(12); // 96 bits pour GCM
const cipher = createCipheriv('aes-256-gcm', Buffer.from(sharedSecret), iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(plaintext, 'utf8')),
cipher.final()
]);
const authTag = cipher.getAuthTag();
return { kemCiphertext, iv, encrypted, authTag };
}
async function decrypt(recipientSecretKey, { kemCiphertext, iv, encrypted, authTag }) {
// 1. Décapsuler le secret ML-KEM
const sharedSecret = ml_kem768.decapsulate(kemCiphertext, recipientSecretKey);
// 2. Déchiffrer avec AES-256-GCM
const decipher = createDecipheriv('aes-256-gcm', Buffer.from(sharedSecret), iv);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
}
// Démonstration
const { publicKey, secretKey } = ml_kem768.keygen();
const message = 'Données sensibles protégées contre les ordinateurs quantiques';
const pkg = await encrypt(publicKey, message);
const recovered = await decrypt(secretKey, pkg);
console.log('Message original :', message);
console.log('Message récupéré :', recovered);
console.log('Intégrité vérifiée:', message === recovered);
Piège courant n°3 : N'utilisez jamais le secret ML-KEM directement comme clé AES sans dérivation de clé (KDF) dans un contexte de production avec plusieurs sessions ou protocoles hybrides. Si votre protocole réutilise des clés ou combine plusieurs secrets (par exemple X25519 + ML-KEM), utilisez HKDF pour dériver une clé propre et isoler les contextes cryptographiques. L'étape 7 couvre ce cas.
Étape 6 : Implémenter ML-DSA-65 (signatures numériques)
ML-DSA (FIPS 204) remplace ECDSA pour les signatures numériques résistantes aux ordinateurs quantiques. ML-DSA-65 correspond au niveau de sécurité 3 (équivalent AES-192 contre les attaques quantiques). Basé sur les réseaux euclidiens (Module-Lattice), il est plus rapide que SLH-DSA et produit des signatures plus compactes, ce qui en fait le choix par défaut pour les API et les infrastructures à fort trafic.
| Variante ML-DSA | Niveau | Clé publique | Clé privée | Signature | Équivalent |
|---|---|---|---|---|---|
| ML-DSA-44 | Niveau 2 | 1 312 octets | 2 528 octets | 2 420 octets | AES-128 |
| ML-DSA-65 | Niveau 3 | 1 952 octets | 4 000 octets | 3 293 octets | AES-192 |
| ML-DSA-87 | Niveau 5 | 2 592 octets | 4 864 octets | 4 627 octets | AES-256 |
Pour comparaison, une signature ECDSA P-256 fait 64 octets et une signature Ed25519 fait 64 octets. ML-DSA-65 est environ 51 fois plus volumineuse qu'Ed25519. Pour les API REST ou les JWT, ce surcoût de quelques kilooctets est négligeable. Pour les PKI embarquées ou les protocoles IoT, un travail d'optimisation est nécessaire.
// signature-demo.mjs
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
// Génération de la paire de clés ML-DSA-65
const { publicKey, secretKey } = ml_dsa65.keygen();
console.log('Clé publique (octets):', publicKey.length); // 1952
console.log('Clé privée (octets) :', secretKey.length); // 4000
// Message à signer
const message = new TextEncoder().encode(
JSON.stringify({ action: 'transfert', montant: 50000, ts: Date.now() })
);
// Signature avec la clé privée
const signature = ml_dsa65.sign(secretKey, message);
console.log('Signature (octets) :', signature.length); // 3293
// Vérification avec la clé publique
const valid = ml_dsa65.verify(publicKey, message, signature);
console.log('Signature valide :', valid); // true
// Tentative de falsification
const tampered = new TextEncoder().encode(
JSON.stringify({ action: 'transfert', montant: 999999, ts: Date.now() })
);
const invalidSig = ml_dsa65.verify(publicKey, tampered, signature);
console.log('Falsification détectée:', !invalidSig); // true
Piège courant n°4 : ML-DSA ne supporte pas le modèle HashAndSign de la même façon qu'ECDSA. La spécification FIPS 204 inclut deux modes : la signature directe du message (mode pure) et la signature du hash avec contexte (mode pre-hash). @noble/post-quantum implémente le mode pure par défaut, ce qui est recommandé pour les nouvelles implémentations. Le mode pre-hash est réservé aux cas où le message est déjà haché par un autre système.
Étape 7 : Échange de clés hybride X25519 + ML-KEM-768
L'échange de clés hybride est la recommandation de l'ANSSI et de l'IETF pour la période de transition. Il combine un algorithme classique (X25519) avec un algorithme post-quantique (ML-KEM-768) : un attaquant doit compromettre les deux simultanément pour récupérer le secret partagé. C'est le schéma adopté par Chrome depuis la version 131 sous le nom X25519MLKEM768.
// hybrid-kem.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem';
import { createECDH, hkdfSync, randomBytes } from 'crypto';
// === SERVEUR : génération des clés ===
function serverKeygen() {
const ecdh = createECDH('x25519');
ecdh.generateKeys();
const kem = ml_kem768.keygen();
return {
classical: { privateKey: ecdh.getPrivateKey(), publicKey: ecdh.getPublicKey() },
pq: { secretKey: kem.secretKey, publicKey: kem.publicKey }
};
}
// === CLIENT : encapsulation hybride ===
function clientEncapsulate(serverClassicalPub, serverPqPub, sessionSalt) {
// 1. Ephemeral X25519
const clientEcdh = createECDH('x25519');
clientEcdh.generateKeys();
const classicalSecret = clientEcdh.computeSecret(serverClassicalPub);
// 2. ML-KEM-768
const { cipherText: kemCt, sharedSecret: kemSecret } = ml_kem768.encapsulate(serverPqPub);
// 3. Combiner les deux secrets avec HKDF-SHA256
const combinedInput = Buffer.concat([classicalSecret, Buffer.from(kemSecret)]);
const hybridKey = hkdfSync('sha256', combinedInput, sessionSalt, 'X25519+ML-KEM-768 v1', 32);
return {
clientClassicalPub: clientEcdh.getPublicKey(),
kemCiphertext: kemCt,
hybridKey: Buffer.from(hybridKey)
};
}
// === SERVEUR : décapsulation hybride ===
function serverDecapsulate(serverKeys, clientClassicalPub, kemCiphertext, sessionSalt) {
// 1. X25519
const serverEcdh = createECDH('x25519');
serverEcdh.setPrivateKey(serverKeys.classical.privateKey);
const classicalSecret = serverEcdh.computeSecret(clientClassicalPub);
// 2. ML-KEM-768
const kemSecret = ml_kem768.decapsulate(kemCiphertext, serverKeys.pq.secretKey);
// 3. Même dérivation HKDF avec le même sel
const combinedInput = Buffer.concat([classicalSecret, Buffer.from(kemSecret)]);
const hybridKey = hkdfSync('sha256', combinedInput, sessionSalt, 'X25519+ML-KEM-768 v1', 32);
return Buffer.from(hybridKey);
}
// Démonstration
const serverKeys = serverKeygen();
const sessionSalt = randomBytes(32); // Partagé via mécanisme sécurisé dans un vrai protocole
const { clientClassicalPub, kemCiphertext, hybridKey: clientKey } = clientEncapsulate(
serverKeys.classical.publicKey,
serverKeys.pq.publicKey,
sessionSalt
);
const serverKey = serverDecapsulate(serverKeys, clientClassicalPub, kemCiphertext, sessionSalt);
console.log('Clé hybride (hex):', clientKey.toString('hex').slice(0, 32) + '...');
console.log('Clés identiques :', Buffer.compare(clientKey, serverKey) === 0);
Piège courant n°5 : Le sel HKDF doit être transmis ou dérivé de façon authentifiée entre les deux parties. Dans cet exemple didactique, il est partagé en clair. En production (TLS 1.3), le sel est dérivé de la transcription du handshake, ce qui lie cryptographiquement le secret aux messages échangés et prévient les attaques par replay.
Étape 8 : Persistance sécurisée des clés post-quantiques
Les clés ML-KEM et ML-DSA sont volumineuses. La clé secrète ML-DSA-87 atteint 4 864 octets, contre 64 octets pour une clé Ed25519. Leur stockage et leur transport nécessitent une attention particulière pour éviter les fuites. Voici un module de gestion des clés avec chiffrement AES-256-GCM des clés au repos, dérivation de clé via scrypt, et sérialisation Base64.
// key-manager.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem';
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
import { writeFileSync, readFileSync } from 'fs';
const ALGO = 'aes-256-gcm';
function deriveKey(password, salt) {
// scrypt : N=131072 (128 Ko de RAM), protège contre les attaques GPU
return scryptSync(password, salt, 32, { N: 131072, r: 8, p: 1 });
}
function encryptKey(keyBytes, password) {
const salt = randomBytes(32);
const key = deriveKey(password, salt);
const iv = randomBytes(12);
const cipher = createCipheriv(ALGO, key, iv);
const encrypted = Buffer.concat([cipher.update(keyBytes), cipher.final()]);
const tag = cipher.getAuthTag();
return JSON.stringify({
salt: salt.toString('base64'),
iv: iv.toString('base64'),
tag: tag.toString('base64'),
encrypted: encrypted.toString('base64'),
algo: 'scrypt+aes-256-gcm',
version: 1
});
}
function decryptKey(stored, password) {
const { salt, iv, tag, encrypted } = JSON.parse(stored);
const key = deriveKey(password, Buffer.from(salt, 'base64'));
const decipher = createDecipheriv(ALGO, key, Buffer.from(iv, 'base64'));
decipher.setAuthTag(Buffer.from(tag, 'base64'));
return Buffer.concat([
decipher.update(Buffer.from(encrypted, 'base64')),
decipher.final()
]);
}
// Générer et sauvegarder une paire ML-KEM-768
const password = process.env.KEY_PASSWORD || 'utilisez-un-vrai-secret-en-production';
const { publicKey, secretKey } = ml_kem768.keygen();
writeFileSync('ml-kem-public.key', Buffer.from(publicKey).toString('base64'));
writeFileSync('ml-kem-secret.enc', encryptKey(Buffer.from(secretKey), password));
console.log('Clés ML-KEM-768 générées et sauvegardées');
// Rechargement et déchiffrement
const storedSk = readFileSync('ml-kem-secret.enc', 'utf8');
const recoveredSk = decryptKey(storedSk, password);
console.log('Clé secrète récupérée:', Buffer.compare(Buffer.from(secretKey), recoveredSk) === 0);
Dans un environnement de production, le mot de passe de chiffrement doit venir d'un gestionnaire de secrets (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) et non d'une variable d'environnement. Les clés publiques peuvent être stockées en clair mais doivent être protégées en intégrité par une signature d'autorité de certification.
Étape 9 : SLH-DSA (SPHINCS+) pour les signatures à longue durée de vie
SLH-DSA (FIPS 205), anciennement SPHINCS+, est la troisième option de signature post-quantique standardisée par le NIST. Contrairement à ML-DSA qui repose sur la dureté des réseaux euclidiens, SLH-DSA est basé uniquement sur des fonctions de hachage, une construction dont la sécurité est mieux comprise et plus conservatrice face à d'éventuelles cryptanalyses futures des réseaux euclidiens.
SLH-DSA présente des signatures beaucoup plus volumineuses (jusqu'à 49 856 octets) mais une clé publique très compacte (32 à 64 octets selon la variante). Les variantes fast (suffixe -f) privilégient la vitesse de signature au détriment de la taille. Les variantes small (suffixe -s) minimisent la taille des signatures au détriment de la vitesse. SLH-DSA est recommandé pour les certificats racine PKI, les documents légaux à valeur probante sur 20 ans, et les cas où la diversité algorithmique est prioritaire.
// slh-dsa-demo.mjs
import { slh_dsa_shake_128f } from '@noble/post-quantum/slh-dsa';
// SLH-DSA-SHAKE-128f : signatures rapides (f = fast), niveau sécurité 1
const { publicKey, secretKey } = slh_dsa_shake_128f.keygen();
console.log('Clé publique (octets):', publicKey.length); // 32
console.log('Clé privée (octets) :', secretKey.length); // 64
const document = new TextEncoder().encode(
'Certificat racine PKI souveraine — valide 20 ans — ANSSI conforme'
);
const signature = slh_dsa_shake_128f.sign(secretKey, document);
console.log('Signature (octets) :', signature.length); // 17088
const valid = slh_dsa_shake_128f.verify(publicKey, document, signature);
console.log('Signature valide :', valid);
Les variantes disponibles dans @noble/post-quantum incluent slh_dsa_sha2_128f, slh_dsa_sha2_128s, slh_dsa_shake_128f, slh_dsa_shake_128s et leurs équivalents 192 et 256 bits. La variante SHAKE est recommandée par le NIST pour les nouvelles implémentations ; SHA2 est disponible pour la compatibilité avec les systèmes FIPS existants.
Étape 10 : Intégration dans une API Express.js
Cette étape montre comment intégrer ML-DSA dans une API Express.js pour signer les réponses et vérifier les requêtes côté serveur. C'est le schéma adapté aux microservices qui doivent s'authentifier mutuellement de façon résistante aux ordinateurs quantiques.
// api-server.mjs
import express from 'express';
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
// npm install express
const app = express();
app.use(express.json());
// Clés chargées au démarrage (en production : depuis un fichier chiffré ou HSM)
const { publicKey: serverPub, secretKey: serverSk } = ml_dsa65.keygen();
// Exposer la clé publique du serveur
app.get('/api/public-key', (req, res) => {
res.json({
algorithm: 'ML-DSA-65',
standard: 'FIPS-204',
publicKey: Buffer.from(serverPub).toString('base64'),
keySize: serverPub.length
});
});
// Middleware : signer toutes les réponses JSON avec ML-DSA-65
function signResponse(req, res, next) {
const originalJson = res.json.bind(res);
res.json = (data) => {
const payload = JSON.stringify(data);
const signature = ml_dsa65.sign(serverSk, new TextEncoder().encode(payload));
res.setHeader('X-PQ-Signature', Buffer.from(signature).toString('base64'));
res.setHeader('X-PQ-Algorithm', 'ML-DSA-65');
return originalJson(data);
};
next();
}
app.get('/api/data', signResponse, (req, res) => {
res.json({ message: 'Données signées post-quantiques', ts: new Date().toISOString() });
});
// Endpoint : vérifier une signature cliente
app.post('/api/verify', (req, res) => {
const { clientPublicKey, payload, signature } = req.body;
try {
const pubKey = new Uint8Array(Buffer.from(clientPublicKey, 'base64'));
const sig = new Uint8Array(Buffer.from(signature, 'base64'));
const msg = new TextEncoder().encode(payload);
const valid = ml_dsa65.verify(pubKey, msg, sig);
res.json({ valid, algorithm: 'ML-DSA-65' });
} catch {
res.status(400).json({ error: 'Données de vérification invalides' });
}
});
app.listen(3000, () => console.log('Serveur PQ démarré sur le port 3000'));
Piège courant n°6 : Régénérer les clés ML-DSA à chaque redémarrage du serveur sans les persister rend votre API inutilisable entre redémarrages : les clients qui ont mis en cache votre ancienne clé publique échoueront à vérifier les nouvelles signatures. En production, chargez les clés depuis un fichier chiffré (comme à l'étape 8) ou un HSM au démarrage.
Étape 11 : Suite de tests pour la cryptographie post-quantique
Les tests cryptographiques doivent couvrir non seulement le fonctionnement nominal, mais aussi la résistance aux manipulations : vecteurs falsifiés, clés corrompues, entrées de longueur incorrecte. Voici une suite complète avec Node.js assert natif, sans dépendances externes.
// tests/pq-crypto.test.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem';
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
import assert from 'assert/strict';
let passed = 0;
function test(name, fn) {
try { fn(); console.log('[PASS]', name); passed++; }
catch(e) { console.error('[FAIL]', name, ':', e.message); }
}
// Tailles des clés ML-KEM-768
test('ML-KEM-768 : tailles des clés', () => {
const { publicKey, secretKey } = ml_kem768.keygen();
assert.equal(publicKey.length, 1184);
assert.equal(secretKey.length, 2400);
});
// Cohérence encapsulation/décapsulation
test('ML-KEM-768 : cohérence encapsulation/décapsulation', () => {
const { publicKey, secretKey } = ml_kem768.keygen();
const { cipherText, sharedSecret: s1 } = ml_kem768.encapsulate(publicKey);
const s2 = ml_kem768.decapsulate(cipherText, secretKey);
assert.deepEqual(s1, s2);
});
// Mauvaise clé secrète produit un résultat différent
test('ML-KEM-768 : mauvaise clé secrète', () => {
const { publicKey } = ml_kem768.keygen();
const { cipherText, sharedSecret: correct } = ml_kem768.encapsulate(publicKey);
const { secretKey: wrongSk } = ml_kem768.keygen();
const wrong = ml_kem768.decapsulate(cipherText, wrongSk);
assert.notDeepEqual(wrong, correct);
});
// ML-DSA-65 : signature valide
test('ML-DSA-65 : sign/verify nominal', () => {
const { publicKey, secretKey } = ml_dsa65.keygen();
const msg = new TextEncoder().encode('Message de test ML-DSA');
const sig = ml_dsa65.sign(secretKey, msg);
assert.equal(sig.length, 3293);
assert.ok(ml_dsa65.verify(publicKey, msg, sig));
});
// Falsification du message détectée
test('ML-DSA-65 : détection de falsification', () => {
const { publicKey, secretKey } = ml_dsa65.keygen();
const original = new TextEncoder().encode('Message original');
const falsified = new TextEncoder().encode('Message falsifié');
const sig = ml_dsa65.sign(secretKey, original);
assert.ok(!ml_dsa65.verify(publicKey, falsified, sig));
});
// Signature corrompue rejetée
test('ML-DSA-65 : signature corrompue rejetée', () => {
const { publicKey, secretKey } = ml_dsa65.keygen();
const msg = new TextEncoder().encode('Test intégrité');
const sig = ml_dsa65.sign(secretKey, msg);
const corrupted = new Uint8Array(sig);
corrupted[42] ^= 0xFF;
assert.ok(!ml_dsa65.verify(publicKey, msg, corrupted));
});
console.log(`\n${passed}/6 tests réussis`);
Exécutez avec node tests/pq-crypto.test.mjs. Pour intégrer ces tests dans un pipeline CI, ajoutez "test": "node tests/pq-crypto.test.mjs" dans le champ scripts de votre package.json.
Étape 12 : Benchmarking et optimisation
Avant de déployer en production, mesurez l'impact réel des algorithmes post-quantiques sur votre infrastructure. Les performances dépendent du matériel, de la charge et de l'implémentation choisie (JavaScript pur vs natif).
// benchmark.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem';
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
import { createECDH } from 'crypto';
const ITER = 100;
function bench(name, fn) {
for (let i = 0; i < 5; i++) fn(); // warm-up
const t0 = performance.now();
for (let i = 0; i < ITER; i++) fn();
const ms = (performance.now() - t0) / ITER;
console.log(`${name.padEnd(30)} ${ms.toFixed(3).padStart(8)} ms/op`);
}
const kemKeys = ml_kem768.keygen();
const { cipherText } = ml_kem768.encapsulate(kemKeys.publicKey);
const dsaKeys = ml_dsa65.keygen();
const msg = new TextEncoder().encode('benchmark');
const sig = ml_dsa65.sign(dsaKeys.secretKey, msg);
bench('ML-KEM-768 keygen', () => ml_kem768.keygen());
bench('ML-KEM-768 encapsulate', () => ml_kem768.encapsulate(kemKeys.publicKey));
bench('ML-KEM-768 decapsulate', () => ml_kem768.decapsulate(cipherText, kemKeys.secretKey));
bench('X25519 keygen', () => { const e = createECDH('x25519'); e.generateKeys(); });
bench('ML-DSA-65 keygen', () => ml_dsa65.keygen());
bench('ML-DSA-65 sign', () => ml_dsa65.sign(dsaKeys.secretKey, msg));
bench('ML-DSA-65 verify', () => ml_dsa65.verify(dsaKeys.publicKey, msg, sig));
Résultats typiques sur un serveur moderne (Intel Xeon, Node.js 22.x) avec @noble/post-quantum (JavaScript pur) :
| Opération | Temps moyen | Comparaison classique | Facteur |
|---|---|---|---|
| ML-KEM-768 keygen | ~0.15 ms | X25519 keygen : ~0.03 ms | 5x plus lent |
| ML-KEM-768 encapsulate | ~0.18 ms | X25519 ECDH : ~0.05 ms | 4x plus lent |
| ML-KEM-768 decapsulate | ~0.20 ms | X25519 ECDH : ~0.05 ms | 4x plus lent |
| ML-DSA-65 keygen | ~1.20 ms | Ed25519 keygen : ~0.10 ms | 12x plus lent |
| ML-DSA-65 sign | ~1.80 ms | Ed25519 sign : ~0.08 ms | 22x plus lent |
| ML-DSA-65 verify | ~0.80 ms | Ed25519 verify : ~0.12 ms | 7x plus lent |
Avec liboqs-node (bindings C natifs avec AVX2), les performances de ML-KEM-768 sont comparables à celles de X25519, et ML-DSA-65 est environ 8 fois plus rapide qu'en JavaScript pur. Pour les serveurs signant plus de 500 réponses par seconde, liboqs-node est recommandé.
5 pièges courants supplémentaires
Piège 7 : Sérialiser les clés comme des chaînes de caractères brutes. Les clés ML-KEM et ML-DSA sont des Uint8Array, pas des objets PEM. Utilisez Buffer.from(key).toString('base64') pour les sérialiser et new Uint8Array(Buffer.from(str, 'base64')) pour les désérialiser. Un simple JSON.stringify(key) produit {"0":43,"1":12,...}, un objet incomplet qui échouera silencieusement lors du rechargement.
Piège 8 : Réutiliser les paires de clés KEM pour plusieurs sessions. ML-KEM est conçu pour un usage à courte durée de vie (échange de clés de session). Contrairement à RSA qui peut chiffrer directement des messages avec une clé longue durée, ML-KEM doit générer un nouveau couple (cipherText, sharedSecret) par session. Stocker et réutiliser un sharedSecret compromet la confidentialité persistante (forward secrecy).
Piège 9 : Négliger la taille des en-têtes HTTP. Un échange de clés hybride X25519 + ML-KEM-768 ajoute environ 1,1 ko au handshake TLS. Certains équilibreurs de charge et proxys (nginx, HAProxy, AWS ALB) limitent la taille des en-têtes HTTP à 8 ko par défaut. Si vous transmettez des clés ML-DSA-65 (1 952 octets encodés en Base64 donnent ~2,6 ko) dans des en-têtes d'authentification, vérifiez la configuration maximale de vos proxys.
Piège 10 : Ignorer la validation de longueur des entrées. Si vous exposez un endpoint qui accepte des clés publiques ou des ciphertexts ML-KEM, validez la longueur avant de les passer aux fonctions cryptographiques. Une entrée de longueur incorrecte déclenchera une exception dans @noble/post-quantum, mais sans validation préalable, un attaquant peut provoquer des crashs ou des fuites de timing dans votre serveur Node.js.
Piège 11 : Mélanger ML-KEM-768 et ML-KEM-512 dans le même système sans versioning. Si votre API passe de ML-KEM-768 à ML-KEM-512 sans versioning des messages, les clients existants qui enverront des ciphertexts de 1 088 octets échoueront silencieusement avec les serveurs attendant 768 octets (ML-KEM-512). Incluez toujours un champ kemVariant dans vos structures de données.
Support natif dans Node.js et OpenSSL en 2026
En juin 2026, Node.js ne supporte pas nativement les algorithmes post-quantiques NIST via son module crypto intégré. Le module crypto de Node.js repose sur OpenSSL, qui expose les algorithmes post-quantiques via le projet OQS-Provider, un provider OpenSSL 3.x développé par l'équipe Open Quantum Safe. Ce provider n'est toutefois pas inclus dans les distributions OpenSSL officielles et nécessite une compilation séparée.
OpenSSL 3.4.x a introduit un support expérimental de ML-KEM dans son code source principal (branche upstream), mais il n'est pas activé par défaut dans les binaires distribués par les systèmes d'exploitation. Le groupe de travail Node.js sur la cryptographie (node/crypto) discute d'une intégration directe de ML-KEM et ML-DSA dans le module crypto natif pour une version future. En attendant, @noble/post-quantum offre la solution la plus accessible pour les environnements Node.js standards.
Cloudflare rapporte que plus de 25% de ses connexions TLS 1.3 utilisent désormais l'échange de clés hybride X25519 + ML-KEM-768 depuis l'activation par défaut dans Chrome 131. Google a activé ce schéma pour Gmail et les services Google Workspace en 2025. Ces déploiements à grande échelle confirment que la latence et le surcoût en bande passante sont acceptables en production.
Conformité et cadre réglementaire européen
En Europe, la migration vers la cryptographie post-quantique s'inscrit dans plusieurs cadres réglementaires qui rendent cette transition non seulement souhaitable mais progressivement obligatoire.
Le Cyber Resilience Act (CRA), avec ses premières échéances entrées en vigueur en 2026, impose aux fabricants de produits numériques de gérer les vulnérabilités cryptographiques pendant la durée de vie commerciale du produit. Une application Node.js distribuée commercialement qui utilise RSA ou ECDH sans plan de migration post-quantique documenté pourra être considérée comme non conforme si des vulnérabilités quantiques sont déclarées avant la fin de vie du produit.
La directive NIS2 exige des entités essentielles et importantes de l'UE qu'elles maintiennent une cartographie de leurs actifs cryptographiques (inventaire). Cet inventaire est la première étape de toute migration post-quantique : vous ne pouvez pas migrer ce que vous n'avez pas identifié.
L'ANSSI recommande depuis 2025 l'adoption de l'approche hybride (algorithme classique + algorithme post-quantique) pour toutes les nouvelles infrastructures critiques françaises. La stratégie nationale de cybersécurité 2026-2030 place la cryptographie post-quantique comme priorité dans la feuille de route des opérateurs d'importance vitale (OIV).
Le règlement eIDAS 2, dont le déploiement avance en 2026, définit des exigences pour les signatures électroniques qualifiées (QES). Les prestataires de services de confiance (PSC) opérant sous eIDAS 2 devront inclure un plan de migration vers ML-DSA ou SLH-DSA dans leur politique de sécurité pour maintenir leur qualification à long terme.
Conseils avancés : liboqs-node et performances natives
Installation de liboqs-node pour des performances de niveau production
Pour les applications à fort volume nécessitant des performances natives, liboqs-node fournit des bindings C/C++ vers la librairie Open Quantum Safe (openquantumsafe.org), qui implémente tous les algorithmes NIST en C optimisé avec support des instructions AVX2 et AVX-512. L'installation nécessite un environnement de compilation C++ :
# Prérequis système (Ubuntu/Debian)
sudo apt-get install cmake ninja-build build-essential libssl-dev
# Compiler liboqs depuis les sources
git clone --depth 1 https://github.com/open-quantum-safe/liboqs.git
cd liboqs && cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release
cmake --build build && sudo cmake --install build
cd ..
# Installer le binding Node.js
npm install node-liboqs
# Vérification
node -e "const oqs = require('node-liboqs'); console.log(oqs.getEnabledKEMAlgorithms().join(', '))"
Avec les bindings natifs liboqs, ML-KEM-768 encapsulation descend à environ 0.04 ms (contre 0.18 ms en JavaScript pur), soit des performances comparables à X25519. ML-DSA-65 sign descend à environ 0.15 ms (contre 1.80 ms), permettant de signer plus de 6 000 messages par seconde sur un seul cœur.
Stratégie de migration en 3 phases
La migration vers la cryptographie post-quantique ne se fait pas en une nuit. La stratégie recommandée par l'ANSSI et le NIST pour 2026 est une approche hybride progressive :
Phase 1 (maintenant, 2026) : Inventaire cryptographique complet. Identifiez tous les usages RSA, ECDH, ECDSA dans votre base de code Node.js. Documentez les durées de vie des données chiffrées. Priorisez les données dont la durée de vie dépasse 5 ans (données médicales, financières, secrets d'État).
Phase 2 (2026-2027) : Déploiement hybride. Ajoutez ML-KEM en parallèle de X25519 pour tous les échanges de clés. Ajoutez ML-DSA en parallèle d'Ed25519 pour les signatures critiques. Les deux algorithmes coexistent, aucun service n'est interrompu. Mesurez l'impact sur les performances.
Phase 3 (2028+) : Migration complète. Une fois les clients mis à jour et la maturité des implémentations confirmée par la communauté cryptographique, retirez les algorithmes classiques des protocoles critiques. Maintenez X25519/Ed25519 uniquement pour la compatibilité descendante avec les anciens clients.
Guide de dépannage : 8 problèmes fréquents
Problème 1 : ERR_REQUIRE_ESM lors de l'import de @noble/post-quantum. Cause : votre fichier utilise require(). Solution : renommez en .mjs ou ajoutez "type": "module" dans package.json. Si vous devez rester en CommonJS, utilisez l'import dynamique : const { ml_kem768 } = await import('@noble/post-quantum/ml-kem');
Problème 2 : TypeError: ml_kem768.keygen is not a function. Cause : mauvais chemin d'import. L'import correct est from '@noble/post-quantum/ml-kem' (avec le sous-chemin), pas from '@noble/post-quantum'. Vérifiez aussi que vous importez la variante correcte : ml_kem512, ml_kem768, ou ml_kem1024.
Problème 3 : RangeError: Invalid typed array length lors de l'encapsulation. Cause : la clé publique fournie n'a pas la bonne longueur. ML-KEM-768 attend exactement 1 184 octets. Si vous sérialisez/désérialisez avec Base64, vérifiez l'absence de caractères d'espacement ou de troncation. Ajoutez assert.equal(publicKey.length, 1184) avant l'appel.
Problème 4 : Les secrets partagés sont différents côté client et serveur. Cause la plus fréquente : erreur de sérialisation du ciphertext. Si vous transmettez le cipherText via HTTP en Base64, la désérialisation doit produire exactement le même Uint8Array. Un JSON.parse(JSON.stringify(cipherText)) produit un objet ordinaire, pas un Uint8Array. Utilisez new Uint8Array(Buffer.from(base64str, 'base64')).
Problème 5 : Performances dégradées sous charge. ML-DSA-65 en JavaScript pur est synchrone et CPU-intensif (~1.8 ms par signature). Avec des centaines de signatures par seconde, le thread Node.js est saturé. Solution : utilisez worker_threads pour déporter les opérations cryptographiques sur des threads séparés, ou migrez vers liboqs-node pour des performances 10 à 15 fois supérieures.
Problème 6 : Échec de vérification de signature ML-DSA après sérialisation. Cause : le message a été modifié entre la signature et la vérification, par exemple modification de l'encodage UTF-8, ajout de BOM, normalisation Unicode. Toujours signer des Uint8Array binaires produits par new TextEncoder().encode(str) de façon cohérente des deux côtés. Évitez de signer des chaînes JavaScript directement.
Problème 7 : Conflit de version entre @noble/post-quantum et @noble/curves. Ces deux paquets partagent des utilitaires internes. En cas de conflit, vérifiez npm ls @noble/post-quantum @noble/curves et épinglez les versions dans package.json. Les versions mineures récentes sont généralement compatibles entre elles.
Problème 8 : Erreur ENOMEM lors de la génération de nombreuses clés ML-DSA. Les clés ML-DSA-87 utilisent jusqu'à 4 864 octets pour la clé privée. Si vous générez des milliers de paires de clés en mémoire sans libération (par exemple dans une boucle de test ou de provisioning), la consommation mémoire peut dépasser les limites de Node.js (512 Mo par défaut). Libérez les références après usage ou augmentez la limite via --max-old-space-size=2048.
Couverture connexe
Articles liés sur shattered.io
- ECDSA et Ed25519 dans Node.js : 12 Étapes [2026] : implémenter les signatures classiques avant de migrer vers ML-DSA
- Cryptographie Post-Quantique : 50% du Web Maintenant Protégé [2026] : panorama de l'adoption mondiale et des standards NIST
- AES-256 dans Node.js : 12 Étapes [2026] : chiffrement symétrique combiné avec ML-KEM pour protéger les données
- TLS 1.3 dans Node.js : 12 Étapes [2026] : configuration HTTPS pour intégrer les échanges de clés hybrides
- Module Crypto de Node.js : 12 Étapes [2026] : référence complète du module crypto natif de Node.js
- Ed25519 vs RSA : 50x Plus Rapide, Clés 8x Plus Petites [2026] : comparaison des signatures classiques, point de départ avant ML-DSA
FAQ : cryptographie post-quantique dans Node.js
Faut-il migrer immédiatement vers la cryptographie post-quantique ? Si vos données chiffrées aujourd'hui doivent rester confidentielles pendant plus de 5 à 10 ans, oui. L'attaque « harvest now, decrypt later » est déjà en cours. Pour les données à courte durée de vie (sessions web, tokens JWT à expiration 1 heure), la migration peut attendre 2027-2028. Commencez par l'inventaire cryptographique maintenant, quel que soit votre profil de risque.
ML-KEM remplace-t-il complètement X25519 dans TLS ? Non, pas encore. La recommandation actuelle (IETF, ANSSI, NIST) est l'hybridation X25519 + ML-KEM-768 pendant la période de transition. ML-KEM seul sera recommandé quand les implémentations seront matures et validées par la cryptanalyse communautaire, à l'horizon 2028+.
Node.js a-t-il un support natif pour ML-KEM ou ML-DSA ? En juin 2026, non. Le module crypto de Node.js ne supporte pas encore ML-KEM ou ML-DSA en natif. Utilisez @noble/post-quantum pour une solution pure JavaScript ou liboqs-node pour des performances natives. Une intégration dans OpenSSL 3.x et Node.js est en cours de discussion dans les groupes de travail respectifs.
@noble/post-quantum est-il audité en sécurité ? La librairie @noble de Paul Miller est largement utilisée dans l'écosystème Ethereum et a fait l'objet d'audits par des firmes spécialisées. @noble/post-quantum suit les vecteurs de test officiels NIST publiés dans les FIPS 203, 204 et 205. Une librairie JavaScript ne peut pas être certifiée FIPS 140-2/3 ; pour une certification formelle, utilisez un HSM ou une librairie C certifiée comme liboqs.
Quelle est la différence entre ML-KEM et ML-DSA ? ML-KEM est un mécanisme d'encapsulation de clés (KEM) : il sert à établir un secret partagé entre deux parties, utilisé ensuite pour le chiffrement symétrique. ML-DSA est un algorithme de signature numérique : il prouve l'authenticité et l'intégrité d'un message sans établir de secret. Ces deux rôles sont complémentaires, exactement comme ECDH (échange de clés) et ECDSA (signature) dans la cryptographie classique.
Quand choisir SLH-DSA plutôt que ML-DSA ? SLH-DSA repose uniquement sur la sécurité des fonctions de hachage, une hypothèse plus conservative que les réseaux euclidiens de ML-DSA. En contrepartie, ses signatures sont beaucoup plus volumineuses (17 à 50 ko). Préférez SLH-DSA pour les usages à très longue durée de vie (certificats racine sur 20 ans, documents légaux) et ML-DSA pour les usages opérationnels à fort volume (API, TLS, microservices).
Quel est l'impact sur les performances d'un serveur Express ? Avec @noble/post-quantum (JavaScript pur), une signature ML-DSA-65 prend environ 1.8 ms sur un CPU moderne. Un serveur qui signe 100 réponses par seconde utilise environ 18% d'un cœur CPU uniquement pour les signatures. Pour des charges plus élevées, utilisez liboqs-node (10 à 15 fois plus rapide) ou déportez les signatures sur des workers dédiés via worker_threads.
Les certificats TLS post-quantiques sont-ils disponibles pour les domaines publics ? Début 2026, les autorités de certification publiques (Let's Encrypt, DigiCert) n'émettent pas encore de certificats TLS ML-DSA pour les domaines publics. L'infrastructure PKI publique évolue lentement en raison des contraintes de compatibilité des navigateurs. Pour les PKI internes (mutual TLS, microservices), vous pouvez dès maintenant déployer vos propres certificats ML-DSA avec OpenSSL + OQS-Provider.




