La signature de messages avec HMAC-SHA256 protège vos API, vos webhooks et vos tokens contre la falsification. Contrairement à un simple hachage SHA-256, HMAC combine une clé secrète au message, ce qui bloque les attaques par extension de longueur et garantit qu’aucun tiers ne peut forger une requête valide. Ce tutoriel construit, étape par étape, un projet Node.js complet : génération de clé, fonction de signature, vérification en temps constant, middleware Express et vérification de webhook de type Stripe.

Tout repose sur le module natif node:crypto. Aucune dépendance cryptographique externe n’est nécessaire. À la fin, vous disposerez d’une API qui signe ses réponses, vérifie l’authenticité des requêtes entrantes et rejette les rejeux (replay attacks). Comptez 30 minutes pour suivre les 12 étapes.

HMAC en bref : signature, intégrité et authentification

HMAC signifie Hash-based Message Authentication Code. Le mécanisme est défini par la RFC 2104 et normalisé par le FIPS 198-1 du NIST. Il prend deux entrées, une clé secrète et un message, puis produit une empreinte de taille fixe. Avec SHA-256, cette empreinte fait 256 bits, soit 64 caractères en hexadécimal.

Le point clé : seul celui qui détient la clé secrète peut produire une signature valide. Le destinataire recalcule la signature de son côté et compare. Si les deux correspondent, le message est authentique et n’a pas été modifié. C’est précisément ce que font Stripe, GitHub et Slack pour signer leurs webhooks.

Pourquoi pas un simple SHA-256 ?

Un hachage brut SHA256(message) n’utilise aucune clé. N’importe qui peut le recalculer, donc il n’authentifie rien. Pire, les fonctions de la famille Merkle-Damgård (SHA-1, SHA-256, SHA-512) sont vulnérables aux attaques par extension de longueur. Si vous signez naïvement avec SHA256(secret + message), un attaquant qui connaît la longueur du secret peut ajouter des données au message et calculer une signature valide sans jamais connaître le secret. HMAC élimine ce risque par sa construction interne à double hachage, avec des remplissages interne et externe distincts.

HMAC face aux autres mécanismes

MécanismeCléUsage principalVitesseRésistance length-extension
SHA-256 brutNonIntégrité simpleTrès rapideNon
HMAC-SHA256SymétriqueAuthentification de message, API, webhooksRapideOui
bcrypt / Argon2Non (sel interne)Stockage de mots de passeLent (volontaire)Sans objet
RSA / ECDSAAsymétriqueSignature publique vérifiableLentSans objet
JWT HS256Symétrique (HMAC)Tokens de sessionRapideOui

HMAC est symétrique : l’émetteur et le récepteur partagent la même clé. Si vous avez besoin qu’un tiers vérifie une signature sans pouvoir en créer, utilisez une signature asymétrique (RSA ou ECDSA). Pour le stockage de mots de passe, HMAC n’est pas adapté : préférez bcrypt ou Argon2, conçus pour être lents à dessein.

Prérequis : versions et outils

Le projet n’utilise que des modules natifs, plus Express pour la démonstration d’API. Voici l’environnement exact testé pour ce tutoriel.

ComposantVersionRôle
Node.js22 LTS (« Jod »)Runtime, fournit node:crypto
npm10.x (livré avec Node 22)Gestion des paquets
Express5.xServeur HTTP de démonstration
node:cryptoNatifHMAC, génération de clé, comparaison sûre
node:testNatifTests unitaires intégrés

Vérifiez votre version de Node avant de commencer. HMAC fonctionne depuis longtemps dans Node, mais node:test et Express 5 supposent une version récente.

$ node --version
v22.14.0
$ npm --version
10.9.2

Si node --version renvoie une version inférieure à 20, installez Node 22 LTS depuis nodejs.org ou via un gestionnaire de versions comme nvm. Une connaissance de base de JavaScript et de la ligne de commande suffit.

Étape 1 : initialiser le projet Node.js

Créez un dossier et initialisez un projet en modules ES. Le drapeau "type": "module" active la syntaxe import moderne, recommandée en 2026.

$ mkdir hmac-api && cd hmac-api
$ npm init -y
$ npm pkg set type=module
$ npm install express@5

Votre package.json doit maintenant contenir "type": "module". Express est la seule dépendance ; toute la cryptographie vient du module natif node:crypto, sans paquet tiers à auditer ou à maintenir.

Étape 2 : générer une clé secrète HMAC robuste

La sécurité d’HMAC repose entièrement sur la qualité de la clé. Le guide des mécanismes cryptographiques de l’ANSSI recommande au minimum 128 bits pour une clé symétrique ; 256 bits constituent un choix prudent et standard. N’utilisez jamais une chaîne choisie à la main comme "motdepasse123".

Créez un fichier keygen.js :

// keygen.js
import { randomBytes } from 'node:crypto';

// 32 octets = 256 bits, encodés en hexadécimal (64 caractères)
const secret = randomBytes(32).toString('hex');
console.log('HMAC_SECRET=' + secret);

Exécutez-le et copiez la clé générée :

$ node keygen.js
HMAC_SECRET=9f8c2b1e4a7d6053c8f1b29e7a4d3c6b0e5f8a1d2c4b6e9f0a3d5c7b8e1f4a2d

randomBytes est un générateur cryptographiquement sûr (CSPRNG). N’utilisez jamais Math.random(), qui est prévisible. Stockez la clé dans une variable d’environnement, jamais dans le code source.

Étape 3 : charger la clé depuis l’environnement

Créez un fichier .env avec votre clé, et ajoutez-le immédiatement à .gitignore pour ne jamais le committer.

# .env
HMAC_SECRET=9f8c2b1e4a7d6053c8f1b29e7a4d3c6b0e5f8a1d2c4b6e9f0a3d5c7b8e1f4a2d

Node 22 lit nativement les fichiers .env via le drapeau --env-file, sans le paquet dotenv. Créez config.js qui valide la présence de la clé au démarrage :

// config.js
const secret = process.env.HMAC_SECRET;

if (!secret || secret.length < 32) {
  throw new Error('HMAC_SECRET manquant ou trop court (minimum 32 caractères).');
}

export const HMAC_SECRET = secret;

Charger la clé une seule fois au démarrage, et non à chaque requête, évite des lectures inutiles et fait échouer l’application tôt si la configuration est absente. C’est une pratique recommandée par l’OWASP Cryptographic Storage Cheat Sheet.

Étape 4 : écrire la fonction de signature HMAC-SHA256

Voici le cœur du projet. Créez hmac.js avec une fonction de signature simple et réutilisable.

// hmac.js
import { createHmac } from 'node:crypto';
import { HMAC_SECRET } from './config.js';

export function sign(message) {
  return createHmac('sha256', HMAC_SECRET)
    .update(message, 'utf8')
    .digest('hex');
}

Testez immédiatement dans un fichier demo.js :

// demo.js
import { sign } from './hmac.js';

const message = 'commande:1042;montant:49.90';
console.log('Signature :', sign(message));
$ node --env-file=.env demo.js
Signature : 7b1d4f8e2a9c0b3d6e5f8a1c4d7b0e3f6a9c2d5e8b1f4a7c0d3e6b9f2a5c8d1e

La même clé et le même message produisent toujours la même empreinte de 64 caractères hexadécimaux. Changez un seul caractère du message et toute la signature change radicalement, c’est l’effet d’avalanche. Cette propriété est la base de la détection de modification.

Étape 5 : vérifier une signature en temps constant

L’erreur la plus répandue consiste à comparer deux signatures avec ===. Cette comparaison s’arrête au premier caractère différent, ce qui ouvre la porte à une attaque par chronométrage (timing attack) : un attaquant mesure le temps de réponse pour deviner la signature octet par octet. Utilisez crypto.timingSafeEqual, qui compare en temps constant.

// hmac.js (ajout)
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(message, receivedSignature) {
  const expected = sign(message);
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(receivedSignature, 'hex');

  // timingSafeEqual exige des buffers de même longueur
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

La vérification de longueur avant timingSafeEqual est nécessaire, car la fonction lève une exception si les deux buffers diffèrent en taille. Comme la longueur d’une empreinte HMAC-SHA256 est toujours de 32 octets, cette fuite de longueur n’a aucune valeur exploitable pour un attaquant.

// vérification en pratique
import { sign, verify } from './hmac.js';

const msg = 'commande:1042;montant:49.90';
const sig = sign(msg);

console.log(verify(msg, sig));                 // true
console.log(verify('message falsifié', sig));  // false

Étape 6 : ajouter un horodatage contre les rejeux

Une signature valide reste valide indéfiniment. Un attaquant qui intercepte une requête signée peut la rejouer plus tard. La parade : inclure un horodatage dans le message signé et rejeter les requêtes trop anciennes.

// hmac.js (ajout)
const TOLERANCE_MS = 5 * 60 * 1000; // 5 minutes

export function signWithTimestamp(body) {
  const timestamp = Date.now().toString();
  const signature = sign(timestamp + '.' + body);
  return { timestamp, signature };
}

export function verifyWithTimestamp(body, timestamp, signature) {
  const age = Math.abs(Date.now() - Number(timestamp));
  if (Number.isNaN(age) || age > TOLERANCE_MS) return false;
  return verify(timestamp + '.' + body, signature);
}

En liant l’horodatage à la signature, vous empêchez aussi un attaquant de modifier l’heure sans invalider la signature. Une fenêtre de 5 minutes est le compromis retenu par Stripe : elle absorbe les décalages d’horloge entre serveurs tout en limitant la fenêtre de rejeu.

Étape 7 : créer un middleware Express de vérification

Passons à une API réelle. Le middleware lit la signature et l’horodatage dans les en-têtes, puis valide le corps brut de la requête. Point crucial : la signature doit porter sur le corps brut, avant tout parsing JSON, car la sérialisation peut réordonner les clés.

// server.js
import express from 'express';
import { verifyWithTimestamp } from './hmac.js';

const app = express();

// Capture le corps brut pour la vérification HMAC
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8'); }
}));

function hmacAuth(req, res, next) {
  const signature = req.get('x-signature');
  const timestamp = req.get('x-timestamp');

  if (!signature || !timestamp) {
    return res.status(401).json({ error: 'En-têtes de signature manquants.' });
  }
  if (!verifyWithTimestamp(req.rawBody, timestamp, signature)) {
    return res.status(401).json({ error: 'Signature invalide ou expirée.' });
  }
  next();
}

app.post('/api/commande', hmacAuth, (req, res) => {
  res.json({ statut: 'reçu', commande: req.body });
});

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

Cette approche reprend le principe utilisé pour l’authentification JWT en Node.js, mais appliquée à des requêtes machine à machine plutôt qu’à des sessions utilisateur.

Étape 8 : écrire un client qui signe ses requêtes

Le client calcule la signature avec la clé partagée et l’envoie dans les en-têtes. Voici un client utilisant fetch, natif dans Node 22.

// client.js
import { signWithTimestamp } from './hmac.js';

const body = JSON.stringify({ produit: 'clé U2F', quantite: 2 });
const { timestamp, signature } = signWithTimestamp(body);

const res = await fetch('http://localhost:3000/api/commande', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'x-timestamp': timestamp,
    'x-signature': signature
  },
  body
});

console.log(res.status, await res.json());

Démarrez le serveur dans un terminal, puis lancez le client dans un autre :

$ node --env-file=.env server.js
API HMAC sur http://localhost:3000

# autre terminal
$ node --env-file=.env client.js
200 { statut: 'reçu', commande: { produit: 'clé U2F', quantite: 2 } }

Modifiez un seul octet du corps côté client après la signature, et le serveur renverra 401 Signature invalide. C’est exactement le comportement attendu.

Étape 9 : vérifier un webhook de type Stripe

Les fournisseurs SaaS signent leurs webhooks avec HMAC-SHA256. Le format d’en-tête de Stripe ressemble à t=1718000000,v1=<signature>. Voici comment le vérifier correctement.

// webhook.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyStripeSignature(rawBody, header, endpointSecret) {
  const parts = Object.fromEntries(
    header.split(',').map(p => p.split('='))
  );
  const timestamp = parts.t;
  const received = parts.v1;
  if (!timestamp || !received) return false;

  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = createHmac('sha256', endpointSecret)
    .update(signedPayload, 'utf8')
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(received, 'hex');
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Notez que Stripe signe la concaténation timestamp.payload, exactement le schéma anti-rejeu de l’étape 6. Réutiliser ce motif maison vous prépare à intégrer n’importe quel webhook professionnel sans surprise.

Étape 10 : écrire des tests unitaires avec node:test

Node 22 intègre un lanceur de tests natif. Aucune dépendance comme Jest ou Mocha n’est requise. Créez hmac.test.js.

// hmac.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { sign, verify } from './hmac.js';

test('une signature valide est acceptée', () => {
  const msg = 'données importantes';
  assert.equal(verify(msg, sign(msg)), true);
});

test('un message modifié est rejeté', () => {
  const sig = sign('original');
  assert.equal(verify('falsifié', sig), false);
});

test('une signature tronquée est rejetée sans erreur', () => {
  assert.equal(verify('original', 'abcd'), false);
});
$ node --env-file=.env --test
✔ une signature valide est acceptée
✔ un message modifié est rejeté
✔ une signature tronquée est rejetée sans erreur
ℹ tests 3
ℹ pass 3
ℹ fail 0

Étape 11 : encoder en base64url pour des en-têtes compacts

L’hexadécimal occupe deux caractères par octet. Pour des en-têtes plus compacts, encodez en base64url, le format utilisé par les JWT. Node le gère nativement.

// hmac.js (variante base64url)
export function signBase64url(message) {
  return createHmac('sha256', HMAC_SECRET)
    .update(message, 'utf8')
    .digest('base64url');
}

Une empreinte HMAC-SHA256 fait 64 caractères en hexadécimal, mais seulement 43 en base64url. Ce format évite les caractères +, / et = qui posent problème dans les URL et les en-têtes. Choisissez un format et restez cohérent entre client et serveur.

Étape 12 : faire tourner les clés sans coupure

Une clé compromise doit pouvoir être remplacée sans interrompre le service. La technique standard consiste à accepter plusieurs clés en vérification tout en signant avec une seule.

// rotation.js
import { createHmac, timingSafeEqual } from 'node:crypto';

// La première clé sert à signer ; toutes servent à vérifier
const KEYS = process.env.HMAC_KEYS.split(',');

export function signRotating(message) {
  return createHmac('sha256', KEYS[0]).update(message).digest('hex');
}

export function verifyRotating(message, received) {
  const b = Buffer.from(received, 'hex');
  return KEYS.some(key => {
    const a = Buffer.from(
      createHmac('sha256', key).update(message).digest('hex'), 'hex'
    );
    return a.length === b.length && timingSafeEqual(a, b);
  });
}

Pour une rotation : ajoutez la nouvelle clé en tête de HMAC_KEYS, déployez, laissez les anciens clients migrer, puis retirez l’ancienne clé après la période de transition. L’ANSSI recommande une rotation périodique des clés symétriques, et un remplacement immédiat en cas de suspicion de compromission.

Cas d’usage concrets d’HMAC en 2026

Au-delà de la théorie, HMAC-SHA256 reste partout dans l’infrastructure web moderne. Le reconnaître vous évite de réinventer une roue déjà éprouvée.

  • Vérification de webhooks. Stripe, GitHub, Slack, Shopify et la plupart des plateformes signent leurs notifications sortantes avec HMAC pour prouver l’origine de chaque appel.
  • Signature de requêtes d’API. Les API d’AWS (Signature Version 4) reposent sur des dérivations HMAC en chaîne pour authentifier chaque requête sans transmettre la clé secrète sur le réseau.
  • Tokens de session JWT. L’algorithme HS256, le plus courant pour les JWT, n’est rien d’autre qu’HMAC-SHA256 appliqué à l’en-tête et à la charge utile du token.
  • Liens signés à durée limitée. Les URL de téléchargement temporaires (objets de stockage cloud, réinitialisation de mot de passe) intègrent souvent une signature HMAC avec date d’expiration.
  • Intégrité des cookies. Les cookies signés empêchent un client de modifier leur contenu sans invalider la signature, sans pour autant chiffrer la valeur.

Dans chacun de ces cas, le motif est identique à celui construit dans ce tutoriel : une clé secrète partagée, une chaîne signée bien définie, et une vérification en temps constant. Maîtriser le mécanisme une fois vous ouvre l’ensemble de ces intégrations.

5 pièges courants à éviter avec HMAC

Ces erreurs reviennent constamment dans les revues de code. Chacune compromet la sécurité de l’authentification.

  • Comparer avec === au lieu de timingSafeEqual. La comparaison de chaînes fuit le temps et expose à une attaque par chronométrage. Comparez toujours des buffers en temps constant.
  • Signer le corps parsé plutôt que le corps brut. Un JSON.parse suivi d’un JSON.stringify peut réordonner les clés ou modifier l’espacement. La signature ne correspond alors plus. Signez toujours les octets bruts reçus.
  • Stocker la clé dans le code source. Une clé committée dans Git reste exposée à vie via l’historique, même après suppression. Utilisez des variables d’environnement ou un gestionnaire de secrets.
  • Omettre l’horodatage. Sans fenêtre temporelle, une requête signée interceptée peut être rejouée indéfiniment. Liez toujours un timestamp à la signature.
  • Réutiliser la clé HMAC pour le chiffrement. Une clé d’authentification et une clé de chiffrement doivent rester distinctes. Mélanger les usages affaiblit les deux.

Dépannage : 8 problèmes fréquents et leurs solutions

SymptômeCause probableSolution
Signature invalide alors que tout semble correctLe serveur signe le corps parsé, pas le brutCapturer req.rawBody dans express.json({ verify })
Input buffers must have the same byte lengthtimingSafeEqual reçoit des buffers de tailles différentesComparer les longueurs avant l’appel et renvoyer false
La signature change à chaque exécutionUn horodatage ou un nonce est inclus dans le messageComportement normal ; comparez via verify, pas par égalité brute
HMAC_SECRET manquant au démarrageOubli du drapeau --env-file=.envLancer node --env-file=.env server.js
Signatures différentes entre client et serveurEncodages distincts (hex contre base64url)Aligner le format de digest() des deux côtés
Webhook Stripe toujours rejetéUsage de la clé API au lieu du endpoint secretUtiliser le secret du webhook (préfixe whsec_)
401 sur des requêtes anciennesDécalage d’horloge supérieur à la toléranceSynchroniser via NTP ou élargir la fenêtre
Caractères + ou / cassés dans l’URLEmpreinte en base64 standard placée dans une URLPasser à digest('base64url')

Déboguer une signature qui ne correspond pas

Quand une vérification échoue, journalisez la chaîne exacte signée des deux côtés, jamais la clé. Neuf fois sur dix, le problème vient d’une différence d’un seul octet : un espace en trop, un saut de ligne, ou un ordre de clés JSON différent. Comparez les deux signedPayload caractère par caractère pour isoler la divergence.

Astuces avancées pour la production

Une fois les bases en place, ces optimisations renforcent la robustesse en production.

  • Dériver des clés par contexte avec HKDF. Plutôt qu’une clé unique partout, utilisez crypto.hkdfSync pour dériver des sous-clés distinctes par usage (webhooks, API interne, cookies) à partir d’un secret maître. Une fuite reste ainsi cloisonnée.
  • Limiter le débit sur les échecs de signature. Couplez la vérification HMAC à une limitation de débit par IP. Un pic d’échecs signale une tentative de forçage et mérite une alerte.
  • Préférer SHA-256 à SHA-512 sur le web. Pour les messages courts typiques d’une API, SHA-256 suffit largement et reste un peu plus rapide que SHA-512 sur la plupart des architectures 64 bits récentes. SHA-3 demeure plus lent en logiciel pur.
  • Hacher les gros fichiers par flux. Pour signer un fichier volumineux, alimentez l’objet HMAC par morceaux avec un stream plutôt que de charger tout le contenu en mémoire.
  • Documenter le schéma de signature. Précisez noir sur blanc quels champs sont signés, dans quel ordre, et avec quel séparateur. Les divergences de format sont la première cause de bugs d’intégration.

HMAC et l’horizon post-quantique

Bonne nouvelle pour la pérennité : HMAC-SHA256 résiste bien aux ordinateurs quantiques. L’algorithme de Grover ne réduit la sécurité que de moitié en théorie, laissant environ 128 bits de marge, ce qui reste hors de portée. Contrairement à RSA ou ECDSA, HMAC n’a pas besoin d’être remplacé dans la transition vers la cryptographie post-quantique. Vos signatures symétriques restent valides à long terme, comme votre certificat SSL protège déjà le transport.

Comprendre la construction interne d’HMAC

Comprendre ce qui se passe sous le capot aide à éviter les mauvaises implémentations maison. HMAC ne se contente pas de hacher la concaténation de la clé et du message. La RFC 2104 définit une construction à deux passes qui neutralise les faiblesses des fonctions de hachage Merkle-Damgård.

Le mécanisme dérive deux blocs de remplissage à partir de la clé : un remplissage interne ipad (l’octet 0x36 répété) et un remplissage externe opad (l’octet 0x5c répété). La clé est d’abord ajustée à la taille de bloc de la fonction de hachage (64 octets pour SHA-256), par hachage si elle est trop longue, par remplissage de zéros si elle est trop courte. La formule complète s’écrit ainsi :

HMAC(K, m) = H( (K' ⊕ opad) || H( (K' ⊕ ipad) || m ) )

K'   : clé ajustée à la taille de bloc
H    : fonction de hachage (ici SHA-256)
⊕    : OU exclusif (XOR)
||   : concaténation

Le double hachage est ce qui rend HMAC immun aux attaques par extension de longueur. Comme la passe externe re-hache le résultat de la passe interne avec un remplissage différent, un attaquant ne peut pas prolonger le message en s’appuyant sur l’état interne de la fonction. C’est précisément la raison pour laquelle vous ne devez jamais réimplémenter HMAC à la main : la moindre erreur sur le remplissage ou l’ajustement de clé casse cette garantie. Le module node:crypto, lui, s’appuie sur OpenSSL, dont l’implémentation est auditée et éprouvée depuis des années.

Cette construction explique aussi pourquoi la longueur de clé recommandée correspond à la taille de sortie du hachage. Une clé plus longue que la taille de bloc est d’abord hachée, ce qui la ramène à la taille de sortie sans gain de sécurité. Une clé de 32 octets pour SHA-256 atteint donc le maximum d’entropie utile sans gaspillage.

Mesurer les performances d’HMAC en Node.js

HMAC-SHA256 est rapide, mais il vaut mieux mesurer que supposer. Voici un micro-benchmark qui compte le nombre de signatures par seconde sur votre machine. Mesurez toujours sur le matériel de production réel, car les chiffres varient fortement selon le processeur.

// bench.js
import { sign } from './hmac.js';

const message = 'commande:1042;montant:49.90';
const iterations = 1_000_000;

const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) sign(message + i);
const end = process.hrtime.bigint();

const ms = Number(end - start) / 1e6;
console.log(`${iterations} signatures en ${ms.toFixed(0)} ms`);
console.log(`${Math.round(iterations / (ms / 1000))} signatures/s`);

Sur une machine de développement récente, ce benchmark traite couramment plusieurs centaines de milliers de signatures par seconde. La latence par opération se compte en microsecondes, négligeable face au temps d’un aller-retour réseau. Le tableau ci-dessous compare l’ordre de grandeur des principaux mécanismes pour signer un message court.

MécanismeCoût relatif par opérationUsage typiqueBon choix pour une API ?
HMAC-SHA256Très faibleSignature de requêtes, webhooksOui
HMAC-SHA512FaibleMessages très longsOui, marginalement plus lent
RSA-2048 (signature)ÉlevéSignature vérifiable publiquementSeulement si l’asymétrie est requise
ECDSA P-256MoyenSignature asymétrique compacteSeulement si l’asymétrie est requise
bcrypt (coût 12)Très élevé (volontaire)Stockage de mots de passeNon, jamais pour signer des requêtes

Pour la grande majorité des API, la signature HMAC n’est jamais le goulot d’étranglement : la sérialisation JSON, l’accès à la base de données et la latence réseau dominent largement. N’optimisez donc pas prématurément le choix de l’algorithme de hachage. SHA-256 offre le meilleur compromis entre sécurité, compatibilité universelle et vitesse, ce qui en fait le défaut raisonnable en 2026. Réservez SHA-512 aux cas où vous signez régulièrement des charges utiles de plusieurs mégaoctets, où son débit par octet peut devenir un léger avantage sur les processeurs 64 bits.

Le projet complet : structure des fichiers

Voici l’arborescence finale du projet fonctionnel construit au fil des étapes.

hmac-api/
├── .env              # clé secrète (jamais committée)
├── .gitignore        # contient .env et node_modules
├── package.json      # "type": "module"
├── config.js         # chargement et validation de la clé
├── keygen.js         # génération de clé
├── hmac.js           # sign, verify, timestamp
├── webhook.js        # vérification type Stripe
├── server.js         # API Express protégée par HMAC
├── client.js         # client qui signe ses requêtes
├── rotation.js       # rotation multi-clés
└── hmac.test.js      # tests unitaires natifs

Chaque fichier reste sous 30 lignes. Toute la logique cryptographique tient dans hmac.js, sans aucune dépendance externe au-delà d’Express pour la démonstration. C’est la force de node:crypto : un code minimal, auditable et performant.

Foire aux questions sur HMAC en Node.js

Quelle est la différence entre HMAC et un hachage SHA-256 simple ?

SHA-256 seul ne prend aucune clé : n’importe qui peut recalculer l’empreinte, donc il n’authentifie rien. HMAC combine une clé secrète au message, ce qui garantit que seul le détenteur de la clé peut produire une signature valide. HMAC bloque aussi les attaques par extension de longueur, contre lesquelles le SHA-256 brut concaténé à une clé reste vulnérable.

Faut-il une bibliothèque externe pour HMAC en Node.js ?

Non. Le module natif node:crypto fournit createHmac et timingSafeEqual, tout ce qui est nécessaire. Évitez les paquets comme crypto-js côté serveur : ils sont plus lents et ajoutent une surface d’attaque inutile alors que la fonctionnalité existe nativement.

Quelle longueur de clé HMAC choisir ?

Au minimum 128 bits selon l’ANSSI, et 256 bits (32 octets) comme choix prudent standard. Générez la clé avec crypto.randomBytes(32) et jamais à la main. Une clé idéale a une longueur égale à celle de la sortie de la fonction de hachage, soit 32 octets pour SHA-256.

HMAC peut-il servir pour les mots de passe ?

Non, ce n’est pas son rôle. HMAC est rapide, ce qui devient un défaut pour le stockage de mots de passe, où la lenteur protège contre le forçage. Utilisez bcrypt ou Argon2, conçus spécifiquement pour ralentir les attaques par dictionnaire.

Pourquoi mes signatures ne correspondent-elles pas entre client et serveur ?

La cause la plus fréquente est une différence dans la chaîne signée : le serveur signe souvent le corps reparsé alors que le client signe la chaîne d’origine. Signez toujours le corps brut identique des deux côtés, et vérifiez que l’encodage de sortie (hex ou base64url) est le même.

HMAC est-il vulnérable aux ordinateurs quantiques ?

Pas en pratique. L’algorithme de Grover divise par deux la sécurité théorique, laissant environ 128 bits effectifs pour HMAC-SHA256, ce qui reste hors d’atteinte. Aucune migration n’est nécessaire, contrairement aux signatures asymétriques RSA et ECDSA menacées par l’algorithme de Shor.

Comment vérifier un webhook GitHub ou Stripe ?

Récupérez le corps brut de la requête, calculez HMAC-SHA256 avec le secret du webhook (pas la clé API), et comparez avec la signature de l’en-tête via timingSafeEqual. Stripe signe timestamp.payload, GitHub signe directement le corps avec l’en-tête X-Hub-Signature-256. Le principe reste identique.

Faut-il chiffrer le message en plus de le signer ?

HMAC garantit l’intégrité et l’authenticité, mais pas la confidentialité : le message reste lisible. Si le contenu est sensible, combinez signature et chiffrement, ou appuyez-vous sur TLS pour le transport et un chiffrement authentifié comme AES-GCM pour les données au repos.

HMAC-SHA256 reste l’un des outils les plus simples et les plus robustes de la boîte à outils cryptographique. Avec une bonne clé, une comparaison en temps constant et un horodatage anti-rejeu, vous disposez d’une authentification de message solide, performante et durable, le tout sans une seule dépendance externe.