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 NISTAlgorithmeAncien nomUsageDate de finalisation
FIPS 203ML-KEMCRYSTALS-KyberÉchange de clés (KEM)Août 2024
FIPS 204ML-DSACRYSTALS-DilithiumSignature numériqueAoût 2024
FIPS 205SLH-DSASPHINCS+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 :

OutilVersion minimaleVersion recommandéeCommande de vérification
Node.js20.0.0 LTS22.x LTS (actuelle)node --version
npm10.0.010.xnpm --version
@noble/post-quantum0.2.0dernière version stablenpm show @noble/post-quantum version
mlkem3.0.0dernière version stablenpm show mlkem version
OSLinux / macOS / Windows 10+Ubuntu 22.04 / macOS 14uname -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.

VarianteNiveau NISTClé publiqueClé secrèteTexte chiffréÉquivalent classique
ML-KEM-512Niveau 1800 octets1 632 octets768 octetsAES-128
ML-KEM-768Niveau 31 184 octets2 400 octets1 088 octetsAES-192
ML-KEM-1024Niveau 51 568 octets3 168 octets1 568 octetsAES-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-DSANiveauClé publiqueClé privéeSignatureÉquivalent
ML-DSA-44Niveau 21 312 octets2 528 octets2 420 octetsAES-128
ML-DSA-65Niveau 31 952 octets4 000 octets3 293 octetsAES-192
ML-DSA-87Niveau 52 592 octets4 864 octets4 627 octetsAES-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érationTemps moyenComparaison classiqueFacteur
ML-KEM-768 keygen~0.15 msX25519 keygen : ~0.03 ms5x plus lent
ML-KEM-768 encapsulate~0.18 msX25519 ECDH : ~0.05 ms4x plus lent
ML-KEM-768 decapsulate~0.20 msX25519 ECDH : ~0.05 ms4x plus lent
ML-DSA-65 keygen~1.20 msEd25519 keygen : ~0.10 ms12x plus lent
ML-DSA-65 sign~1.80 msEd25519 sign : ~0.08 ms22x plus lent
ML-DSA-65 verify~0.80 msEd25519 verify : ~0.12 ms7x 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

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.