ECDSA (Elliptic Curve Digital Signature Algorithm) reste en 2026 l’un des mécanismes de signature les plus utilisés sur le web, dans les API et dans les portefeuilles crypto. Une étude publiée en août 2026 sous le titre “The State of Signatures on the Web” a mesuré, sur son échantillon de certificats TLS, une répartition de 61,6 % pour RSA et 38,4 % pour ECDSA, sans aucun certificat Ed25519 émis par une autorité publique dans ce même échantillon. Une autre mesure, basée sur des données de transparence des certificats analysées en juillet 2026, donnait un résultat quasi inversé (50,3 % RSA contre 49,7 % ECDSA). Les deux chiffres ne portent pas sur le même périmètre, mais ils racontent la même histoire : ECDSA a largement rattrapé RSA et s’impose comme le choix par défaut pour signer des jetons JWT, des webhooks et des transactions.

Ce tutoriel vous montre comment générer des clés ECDSA, signer et vérifier des messages, encoder des signatures au bon format, et construire un projet complet avec Node.js : une API Express qui signe ses webhooks et émet des jetons JWT en ES256. Chaque bloc de code a été testé avec le module crypto natif de Node.js, sans dépendance externe pour la partie cryptographique.

ECDSA n’est pas qu’une curiosité académique : c’est l’algorithme qui signe chaque transaction Bitcoin et Ethereum (sur la courbe secp256k1), qui protège une bonne partie des certificats TLS modernes, et qui équipe par défaut la signature de jetons JWT en ES256 dans de nombreux frameworks d’authentification. Si vous développez une API qui doit prouver l’origine d’un message, émettre des jetons de session, ou signer des réponses pour qu’un client tiers puisse vérifier leur intégrité sans partager de secret, ECDSA est très probablement le bon outil. Ce guide part du générateur de clés le plus simple jusqu’à une API complète, en passant par les pièges qui causent le plus d’incidents en production.

Qu’est-ce que la signature ECDSA et pourquoi elle compte en 2026

ECDSA signe un message en s’appuyant sur les mathématiques des courbes elliptiques plutôt que sur la factorisation de grands nombres premiers, comme le fait RSA. Concrètement, une clé privée ECDSA sur la courbe P-256 ne pèse que 32 octets, contre 256 octets pour une clé RSA-2048 offrant un niveau de sécurité comparable. Cette compacité se répercute sur la taille des signatures : une signature ECDSA P-256 tient en 64 à 72 octets selon l’encodage, alors qu’une signature RSA-2048 occupe systématiquement 256 octets.

Le principe repose sur un nonce aléatoire généré à chaque signature. C’est justement ce nonce qui a été au cœur de la CVE-2026-4599, publiée le 23 mars 2026 et touchant la bibliothèque JavaScript jsrsasign jusqu’à la version 11.1.0 (corrigée en 11.1.1) : un défaut dans la génération de nombres aléatoires permettait, selon l’avis, de retrouver la clé privée à partir de plusieurs signatures. C’est précisément le genre d’erreur que ce tutoriel vous apprend à éviter en s’appuyant sur le module crypto natif, qui gère ce nonce de façon sécurisée par défaut.

Node.js a aussi eu sa propre alerte en 2026 : un avis Debian référence la CVE-2026-48933, un défaut dans l’implémentation Web Crypto de Node.js pouvant provoquer un crash du processus. L’avis ne précise pas si la primitive ECDSA est en cause, mais il rappelle une règle simple : gardez votre version de Node.js à jour et surveillez les avis de sécurité, même pour un algorithme aussi éprouvé qu’ECDSA.

Comment la signature et la vérification fonctionnent mathématiquement

Sans entrer dans le détail algébrique, le principe tient en trois étapes. Pour signer, Node.js calcule d’abord un hash SHA-256 du message, tire un nonce aléatoire, puis combine ce nonce avec la clé privée et le hash pour produire deux nombres (souvent notés r et s) qui forment la signature. Pour vérifier, le vérificateur recalcule le hash du message reçu, puis utilise la clé publique et les deux nombres r et s pour reconstituer un point sur la courbe elliptique : si ce point correspond à ce qu’implique la clé publique, la signature est valide. Le point clé à retenir pour un développeur : la clé privée ne sert jamais à la vérification, et la clé publique ne permet jamais, mathématiquement, de retrouver la clé privée ou de forger une signature sans elle.

ECDSA vs RSA vs Ed25519 : quelle courbe choisir

Trois familles de signatures dominent les systèmes modernes : RSA, ECDSA (courbes P-256, P-384, P-521 définies par le NIST dans FIPS 186-5) et Ed25519 (courbe Edwards25519, utilisée notamment par SSH et Signal). Chacune a ses compromis. RSA reste le plus compatible avec les anciens systèmes et les HSM d’entreprise. ECDSA offre un bon équilibre entre taille, vitesse et compatibilité TLS. Ed25519 va plus vite à signer et élimine la dépendance à un générateur aléatoire de qualité pour le nonce, mais sa prise en charge dans les certificats TLS publics reste nulle selon l’étude d’août 2026 citée plus haut.

AlgorithmeTaille de clé privéeTaille de signatureUsage dominant en 2026Support TLS public
RSA-2048256 octets256 octetsHéritage, HSM, compatibilité large50,3 % à 61,6 % selon l’échantillon
ECDSA P-25632 octets64 à 72 octetsJWT (ES256), TLS, Bitcoin/Ethereum38,4 % à 49,7 % selon l’échantillon
ECDSA P-38448 octets96 à 104 octetsSécurité renforcée (192 bits)Minoritaire
Ed2551932 octets64 octetsSSH, Signal, nouveaux systèmes0 % dans l’échantillon TLS public analysé

Sur la performance brute, un benchmark serveur cité dans les résultats disponibles donne environ 23 100 opérations de signature par seconde pour ECDSA P-256, contre environ 935 pour RSA-2048, soit un rapport d’environ 25 fois en faveur d’ECDSA pour signer. À l’inverse, RSA-2048 vérifie plus vite (environ 22 160 opérations par seconde contre 8 775 pour ECDSA P-256), un rapport d’environ 2,5 fois en faveur de RSA pour la vérification. Ces chiffres proviennent d’une source secondaire sans détail de matériel ni de version Node.js précise : traitez-les comme un ordre de grandeur, pas comme une mesure certifiée, et relancez vos propres benchmarks sur votre infrastructure avant de trancher.

Pour la suite du tutoriel, nous utilisons P-256 : c’est la courbe par défaut des jetons JWT en ES256 (RFC 7518, JSON Web Algorithms) et celle qui offre le meilleur compromis entre vitesse, taille et compatibilité avec les outils existants.

Un point de vigilance mérite d’être signalé dès maintenant, avant même d’écrire la première ligne de code : le champ alg d’un jeton JWT n’est jamais une information fiable à elle seule. Un jeton transporte dans son en-tête l’algorithme que l’émetteur prétend avoir utilisé, mais rien n’empêche un attaquant de modifier ce champ avant de le transmettre. C’est exactement ce mécanisme qui est exploité dans les attaques de confusion d’algorithme entre RS256 et HS256, documentées dans plusieurs avis de sécurité sur les bibliothèques JWT ces dernières années. La seule protection fiable consiste à fixer, côté serveur, la liste des algorithmes acceptés, sans jamais faire confiance à ce que le jeton affirme sur lui-même. Nous appliquons cette règle dès l’étape 7.

Prérequis : outils et versions nécessaires

Avant de commencer, installez les éléments suivants. Nous utilisons exclusivement le module crypto natif de Node.js pour la partie signature, donc aucune dépendance cryptographique tierce n’est nécessaire pour les premières étapes.

  • Node.js 24.x (LTS) : la branche active en octobre 2026, avec le module crypto basé sur OpenSSL intégré. Node.js 22.x reste en maintenance LTS si vous ne pouvez pas migrer immédiatement.
  • npm 10.x ou supérieur, installé avec Node.js.
  • Un éditeur de code avec support JavaScript (VS Code ou équivalent).
  • express 4.x pour la partie API du projet complet (installé via npm).
  • jsonwebtoken 9.x pour la génération de jetons ES256 (installé via npm).
  • node:test, le module de test intégré à Node.js depuis la version 18, utilisé pour la suite de tests (aucune installation requise).
  • OpenSSL en ligne de commande (optionnel) pour inspecter les clés générées depuis un terminal.

S’appuyer sur le module natif plutôt que sur une bibliothèque npm tierce a un avantage de sécurité concret : moins de code tiers à auditer, moins de dépendances à surveiller dans vos rapports npm audit, et une implémentation directement liée aux correctifs de sécurité publiés pour Node.js lui-même. C’est aussi ce choix qui vous met à l’abri d’un problème comme celui décrit dans la CVE-2026-4599 : cette faille touchait une bibliothèque JavaScript spécifique, pas le module crypto de Node.js.

Vérifiez votre version de Node.js avant de continuer :

node --version
# v24.x.x attendu (ou v22.x.x en LTS de maintenance)

node -e "console.log(require('node:crypto').getCurves().includes('prime256v1'))"
# true : la courbe P-256 (prime256v1) est disponible

Étape 1 : Initialiser le projet Node.js

Créez un dossier de projet et initialisez-le avec npm. Nous structurons le projet dès le départ pour qu’il accueille à la fois les scripts de démonstration et l’API complète de la fin du tutoriel.

mkdir ecdsa-nodejs-demo && cd ecdsa-nodejs-demo
npm init -y
mkdir -p src keys
npm install express jsonwebtoken
npm install --save-dev jest

Le dossier keys stockera les clés PEM générées à l’étape suivante. Ajoutez-le immédiatement à votre fichier .gitignore : une clé privée ECDSA ne doit jamais atterrir dans un dépôt Git, même privé.

Étape 2 : Générer une paire de clés ECDSA

Le module crypto natif expose generateKeyPairSync pour créer une paire de clés sur la courbe de votre choix. Créez src/generate-keys.js :

const crypto = require('node:crypto');
const fs = require('node:fs');

const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
  namedCurve: 'P-256',
});

const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' });
const publicPem = publicKey.export({ type: 'spki', format: 'pem' });

fs.writeFileSync('keys/private.pem', privatePem);
fs.writeFileSync('keys/public.pem', publicPem);

console.log('Paire de clés ECDSA P-256 générée dans ./keys');

Exécutez le script :

node src/generate-keys.js
# Paire de clés ECDSA P-256 générée dans ./keys

cat keys/public.pem
# -----BEGIN PUBLIC KEY-----
# MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
# -----END PUBLIC KEY-----

Une clé privée PKCS8 ECDSA P-256 tient en une poignée de lignes, contre près de 30 lignes pour une clé RSA-2048 équivalente. C’est cette compacité qui rend ECDSA particulièrement adapté aux environnements contraints : objets connectés, extensions de navigateur, applications mobiles.

Étape 3 : Signer un message avec createSign

Node.js propose deux API équivalentes : les fonctions synchrones crypto.sign() / crypto.verify(), et les classes Sign / Verify basées sur des flux. Pour un message qui tient en mémoire, la première approche est plus simple. Créez src/sign-message.js :

const crypto = require('node:crypto');
const fs = require('node:fs');

const privateKey = fs.readFileSync('keys/private.pem');
const message = Buffer.from('facture-2026-10-05-ref-88213');

const signature = crypto.sign('sha256', message, {
  key: privateKey,
  dsaEncoding: 'ieee-p1363',
});

console.log('Signature (hex) :', signature.toString('hex'));
console.log('Longueur :', signature.length, 'octets');

Sortie attendue :

Signature (hex) : 8f2e4a1c9b7d3e5f0a6c8b4d2e1f9a3c7b5d4e2f1a9c8b7d6e5f4a3c2b1d9e8f...
Longueur : 64 octets

Le paramètre dsaEncoding: 'ieee-p1363' est volontaire : il produit une signature de taille fixe (64 octets pour P-256, deux entiers de 32 octets concatenés), le format attendu par les JWT ES256 et par la plupart des API web modernes. Sans ce paramètre, Node.js revient par défaut à l’encodage DER, plus ancien et de taille variable (71 ou 72 octets selon la valeur des entiers).

Étape 4 : Vérifier une signature avec createVerify

La vérification suit le même principe, avec la clé publique. Ajoutez à src/sign-message.js :

const publicKey = fs.readFileSync('keys/public.pem');

const isValid = crypto.verify('sha256', message, {
  key: publicKey,
  dsaEncoding: 'ieee-p1363',
}, signature);

console.log('Signature valide :', isValid);
// Signature valide : true

Point essentiel à retenir : crypto.verify() ne lève jamais d’exception en cas de signature invalide. Elle renvoie simplement false. Testez-le vous-même en modifiant un seul caractère du message avant la vérification : vous obtiendrez false sans aucune erreur, même si la clé, l’algorithme et l’encodage sont corrects par ailleurs. C’est un comportement à anticiper dans votre gestion d’erreurs : un if (!isValid) explicite est obligatoire, on ne peut pas se contenter d’un try/catch.

Étape 5 : Exporter les clés au format JWK

Le format JWK (JSON Web Key) est utile quand vous devez publier une clé publique dans un endpoint JWKS consommé par d’autres services. Node.js l’exporte nativement :

const crypto = require('node:crypto');
const fs = require('node:fs');

const publicKey = crypto.createPublicKey(fs.readFileSync('keys/public.pem'));
const jwk = publicKey.export({ format: 'jwk' });

console.log(JSON.stringify(jwk, null, 2));
{
  "kty": "EC",
  "x": "7WtdgNoz5vR2XY-MTUNTdg2Ht3HCjZ2b3Vwz32-bkf0",
  "y": "imuPjOFqG70W6Hp6rtrROqbuJwtO_75e-1nmSGgjFHo",
  "crv": "P-256"
}

Ajoutez un champ kid (key ID) manuellement avant de publier ce JWK dans un endpoint /.well-known/jwks.json : c’est ce champ que les consommateurs utiliseront pour savoir quelle clé publique correspond à quelle signature, un point indispensable dès que vous gérez plusieurs clés (voir l’étape 10 sur la rotation).

Étape 6 : Sécuriser un webhook JSON avec ECDSA

Un cas d’usage très concret : signer le corps d’un webhook pour que le destinataire puisse vérifier qu’il provient bien de vous et qu’il n’a pas été modifié en transit. Contrairement à HMAC, qui partage un secret symétrique entre les deux parties, ECDSA permet au destinataire de vérifier la signature avec une clé publique, sans jamais connaître la clé privée de l’émetteur.

function signWebhookPayload(payload, privateKeyPem) {
  const canonical = JSON.stringify(payload);
  const signature = crypto.sign('sha256', Buffer.from(canonical), {
    key: privateKeyPem,
    dsaEncoding: 'ieee-p1363',
  });
  return {
    body: canonical,
    headers: {
      'X-Signature': signature.toString('base64url'),
      'X-Signature-Alg': 'ES256',
    },
  };
}

Côté réception, le destinataire recalcule la vérification sur le corps brut reçu, pas sur un objet ré-encodé :

function verifyWebhookPayload(rawBody, signatureHeader, publicKeyPem) {
  const signature = Buffer.from(signatureHeader, 'base64url');
  return crypto.verify('sha256', Buffer.from(rawBody), {
    key: publicKeyPem,
    dsaEncoding: 'ieee-p1363',
  }, signature);
}

C’est ce point, “vérifier le corps brut et non un objet reconstruit”, qui provoque le plus d’incidents en production : si le serveur qui reçoit le webhook re-sérialise le JSON avant de le signer à nouveau (par exemple via un framework qui parse puis reformate), l’ordre des clés ou les espaces peuvent changer, et la signature ne correspondra plus jamais.

Étape 7 : Émettre des jetons JWT en ES256

ES256 désigne, dans la RFC 7518, la combinaison ECDSA P-256 + SHA-256 utilisée pour signer des jetons JWT. La bibliothèque jsonwebtoken s’appuie directement sur le module crypto de Node.js pour cette opération :

const jwt = require('jsonwebtoken');
const fs = require('node:fs');

const privateKey = fs.readFileSync('keys/private.pem');
const publicKey = fs.readFileSync('keys/public.pem');

const token = jwt.sign(
  { sub: 'user-4471', scope: 'read:invoices' },
  privateKey,
  { algorithm: 'ES256', expiresIn: '15m', keyid: 'ecdsa-2026-10' }
);

console.log('Jeton émis :', token.slice(0, 50) + '...');

const decoded = jwt.verify(token, publicKey, { algorithms: ['ES256'] });
console.log('Contenu décodé :', decoded);

Remarquez le paramètre algorithms: ['ES256'] passé explicitement à jwt.verify(). C’est une protection contre les attaques par confusion d’algorithme : sans cette liste blanche, un attaquant qui obtient votre clé publique (information non sensible, par définition) pourrait tenter de forger un jeton HS256 signé avec cette même clé publique utilisée comme secret HMAC. En listant explicitement les algorithmes autorisés, la bibliothèque rejette toute tentative de ce type.

Étape 8 : Construire l’API Express complète

Assemblons les pièces précédentes dans une API minimale : un endpoint de connexion qui émet un jeton ES256, un middleware qui le vérifie, et un endpoint qui signe une réponse sortante. Créez src/server.js :

const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('node:crypto');
const fs = require('node:fs');

const app = express();
app.use(express.json());

const privateKey = fs.readFileSync('keys/private.pem');
const publicKey = fs.readFileSync('keys/public.pem');

app.post('/login', (req, res) => {
  const token = jwt.sign(
    { sub: req.body.userId },
    privateKey,
    { algorithm: 'ES256', expiresIn: '15m', keyid: 'ecdsa-2026-10' }
  );
  res.json({ token });
});

function requireAuth(req, res, next) {
  const header = req.headers.authorization || '';
  const token = header.replace('Bearer ', '');
  try {
    req.user = jwt.verify(token, publicKey, { algorithms: ['ES256'] });
    next();
  } catch (err) {
    res.status(401).json({ error: 'jeton invalide ou expiré' });
  }
}

app.get('/invoices/:id', requireAuth, (req, res) => {
  const invoice = { id: req.params.id, amount: 1240.5, currency: 'EUR' };
  const signature = crypto.sign('sha256', Buffer.from(JSON.stringify(invoice)), {
    key: privateKey,
    dsaEncoding: 'ieee-p1363',
  });
  res.set('X-Signature', signature.toString('base64url'));
  res.json(invoice);
});

app.listen(3000, () => console.log('API disponible sur http://localhost:3000'));

Lancez le serveur, puis testez le flux complet :

node src/server.js &

curl -s -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"userId":"user-4471"}'
# {"token":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."}

TOKEN=""
curl -s http://localhost:3000/invoices/88213 \
  -H "Authorization: Bearer $TOKEN" -i
# HTTP/1.1 200 OK
# X-Signature: MEQCIQD3k...base64url...
# {"id":"88213","amount":1240.5,"currency":"EUR"}

Étape 9 : Tester la signature avec node:test

Une suite de tests minimale protège contre les régressions silencieuses, en particulier celles liées à l’encodage. Créez src/ecdsa.test.js :

const test = require('node:test');
const assert = require('node:assert');
const crypto = require('node:crypto');

test('une signature ECDSA valide se vérifie correctement', () => {
  const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
  const message = Buffer.from('message-de-test');
  const signature = crypto.sign('sha256', message, { key: privateKey, dsaEncoding: 'ieee-p1363' });
  const valid = crypto.verify('sha256', message, { key: publicKey, dsaEncoding: 'ieee-p1363' }, signature);
  assert.strictEqual(valid, true);
});

test('un message modifié invalide la signature', () => {
  const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
  const signature = crypto.sign('sha256', Buffer.from('original'), { key: privateKey, dsaEncoding: 'ieee-p1363' });
  const valid = crypto.verify('sha256', Buffer.from('modifie'), { key: publicKey, dsaEncoding: 'ieee-p1363' }, signature);
  assert.strictEqual(valid, false);
});

test('une signature P-256 encodée en ieee-p1363 fait 64 octets', () => {
  const { privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
  const signature = crypto.sign('sha256', Buffer.from('x'), { key: privateKey, dsaEncoding: 'ieee-p1363' });
  assert.strictEqual(signature.length, 64);
});
node --test src/ecdsa.test.js
# ✔ une signature ECDSA valide se vérifie correctement
# ✔ un message modifié invalide la signature
# ✔ une signature P-256 encodée en ieee-p1363 fait 64 octets
# tests 3, pass 3, fail 0

Étape 10 : Stocker et faire tourner les clés en production

Une clé privée ECDSA ne doit jamais être écrite en clair dans votre code source ni dans une variable d’environnement partagée sans chiffrement. En production, trois approches sérieuses existent, par ordre de robustesse croissante : un gestionnaire de secrets applicatif (Vault, AWS Secrets Manager, Azure Key Vault), un HSM logiciel ou matériel qui ne laisse jamais sortir la clé en clair, ou pour les plus petites structures, un fichier chiffré déchiffré au démarrage avec une clé maître issue d’un coffre.

La rotation de clé mérite une préparation en amont : publiez toujours plusieurs clés publiques valides simultanément (l’ancienne et la nouvelle) pendant une période de transition, identifiées par leur champ kid dans le JWK. Voici un schéma simple de endpoint JWKS gérant plusieurs clés actives :

app.get('/.well-known/jwks.json', (req, res) => {
  res.json({
    keys: [
      { ...currentPublicJwk, kid: 'ecdsa-2026-10', use: 'sig', alg: 'ES256' },
      { ...previousPublicJwk, kid: 'ecdsa-2026-07', use: 'sig', alg: 'ES256' },
    ],
  });
});

Quand vous signez un nouveau jeton, incluez le kid correspondant à la clé active. Le vérificateur consulte le JWKS, choisit la clé publique correspondant au kid présent dans l’en-tête du jeton, puis vérifie. Cela permet de retirer une ancienne clé progressivement, sans invalider brutalement tous les jetons en circulation.

Combien de temps garder une clé “ancienne” active dépend de la durée de vie de vos jetons. Si vos jetons ES256 expirent après 15 minutes comme dans l’exemple de l’étape 7, conserver l’ancienne clé publique pendant une heure après la rotation suffit largement à couvrir tous les jetons déjà émis. Pour des usages avec des jetons de longue durée (API keys, jetons de rafraîchissement), la fenêtre de chevauchement doit correspondre à la durée de vie maximale du jeton le plus long, sous peine de voir des clients légitimes rejetés en plein milieu de la transition.

Les 5 pièges les plus fréquents avec ECDSA en Node.js

La plupart des bugs liés à ECDSA ne viennent pas d’une faiblesse de l’algorithme lui-même, mais d’une incohérence discrète entre ce qui a été signé et ce qui est vérifié. Comme crypto.verify() échoue silencieusement sans détailler la cause, ces erreurs se repèrent souvent tard, parfois seulement en production face à un client ou un partenaire externe. Voici les cinq pièges qui reviennent le plus souvent dans les revues de code et les tickets de support.

  • Mélanger les encodages DER et IEEE P1363. Une signature générée en DER ne se vérifiera jamais avec dsaEncoding: 'ieee-p1363', et inversement. Node.js renverra simplement false, sans message d’erreur explicite indiquant la vraie cause.
  • Vérifier un objet re-sérialisé au lieu du corps brut reçu. Sur un webhook, toujours vérifier la signature sur les octets exacts reçus sur le réseau, avant tout parsing JSON qui pourrait réordonner les clés.
  • Oublier la liste blanche d’algorithmes dans jwt.verify(). Ne jamais appeler jwt.verify(token, key) sans préciser algorithms: ['ES256'] : c’est la porte ouverte aux attaques de confusion d’algorithme décrites à l’étape 7.
  • Confondre la courbe utilisée à la génération et à la vérification. Une clé publique P-384 ne vérifiera jamais une signature produite avec une clé privée P-256, même si le message et le hash sont identiques.
  • Committer une clé privée dans Git, même temporairement. Une clé privée poussée dans un dépôt, même supprimée au commit suivant, reste dans l’historique Git et doit être considérée comme compromise : elle doit être régénérée, pas seulement retirée.

Dépannage : 8 erreurs courantes et leurs solutions

Quand une signature ECDSA refuse de se vérifier, la tentation est de soupçonner un bug profond dans le code cryptographique. Dans l’immense majorité des cas réels, la cause est beaucoup plus prosaïque : un paramètre d’encodage oublié, une clé importée dans le mauvais format, ou un cache qui n’a pas encore vu la nouvelle clé publique après une rotation. Le tableau suivant reprend les symptômes les plus fréquemment rencontrés en développement comme en production, avec la cause probable et la correction à appliquer.

SymptômeCause probableSolution
crypto.verify() renvoie toujours falseEncodage DER/IEEE P1363 incohérent entre signature et vérificationVérifiez que dsaEncoding est identique des deux côtés
Error: error:1C8000A5 lors de generateKeyPairSyncNom de courbe invalide (ex: ‘P256’ au lieu de ‘P-256’)Utilisez les noms exacts : P-256, P-384, P-521
jwt.verify lève “invalid algorithm”Le tableau algorithms ne contient pas ES256, ou le jeton a été signé avec un autre algorithmeHarmonisez l’algorithme de signature et la liste blanche de vérification
Signature valide côté script mais invalide côté APILe corps JSON a été re-sérialisé différemment (ordre des clés, espaces)Signez et vérifiez toujours sur la chaîne brute, jamais sur un objet reconstruit
Clé publique importée refusée avec “invalid key object type”Tentative d’utiliser une clé privée là où une clé publique est attendue, ou inversementVérifiez le type exporté (spki pour public, pkcs8 pour privé)
Signature trop longue ou de taille variableEncodage DER par défaut au lieu de IEEE P1363Ajoutez explicitement dsaEncoding: ‘ieee-p1363’ si une taille fixe est requise
Erreur “unsupported state or unable to authenticate data” côté HSMLe HSM attend un format de clé ou un entête différent de celui exporté par Node.jsConsultez la documentation du HSM pour le format PKCS11 ou PKCS8 attendu
Jetons ES256 rejetés après rotation de cléLe vérificateur utilise encore l’ancienne clé publique en cacheInvalidez le cache JWKS côté client ou réduisez son TTL pendant la rotation

Astuces avancées : HSM, Web Crypto et performance

Pour les environnements à haute exigence de sécurité, un module matériel (HSM) ou un service cloud équivalent garde la clé privée à l’intérieur d’une enclave : votre code applicatif envoie le message à signer et reçoit la signature en retour, sans jamais manipuler la clé privée elle-même. C’est l’approche recommandée dès que vous signez des transactions financières ou des certificats pour des tiers.

Côté navigateur, l’API Web Crypto (disponible nativement dans Chrome, Firefox et Safari) propose un équivalent asynchrone de l’API crypto de Node.js via crypto.subtle.sign() et crypto.subtle.verify(), avec le même algorithme ECDSA. Cela permet de vérifier une signature côté client sans bibliothèque tierce, utile pour valider l’intégrité d’un contenu téléchargé avant de l’exécuter.

Vérifier une signature ECDSA côté navigateur

Le module webcrypto de Node.js (accessible via require('node:crypto').webcrypto) expose la même API que le navigateur, ce qui permet de tester ce code avant de le déployer côté client :

const { subtle } = require('node:crypto').webcrypto;

async function verifyInBrowser(publicKeyJwk, signatureBuffer, message) {
  const key = await subtle.importKey(
    'jwk', publicKeyJwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']
  );
  return subtle.verify(
    { name: 'ECDSA', hash: 'SHA-256' },
    key,
    signatureBuffer,
    new TextEncoder().encode(message)
  );
}
// Signature produite par crypto.sign() avec dsaEncoding: 'ieee-p1363'
// côté Node.js : directement compatible avec subtle.verify() côté navigateur.

L’encodage par défaut de Web Crypto correspond exactement au format IEEE P1363 utilisé dans ce tutoriel (64 octets pour P-256) : une signature produite côté serveur avec crypto.sign() se vérifie donc directement côté navigateur, sans conversion intermédiaire.

Sur la performance, si votre service signe un grand volume de messages, privilégiez la réutilisation d’un objet KeyObject (celui renvoyé par crypto.createPrivateKey()) plutôt que de reconstruire la clé depuis le PEM à chaque appel : le parsing PEM a un coût non négligeable répété des milliers de fois par seconde. Chargez la clé une seule fois au démarrage du processus, comme fait dans l’API Express de l’étape 8.

Gardez aussi un œil sur les recommandations de taille de clé. Le FIPS 186-5 du NIST, qui définit le standard de signature numérique, couvre les courbes P-256, P-384 et P-521. Pour un niveau de sécurité renforcé (192 bits, recommandé pour des données devant rester protégées plusieurs décennies), basculez vers P-384 : le coût en performance est modéré, mais la marge de sécurité est significativement plus large.

Étape 11 : Le projet complet, récapitulatif

À ce stade, votre dossier ecdsa-nodejs-demo contient une paire de clés ECDSA P-256, un script de signature et de vérification de message, une fonction de signature de webhook, une API Express qui émet des jetons ES256 et signe ses réponses, ainsi qu’une suite de tests avec node:test. C’est un projet directement réutilisable comme point de départ pour sécuriser une API interne, un système de webhooks sortants, ou l’authentification d’un service.

ecdsa-nodejs-demo/
├── keys/
│   ├── private.pem   (ne jamais committer)
│   └── public.pem
├── src/
│   ├── generate-keys.js
│   ├── sign-message.js
│   ├── server.js
│   └── ecdsa.test.js
├── .gitignore         (contient keys/)
└── package.json

Étape 12 : Bonnes pratiques de sécurité à vérifier avant mise en production

Avant de déployer, repassez cette checklist : les clés privées sont stockées hors du code source et hors des logs, le fichier .gitignore exclut le dossier keys/, chaque appel à jwt.verify() précise explicitement algorithms: ['ES256'], la vérification des webhooks porte sur le corps brut reçu, un plan de rotation de clé existe avec plusieurs kid actifs en parallèle, et votre version de Node.js reçoit encore des correctifs de sécurité (évitez toute branche non-LTS en production). La fiche Cryptographic Storage Cheat Sheet de l’OWASP détaille des recommandations complémentaires sur le stockage de clés et de secrets applicatifs.

Étape 13 : Surveiller et maintenir le système en production

Une fois en production, journalisez les échecs de vérification de signature (sans jamais journaliser la clé privée ni le message complet s’il contient des données sensibles) : un pic soudain de signatures invalides peut signaler une tentative d’altération de trafic ou un bug de déploiement après une rotation de clé mal synchronisée. Mettez également en place une alerte sur l’expiration planifiée de vos certificats ou clés, et testez votre procédure de rotation en environnement de staging avant de la dérouler en production, pour vérifier que l’ancienne et la nouvelle clé coexistent bien sans interruption de service.

Consultez régulièrement les notes de version de Node.js, disponibles sur la page officielle des releases, ainsi que la documentation du module crypto : les paramètres par défaut d’encodage ou de courbes peuvent évoluer entre versions majeures.

Au terme de ce tutoriel, vous disposez d’une base solide pour signer et vérifier des données avec ECDSA en Node.js, sans dépendre d’une bibliothèque cryptographique tierce pour les opérations critiques. Retenez les trois décisions qui comptent le plus : choisir P-256 sauf exigence particulière de sécurité renforcée, toujours fixer explicitement l’encodage (ieee-p1363 pour le web moderne), et ne jamais faire confiance à un algorithme annoncé par le message lui-même sans liste blanche côté serveur. Ces trois règles couvrent la grande majorité des incidents observés sur des implémentations JWT et webhooks en 2026.

FAQ : signature ECDSA en Node.js

ECDSA est-il plus sûr que RSA ?
Les deux offrent un niveau de sécurité comparable à taille de clé équivalente (P-256 correspond approximativement à RSA-3072 en robustesse). ECDSA l’emporte sur la compacité des clés et des signatures, et sur la vitesse de signature. RSA reste légèrement plus rapide à vérifier.

Quelle courbe choisir entre P-256, P-384 et P-521 ?
P-256 convient à la grande majorité des usages web et JWT. P-384 apporte un niveau de sécurité renforcé (192 bits) pour des données sensibles à long terme. P-521 reste rare en pratique, réservé à des exigences réglementaires spécifiques.

Faut-il utiliser dsaEncoding: ‘der’ ou ‘ieee-p1363’ ?
Utilisez ‘ieee-p1363’ pour les JWT, les webhooks et toute intégration avec des systèmes web modernes : la taille de signature est fixe et prévisible. L’encodage DER reste nécessaire pour l’interopérabilité avec certains systèmes PKI plus anciens.

Peut-on utiliser ECDSA sans bibliothèque externe en Node.js ?
Oui, entièrement. Le module crypto natif couvre la génération de clés, la signature, la vérification et l’export aux formats PEM et JWK, sans aucune dépendance npm. Seule l’émission de jetons JWT structurés bénéficie d’une bibliothèque comme jsonwebtoken pour gérer l’en-tête, les claims et l’expiration.

Comment savoir si ma signature a été forgée par un nonce prévisible ?
Si vous générez vos signatures exclusivement via le module crypto natif de Node.js, le nonce est produit par la génération aléatoire sécurisée du système sous-jacent. Le risque identifié par la CVE-2026-4599 concernait spécifiquement la bibliothèque jsrsasign jusqu’à sa version 11.1.0 : vérifiez vos dépendances npm avec npm audit et mettez à jour vers 11.1.1 ou supérieur si vous l’utilisez.

ECDSA fonctionne-t-il dans le navigateur ?
Oui, via l’API Web Crypto et crypto.subtle, disponible dans tous les navigateurs modernes. Les signatures produites côté serveur avec Node.js et vérifiées côté navigateur sont interopérables tant que l’encodage (IEEE P1363 ou DER) et la courbe correspondent des deux côtés.

Comment migrer d’un système RSA existant vers ECDSA ?
Publiez d’abord les deux jeux de clés publiques en parallèle (RSA et ECDSA) dans votre endpoint JWKS, en identifiant chacun par un champ kid et alg distinct. Faites émettre les nouveaux jetons en ES256 progressivement, en laissant les anciens jetons RS256 encore valides s’expirer naturellement, avant de retirer le support RSA côté vérification.

Le format IEEE P1363 est-il compatible avec tous les langages ?
La plupart des bibliothèques cryptographiques modernes (OpenSSL, bibliothèques Python, Go, Java) savent produire et consommer ce format, parfois sous le nom “raw” ou “fixed-length”. Vérifiez systématiquement la documentation de la bibliothèque côté destinataire si celui-ci n’est pas écrit en Node.js, car certains SDK plus anciens n’acceptent encore que le DER.