Un paquet npm modifié, un firmware trafiqué, un fichier de sauvegarde corrompu en silence : dans chacun de ces cas, la seule chose qui aurait pu tirer la sonnette d’alarme, c’est une empreinte cryptographique. En 2026, la vérification d’intégrité par hachage reste l’un des contrôles les plus simples à mettre en place et les plus souvent négligés. Ce tutoriel vous montre comment construire, en Node.js, un outil complet de vérification d’intégrité de fichiers avec SHA-256 et SHA-3, du script en ligne de commande jusqu’à l’intégration dans une chaîne CI/CD.

Vous repartirez avec un projet fonctionnel : génération de manifestes de checksums, vérification en masse, détection de falsification, et export dans plusieurs formats. Le tout en environ 5000 mots, 12 étapes numérotées, et suffisamment de pièges documentés pour éviter les erreurs classiques.

Pourquoi le hachage d’intégrité redevient un sujet brûlant en 2026

La supply chain logicielle est redevenue la cible numéro un des attaquants. Selon une analyse 2026 de ReversingLabs, npm a dépassé 10 819 paquets open source malveillants détectés en 2025, soit une hausse de plus de 100 % par rapport à l’année précédente, et cela représente près de 90 % des détections toutes plateformes open source confondues. Le cabinet Sonatype, de son côté, évalue à plus de 1,233 million le nombre cumulé de paquets malveillants recensés dans son registre de sécurité open source en 2026.

Deux campagnes illustrent bien le problème. La première, surnommée Miasma, a touché des paquets npm de la famille @redhat-cloud-services : Wiz a documenté au moins 32 versions publiées contenant des modifications non autorisées, ne correspondant pas au code source déclaré, avec un payload chiffré généré différemment à chaque infection, ce qui réduit fortement l’utilité d’une simple liste noire de hachages connus. La seconde, baptisée Mini Shai-Hulud, a fait l’objet d’une alerte du NHS britannique le 12 mai 2026, après la publication de centaines de versions malveillantes touchant des projets npm et PyPI.

Un troisième exemple est plus parlant encore pour ce tutoriel : la faille CVE-2026-42575 dans l’outil de construction d’images apko. Le problème ne venait pas d’un algorithme de hachage cassé, mais d’une vérification incomplète : apko validait bien la signature du fichier d’index APKINDEX.tar.gz, mais ne comparait pas correctement les checksums individuels des paquets .apk téléchargés avec ceux déclarés dans cet index signé. Résultat, un paquet modifié pouvait être accepté silencieusement. Les versions antérieures à 1.2.7 sont concernées, la correction consiste à migrer vers 1.2.7 ou une version plus récente. C’est exactement le type d’erreur qu’un bon script de vérification d’intégrité doit empêcher, et c’est ce que nous allons construire pas à pas.

Pour les équipes françaises et européennes, ce type de contrôle dépasse le simple confort technique. La directive NIS2, transposée progressivement dans les États membres, impose aux entités concernées de démontrer des mesures de gestion des risques couvrant explicitement la sécurité de la chaîne d’approvisionnement logicielle. L’ANSSI recommande de longue date la vérification de l’intégrité des artefacts logiciels avant déploiement comme mesure d’hygiène de base. Un script de vérification d’intégrité documenté, versionné et intégré à une pipeline CI/CD constitue une preuve concrète et facile à présenter lors d’un audit, bien plus convaincante qu’une simple déclaration d’intention sur un document de politique de sécurité.

SHA-256 et SHA-3 : quelle différence pour votre code

SHA-256 fait partie de la famille SHA-2, publiée par le NIST en 2001. SHA-3 (dont la variante SHA3-256 nous intéresse ici) repose sur une construction totalement différente, l’éponge Keccak, standardisée dans le FIPS 202 en 2015. Les deux algorithmes sont aujourd’hui jugés sûrs par le NIST : ils offrent une résistance à la collision de 128 bits et une résistance à la préimage de 256 bits pour une sortie de 256 bits. Le NIST ne recommande pas de migration générale de SHA-256 vers SHA3-256 pour les systèmes existants ; sa position actuelle est celle d’une coexistence, les deux familles étant acceptables pour les applications de hachage sécurisées, avec SHA-256 conseillé au minimum pour des raisons d’interopérabilité.

La différence pratique se joue surtout sur la vitesse. Dans des comparaisons publiées en 2026, un écart d’environ 3,5 fois en faveur de SHA-256 a été mesuré sur des charges de travail classiques, principalement parce que SHA-256 bénéficie d’un support matériel plus large (extensions SHA sur les processeurs modernes) et d’implémentations plus optimisées dans OpenSSL. SHA3-256 reste solide cryptographiquement, mais moins rapide en pratique quand l’implémentation ne profite pas d’accélérations spécifiques. Pour un outil de vérification d’intégrité de fichiers volumineux (images disque, archives, binaires), ce facteur compte : c’est pourquoi notre projet proposera les deux algorithmes au choix, avec SHA-256 par défaut.

CritèreSHA-256SHA3-256
FamilleSHA-2 (Merkle-Damgård)SHA-3 (éponge Keccak)
StandardFIPS 180-4FIPS 202
Taille de sortie256 bits256 bits
Résistance collision128 bits128 bits
Résistance préimage256 bits256 bits
Vitesse relativeRéférence (plus rapide)Environ 3,5x plus lent en pratique
Support matérielTrès large (instructions SHA-NI)Plus limité
Cas d’usage conseilléInteropérabilité, gros volumesDiversité algorithmique, conformité spécifique

Prérequis avant de commencer

Avant de coder, vérifiez que votre environnement correspond à ces versions. Le module crypto natif de Node.js suffit pour tout ce tutoriel, aucune dépendance cryptographique tierce n’est nécessaire.

  • Node.js 24 LTS (version LTS active en septembre 2026, dernière publication de cette ligne : 24.21.0) ou Node.js 22 LTS en environnement de production plus conservateur
  • npm 10 ou supérieur (fourni avec Node.js 24)
  • Un terminal Linux, macOS ou Windows avec PowerShell
  • OpenSSL disponible dans le build Node.js utilisé (nécessaire pour SHA3-256 ; vérifiable via node -e "console.log(require('crypto').getHashes())")
  • Un éditeur de code (VS Code, JetBrains ou équivalent)
  • Environ 30 à 45 minutes pour suivre l’ensemble des 12 étapes

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

node --version
# doit afficher v24.x.x ou v22.x.x (LTS)

npm --version
# doit afficher 10.x.x ou plus récent

Étape 1 : initialiser le projet Node.js

Créez un nouveau dossier de projet et initialisez-le avec npm. Nous utilisons les modules ES (type module) pour profiter de la syntaxe d’import moderne prise en charge nativement par Node.js 24.

mkdir integrite-fichiers
cd integrite-fichiers
npm init -y
npm pkg set type="module"
mkdir src

Aucune dépendance externe n’est requise : le module node:crypto intégré expose déjà tout ce qu’il faut pour SHA-256 et SHA-3. C’est d’ailleurs un des arguments forts de cette approche par rapport à une bibliothèque tierce : moins de surface d’attaque supply chain, puisque vous ne dépendez d’aucun paquet externe pour l’opération la plus sensible de votre pipeline.

Étape 2 : générer une empreinte SHA-256 d’un fichier

Commençons par la brique de base : calculer le hachage SHA-256 d’un seul fichier. Le module crypto de Node.js documente la méthode crypto.createHash(algorithm[, options]), qui retourne un objet Hash. Les données sont ajoutées avec hash.update() puis finalisées avec hash.digest(). Ces objets Hash ne doivent jamais être instanciés directement avec new, il faut toujours passer par createHash().

Pour un petit fichier, une lecture complète en mémoire fonctionne, mais pour un gros fichier (image disque, sauvegarde, vidéo), il faut passer par un flux (stream) afin d’éviter de saturer la mémoire du processus. Voici la version en flux, recommandée dans tous les cas :

// src/hash-fichier.js
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';

export function hacherFichier(cheminFichier, algorithme = 'sha256') {
  return new Promise((resolve, reject) => {
    const hash = createHash(algorithme);
    const flux = createReadStream(cheminFichier);

    flux.on('data', (morceau) => hash.update(morceau));
    flux.on('end', () => resolve(hash.digest('hex')));
    flux.on('error', (erreur) => reject(erreur));
  });
}

Testez cette fonction directement :

// test-rapide.js
import { hacherFichier } from './src/hash-fichier.js';

const empreinte = await hacherFichier('./package.json', 'sha256');
console.log(empreinte);

Sortie attendue (l’empreinte réelle dépendra du contenu exact de votre fichier package.json) :

a3f5c8e1d4b7962f0e8c1a5d9b3e7f2c4a6d8b0e2f4c6a8b0d2e4f6a8c0b2d4e

Étape 3 : ajouter le support de SHA-3 (SHA3-256)

La bonne nouvelle, c’est que la fonction écrite à l’étape précédente fonctionne déjà pour SHA-3 : il suffit de passer le nom d’algorithme correspondant. Node.js expose SHA3-256 sous l’identifiant sha3-256 si votre build OpenSSL le supporte (c’est le cas par défaut sur les binaires officiels Node.js 24 pour Linux, macOS et Windows).

const empreinteSha3 = await hacherFichier('./package.json', 'sha3-256');
console.log(empreinteSha3);

Avant d’aller plus loin, vérifiez la liste des algorithmes disponibles sur votre installation. C’est une commande à garder sous la main, car elle diffère selon la version d’OpenSSL liée à votre binaire Node.js :

node -e "console.log(require('node:crypto').getHashes().filter(h => h.includes('sha3')))"
// ['sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']

Étape 4 : construire un manifeste de checksums pour tout un dossier

Un contrôle fichier par fichier n’a que peu d’intérêt en pratique. Ce que vous voulez, c’est un manifeste : une liste de toutes les empreintes d’un dossier (un build, une release, un dossier de sauvegarde), que vous pourrez rejouer plus tard pour détecter la moindre modification. Utilisons le module node:fs/promises pour parcourir récursivement l’arborescence.

// src/manifeste.js
import { readdir } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { hacherFichier } from './hash-fichier.js';

async function listerFichiers(dossier, racine = dossier) {
  const entrees = await readdir(dossier, { withFileTypes: true });
  let fichiers = [];

  for (const entree of entrees) {
    const cheminComplet = join(dossier, entree.name);
    if (entree.isDirectory()) {
      if (entree.name === 'node_modules' || entree.name === '.git') continue;
      fichiers = fichiers.concat(await listerFichiers(cheminComplet, racine));
    } else if (entree.isFile()) {
      fichiers.push(cheminComplet);
    }
  }
  return fichiers;
}

export async function genererManifeste(dossier, algorithme = 'sha256') {
  const fichiers = await listerFichiers(dossier);
  const manifeste = {};

  for (const fichier of fichiers) {
    const cheminRelatif = relative(dossier, fichier);
    manifeste[cheminRelatif] = await hacherFichier(fichier, algorithme);
  }

  return manifeste;
}

Notez l’exclusion explicite de node_modules et .git : sans ce filtre, générer un manifeste sur un vrai projet peut prendre plusieurs minutes et produire un fichier de plusieurs dizaines de milliers de lignes sans aucune valeur ajoutée pour la sécurité. C’est une erreur fréquente chez les débutants sur ce type de script.

Étape 5 : sauvegarder et rejouer le manifeste au format JSON

Le manifeste doit être stocké quelque part de fiable, idéalement hors du dossier qu’il décrit (sur un stockage séparé, signé, ou versionné à part). Ajoutons l’écriture et la lecture au format JSON, avec horodatage et algorithme utilisé, afin de garder une trace complète du contexte de génération.

// src/io-manifeste.js
import { writeFile, readFile } from 'node:fs/promises';

export async function sauvegarderManifeste(manifeste, algorithme, cheminSortie) {
  const contenu = {
    algorithme,
    genereLe: new Date().toISOString(),
    nombreFichiers: Object.keys(manifeste).length,
    empreintes: manifeste,
  };
  await writeFile(cheminSortie, JSON.stringify(contenu, null, 2), 'utf8');
}

export async function chargerManifeste(cheminFichier) {
  const brut = await readFile(cheminFichier, 'utf8');
  return JSON.parse(brut);
}

Exemple de manifeste généré, tel qu’il apparaîtra dans manifeste.json :

{
  "algorithme": "sha256",
  "genereLe": "2026-09-11T08:42:17.204Z",
  "nombreFichiers": 3,
  "empreintes": {
    "package.json": "a3f5c8e1d4b7962f0e8c1a5d9b3e7f2c4a6d8b0e2f4c6a8b0d2e4f6a8c0b2d4e",
    "src/hash-fichier.js": "7c1e9a3b5d7f9102a4c6e8f0b2d4a6c8e0f2a4c6e8f0a2c4e6a8c0e2a4c6e8f0",
    "src/manifeste.js": "5e2a8c4f6d0b2e4a6c8e0f2a4c6e8a0c2e4a6c8e0f2a4c6e8f0a2c4e6a8c0e2a"
  }
}

Étape 6 : vérifier l’intégrité et détecter toute falsification

C’est le cœur du projet : recalculer les empreintes actuelles du dossier, et les comparer une par une au manifeste de référence. Trois cas doivent être distingués clairement dans le rapport : fichier modifié, fichier manquant, et fichier nouveau (non présent dans le manifeste d’origine). Confondre ces trois cas est une source classique de faux diagnostics.

// src/verifier.js
import { genererManifeste } from './manifeste.js';

export async function verifierIntegrite(dossier, manifesteReference) {
  const { algorithme, empreintes: reference } = manifesteReference;
  const actuel = await genererManifeste(dossier, algorithme);

  const modifies = [];
  const manquants = [];
  const nouveaux = [];

  for (const [chemin, empreinteAttendue] of Object.entries(reference)) {
    if (!(chemin in actuel)) {
      manquants.push(chemin);
    } else if (actuel[chemin] !== empreinteAttendue) {
      modifies.push({ chemin, attendu: empreinteAttendue, obtenu: actuel[chemin] });
    }
  }

  for (const chemin of Object.keys(actuel)) {
    if (!(chemin in reference)) nouveaux.push(chemin);
  }

  return {
    conforme: modifies.length === 0 && manquants.length === 0,
    modifies,
    manquants,
    nouveaux,
  };
}

Remarquez la clé conforme, qui ignore volontairement les fichiers nouveaux : dans la plupart des cas d’usage réels (release, dossier de sauvegarde), l’apparition d’un fichier non prévu mérite un avertissement mais ne doit pas forcément bloquer un pipeline, contrairement à un fichier modifié ou disparu.

Étape 7 : assembler un script en ligne de commande

Réunissons tout dans un point d’entrée exécutable, avec deux commandes : generer pour créer le manifeste, verifier pour le contrôler. Utilisons process.argv directement plutôt qu’une dépendance de parsing d’arguments, pour garder ce projet sans dépendance externe.

// src/cli.js
import { genererManifeste } from './manifeste.js';
import { sauvegarderManifeste, chargerManifeste } from './io-manifeste.js';
import { verifierIntegrite } from './verifier.js';

const [commande, dossierCible, algorithme = 'sha256'] = process.argv.slice(2);

if (commande === 'generer') {
  const manifeste = await genererManifeste(dossierCible, algorithme);
  await sauvegarderManifeste(manifeste, algorithme, 'manifeste.json');
  console.log(`Manifeste genere : ${Object.keys(manifeste).length} fichiers (${algorithme}).`);
} else if (commande === 'verifier') {
  const reference = await chargerManifeste('manifeste.json');
  const resultat = await verifierIntegrite(dossierCible, reference);

  if (resultat.conforme) {
    console.log('Integrite confirmee : aucune alteration detectee.');
  } else {
    console.error('ALERTE : alteration detectee.');
    resultat.modifies.forEach(f => console.error(`  Modifie : ${f.chemin}`));
    resultat.manquants.forEach(f => console.error(`  Manquant : ${f}`));
    process.exitCode = 1;
  }
  resultat.nouveaux.forEach(f => console.warn(`  Nouveau fichier (non reference) : ${f}`));
} else {
  console.log('Usage : node src/cli.js   [algorithme]');
}

Utilisation depuis le terminal :

node src/cli.js generer ./dist sha256
# Manifeste genere : 47 fichiers (sha256).

node src/cli.js verifier ./dist
# Integrite confirmee : aucune alteration detectee.

Étape 8 : simuler une falsification pour valider le script

Un script de sécurité qu’on n’a jamais vu échouer est un script auquel on ne peut pas faire confiance. Modifiez volontairement un fichier du dossier surveillé, puis relancez la vérification pour confirmer que l’alerte se déclenche correctement.

echo "// ligne ajoutee" >> ./dist/app.js
node src/cli.js verifier ./dist

Sortie attendue :

ALERTE : alteration detectee.
  Modifie : app.js

Le code de sortie du process passe également à 1, ce qui est essentiel pour l’étape suivante : sans ce détail, un pipeline CI/CD continuerait silencieusement même en cas d’alerte.

Étape 9 : générer un fichier de checksums compatible sha256sum

Pour interopérer avec les outils Unix classiques (sha256sum -c) ou avec des scripts tiers qui attendent ce format, ajoutons un export au format texte standard : une empreinte, deux espaces, un nom de fichier, une ligne par entrée.

// src/export-txt.js
import { writeFile } from 'node:fs/promises';

export async function exporterFormatSha256sum(manifeste, cheminSortie) {
  const lignes = Object.entries(manifeste)
    .map(([chemin, empreinte]) => `${empreinte}  ${chemin}`)
    .join('\n');
  await writeFile(cheminSortie, lignes + '\n', 'utf8');
}

Ce fichier peut ensuite être vérifié directement en ligne de commande, sans même passer par Node.js, ce qui est pratique pour les environnements où seul un shell POSIX est disponible :

sha256sum -c checksums.txt
package.json: OK
src/hash-fichier.js: OK
src/manifeste.js: OK

Étape 10 : intégrer le contrôle dans une pipeline CI/CD

L’intérêt réel de ce script se révèle une fois automatisé. Voici un exemple d’intégration dans une pipeline GitHub Actions, qui génère le manifeste après le build puis le vérifie avant chaque déploiement.

# .github/workflows/integrite.yml
name: Verification integrite
on: [push]
jobs:
  verifier:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '24'
      - run: npm ci
      - run: npm run build
      - run: node src/cli.js generer ./dist sha256
      - run: node src/cli.js verifier ./dist
      - name: Publier le manifeste comme artefact
        uses: actions/upload-artifact@v4
        with:
          name: manifeste-integrite
          path: manifeste.json

Point important, en écho direct à la faille CVE-2026-42575 évoquée plus haut : le manifeste doit être publié comme artefact signé ou stocké séparément du dépôt qu’il vérifie. Si un attaquant peut modifier à la fois le fichier et son manifeste de référence dans le même commit, le contrôle ne sert plus à rien. C’est exactement le trou de sécurité qui a permis à apko d’accepter des paquets modifiés : la signature portait sur l’index, pas sur une vérification croisée fiable des checksums individuels.

Étape 11 : ajouter une détection par lot de plusieurs algorithmes

Pour renforcer la défense en profondeur, il est possible de calculer deux empreintes différentes (SHA-256 et SHA3-256) pour chaque fichier. L’intérêt : si une faiblesse structurelle venait à être découverte sur une des deux familles d’algorithmes, l’autre resterait un filet de sécurité, puisque les deux constructions mathématiques (Merkle-Damgård contre éponge Keccak) sont indépendantes.

// src/hash-double.js
import { hacherFichier } from './hash-fichier.js';

export async function hacherDouble(cheminFichier) {
  const [sha256, sha3] = await Promise.all([
    hacherFichier(cheminFichier, 'sha256'),
    hacherFichier(cheminFichier, 'sha3-256'),
  ]);
  return { sha256, sha3256: sha3 };
}

Ce calcul en parallèle avec Promise.all évite de doubler le temps d’exécution en lisant le fichier deux fois de façon séquentielle : les deux flux de lecture s’exécutent en même temps, avec un coût CPU supplémentaire mais un temps d’attente disque quasiment inchangé.

Étape 12 : écrire des tests automatisés avec le testeur intégré de Node.js

Depuis plusieurs versions, Node.js embarque son propre exécuteur de tests (node:test), ce qui évite là encore une dépendance externe. Ajoutons trois cas de test essentiels : hachage stable, détection de modification, et détection de fichier manquant.

// test/verifier.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { writeFile, mkdir, rm } from 'node:fs/promises';
import { genererManifeste } from '../src/manifeste.js';
import { verifierIntegrite } from '../src/verifier.js';

test('detecte un fichier modifie', async () => {
  await mkdir('./tmp-test', { recursive: true });
  await writeFile('./tmp-test/a.txt', 'contenu initial');

  const reference = { algorithme: 'sha256', empreintes: await genererManifeste('./tmp-test') };
  await writeFile('./tmp-test/a.txt', 'contenu modifie');

  const resultat = await verifierIntegrite('./tmp-test', reference);
  assert.equal(resultat.conforme, false);
  assert.equal(resultat.modifies.length, 1);

  await rm('./tmp-test', { recursive: true });
});

Lancez la suite avec la commande intégrée :

node --test
# ✔ detecte un fichier modifie (12.4ms)
# tests 1
# pass 1
# fail 0

5 pièges courants à éviter

Ces erreurs reviennent régulièrement dans les implémentations de vérification d’intégrité, y compris dans des outils publiés. Les connaître à l’avance évite des heures de débogage.

  • Charger tout le fichier en mémoire avec readFile. Sur un fichier de plusieurs gigaoctets, cela peut faire planter le process avec une erreur de dépassement de mémoire. Utilisez toujours createReadStream comme montré à l’étape 2.
  • Stocker le manifeste au même endroit que les fichiers surveillés. Si l’attaquant qui modifie un fichier peut aussi régénérer le manifeste local, la vérification devient inutile. Signez ou externalisez toujours le manifeste de référence.
  • Comparer les empreintes avec une simple égalité de chaîne dans un contexte sensible aux attaques temporelles. Pour un usage de vérification d’intégrité pure ce n’est pas critique, mais dès que le hachage sert à valider un secret (jeton, mot de passe), utilisez crypto.timingSafeEqual plutôt qu’un simple ===.
  • Oublier la casse et l’encodage de sortie. digest(‘hex’) produit des minuscules ; si vous comparez à une valeur venant d’un autre outil qui produit des majuscules, la comparaison échouera à tort. Normalisez systématiquement en minuscule avant de comparer.
  • Confondre intégrité et authenticité. Un hachage prouve qu’un fichier n’a pas changé depuis le calcul du manifeste, pas qu’il provient d’une source de confiance. Sans signature cryptographique du manifeste lui-même (GPG, Sigstore, ou équivalent), un attaquant capable de remplacer à la fois le fichier et le manifeste reste invisible à ce contrôle, exactement comme dans le cas apko décrit plus haut.

Comparatif : SHA-256, SHA-3 et les autres options de hachage disponibles en Node.js

Le module crypto de Node.js expose bien plus que SHA-256 et SHA-3. Voici comment se positionnent les principales options pour un usage de vérification d’intégrité de fichiers, à ne pas confondre avec le hachage de mots de passe (qui nécessite des fonctions dédiées comme Argon2id ou bcrypt, conçues pour être lentes, à l’inverse de ce qu’on recherche ici).

AlgorithmeSortieStatut sécurité 2026Usage recommandé
MD5128 bitsCassé (collisions praticables)À proscrire pour la sécurité
SHA-1160 bitsCassé (collisions démontrées)À proscrire pour la sécurité
SHA-256256 bitsSûr, recommandé NISTIntégrité de fichiers, checksums, usage général
SHA3-256256 bitsSûr, recommandé NISTDiversité algorithmique, conformité spécifique
BLAKE3256 bits (extensible)Sûr, non normalisé NISTPerformance brute sur très gros volumes
SHA-512512 bitsSûr, recommandé NISTMarge de sécurité supplémentaire

MD5 et SHA-1 apparaissent volontairement dans ce tableau : on les retrouve encore dans une quantité surprenante de scripts de vérification hérités, souvent parce qu’ils sont plus rapides ou parce que le script n’a simplement jamais été mis à jour. Pour toute nouvelle implémentation en 2026, ni l’un ni l’autre ne doit servir à un contrôle de sécurité, même si leur usage reste toléré pour de la simple détection de doublons sans enjeu de sécurité (déduplication de fichiers par exemple).

Astuces avancées pour aller plus loin

Une fois le script de base maîtrisé, plusieurs améliorations méritent d’être envisagées selon votre contexte de production.

Premièrement, signez le manifeste lui-même. Un manifeste JSON non signé reste vulnérable si un attaquant a accès en écriture au même espace de stockage. Une signature GPG du fichier manifeste.json, vérifiée avant toute lecture, ferme cette brèche à moindre coût.

Deuxièmement, pensez à la parallélisation contrôlée. Le code de l’étape 4 traite les fichiers séquentiellement, ce qui est simple mais lent sur un dossier contenant des milliers de petits fichiers. Un pool de promesses limité (par exemple 8 hachages en parallèle avec un compteur de concurrence) accélère nettement le traitement sans saturer les descripteurs de fichiers ouverts du système.

Troisièmement, si votre chaîne de build produit des artefacts publiés publiquement (paquet npm, binaire, image de conteneur), envisagez d’aller au-delà du simple manifeste local et d’adopter une attestation de provenance de type SLSA, qui lie cryptographiquement le hachage de l’artefact à son pipeline de build d’origine. C’est ce qui manquait précisément dans les incidents Miasma et Mini Shai-Hulud évoqués en introduction : les hachages génériques, seuls, n’ont pas suffi à bloquer des versions générées avec un payload différent à chaque publication.

Enfin, journalisez chaque vérification avec horodatage, résultat et empreinte de la dernière exécution réussie, dans un système de logs centralisé et en écriture seule pour les processus applicatifs. Cela transforme un script ponctuel en un contrôle de sécurité auditable dans la durée, ce qu’attendent la plupart des référentiels de conformité actuels, y compris les exigences documentaires associées à NIS2 en Europe.

Trois cas d’usage concrets pour ce script en production

Ce script trouve sa place dans des contextes très différents. Voici trois scénarios réels où la vérification d’intégrité par hachage fait une vraie différence opérationnelle, avec les ajustements à prévoir pour chacun.

Le premier scénario concerne les releases logicielles publiées publiquement. Avant de publier un binaire ou une archive sur un site de téléchargement, générez le manifeste, publiez-le à côté du fichier, et signez-le avec une clé GPG dédiée à la publication. Un utilisateur qui télécharge votre logiciel peut alors reproduire le calcul avec la fonction de l’étape 2 et comparer le résultat, sans avoir besoin de faire confiance au canal de téléchargement lui-même (miroir tiers, CDN, torrent). C’est exactement le principe que suivent les distributions Linux depuis des années pour leurs paquets, et c’est ce même principe que la faille apko décrite plus haut a fini par contourner faute d’une vérification unitaire correcte.

Le deuxième scénario touche les sauvegardes serveur. Un dossier de sauvegarde peut être corrompu silencieusement par une erreur disque, une interruption réseau pendant une copie, ou une compression défaillante, bien avant qu’un attaquant n’entre en jeu. Générez un manifeste immédiatement après chaque sauvegarde complète et stockez-le sur un support distinct (un bucket de stockage objet séparé, par exemple). Lors d’une restauration, faites tourner la vérification avant de considérer la sauvegarde comme fiable : mieux vaut découvrir un fichier corrompu au moment du contrôle qu’au moment critique d’une restauration d’urgence.

Le troisième scénario est celui de la réponse à incident en forensic numérique. Lorsqu’une machine est suspectée d’avoir été compromise, générer un manifeste d’intégrité de l’ensemble du système de fichiers (binaires système, bibliothèques partagées, fichiers de configuration) et le comparer à un manifeste de référence pris sur une image saine permet d’identifier rapidement les fichiers modifiés par l’attaquant. Dans ce cas précis, privilégiez le calcul en double algorithme présenté à l’étape 11 : un investigateur qui doit présenter ses conclusions devant un tiers (client, assureur, autorité) bénéficie d’une preuve renforcée par deux constructions cryptographiques indépendantes plutôt que par une seule.

Projet complet : structure finale du dépôt

Voici l’arborescence complète du projet tel qu’il se présente à la fin de ce tutoriel. Chaque fichier correspond à une étape déjà détaillée ci-dessus.

integrite-fichiers/
├── package.json
├── manifeste.json
├── checksums.txt
├── src/
│   ├── hash-fichier.js
│   ├── hash-double.js
│   ├── manifeste.js
│   ├── io-manifeste.js
│   ├── verifier.js
│   ├── export-txt.js
│   └── cli.js
├── test/
│   └── verifier.test.js
└── .github/
    └── workflows/
        └── integrite.yml

Ajoutez ces deux scripts pratiques dans votre package.json pour retrouver rapidement les commandes principales :

{
  "scripts": {
    "integrite:generer": "node src/cli.js generer ./dist sha256",
    "integrite:verifier": "node src/cli.js verifier ./dist",
    "test": "node --test"
  }
}

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

Voici les erreurs les plus fréquemment rencontrées en suivant ce type de tutoriel, avec la cause probable et la correction associée.

  • Error: Digest method not supported. L’algorithme sha3-256 n’est pas reconnu par votre build Node.js. Vérifiez avec require(‘node:crypto’).getHashes() et mettez à jour vers Node.js 24 LTS si SHA-3 est absent de la liste.
  • ENOENT: no such file or directory. Le chemin passé en argument à la CLI est relatif à un mauvais répertoire de travail. Utilisez toujours un chemin absolu ou vérifiez votre répertoire courant avec pwd avant de lancer le script.
  • JavaScript heap out of memory. Un fichier a été lu avec readFile au lieu de createReadStream. Revenez à l’implémentation en flux de l’étape 2, qui traite les données par morceaux plutôt qu’en une seule fois.
  • Le manifeste indique des milliers de fichiers modifiés après un simple git pull. Les fins de ligne ont probablement été réécrites par Git (conversion LF/CRLF automatique). Configurez git config core.autocrlf false sur les postes concernés avant de régénérer le manifeste.
  • Les empreintes diffèrent entre Linux et Windows pour un fichier identique. Même cause que ci-dessus : la conversion de fin de ligne modifie le contenu binaire réel du fichier. Le hachage cryptographique est correct, c’est le contenu source qui a changé.
  • TypeError: hash.update is not a function. Un objet Hash a probablement été instancié avec new crypto.Hash() au lieu de crypto.createHash(). Corrigez l’appel comme indiqué à l’étape 2.
  • Le script de vérification est très lent sur un dossier réseau (NFS, SMB). La latence réseau domine le temps total. Copiez temporairement les fichiers en local avant de calculer les empreintes, ou augmentez la taille des morceaux lus par le flux.
  • node –test ne trouve aucun test. Le nom du fichier ne respecte pas la convention attendue. Node.js recherche par défaut les fichiers se terminant par .test.js ou situés dans un dossier test/ ; renommez le fichier si besoin.

Le hachage seul ne suffit jamais : ce qu’il faut ajouter en production

Ce tutoriel a volontairement insisté sur les limites du hachage brut, car c’est le piège numéro un pour toute équipe qui déploie ce type de contrôle sans recul. Un hachage seul répond uniquement à la question a-t-on modifié ce fichier depuis la dernière fois. Il ne répond ni à qui l’a modifié, ni à quand exactement, ni à cette version vient-elle vraiment de la bonne source. Ces trois questions nécessitent respectivement des logs d’accès, un système d’horodatage de confiance, et une chaîne de signature (GPG, Sigstore, ou une attestation SLSA comme évoqué plus haut).

Dans le contexte 2026 marqué par la multiplication des attaques de supply chain (37 campagnes recensées au premier semestre 2026 selon les données Phoenix, avec un pic de 14 campagnes rien qu’en mai 2026), la vérification d’intégrité par hachage doit être vue comme une couche parmi d’autres, pas comme une solution complète en elle-même. Elle reste néanmoins l’une des couches les moins coûteuses à mettre en place, ce qui justifie amplement les 30 à 45 minutes investies dans ce tutoriel.

Questions fréquentes

SHA-256 est-il toujours sûr en 2026 ?
Oui. Le NIST continue de recommander SHA-256 au minimum pour l’interopérabilité, et aucune attaque pratique de collision ou de préimage n’a été démontrée contre cet algorithme à ce jour.

Faut-il utiliser SHA-256 ou SHA3-256 pour un nouveau projet ?
Pour la grande majorité des cas d’usage, SHA-256 reste le choix par défaut, plus rapide et plus largement supporté. SHA3-256 se justifie quand une diversité algorithmique est explicitement recherchée, ou quand une exigence de conformité l’impose.

Peut-on utiliser MD5 pour vérifier l’intégrité d’un fichier téléchargé ?
Non, pas dans un contexte de sécurité. MD5 est cassé depuis des années et permet de construire des collisions volontaires. Il reste acceptable uniquement pour de la détection de doublons sans enjeu de sécurité.

Le hachage protège-t-il contre un attaquant qui modifie à la fois le fichier et le manifeste ?
Non. C’est exactement la limite décrite dans ce tutoriel à propos de la faille CVE-2026-42575 sur apko. Il faut signer le manifeste ou le stocker sur un système que l’attaquant ne peut pas modifier en même temps que le fichier surveillé.

Node.js 24 est-il obligatoire pour suivre ce tutoriel ?
Non, Node.js 22 LTS fonctionne également pour tout le code présenté. Node.js 24 est simplement la ligne LTS active recommandée en septembre 2026.

Comment vérifier qu’un fichier téléchargé correspond bien au checksum publié par l’éditeur ?
Calculez l’empreinte SHA-256 du fichier téléchargé avec la fonction de l’étape 2, puis comparez-la caractère par caractère (en minuscules) avec la valeur publiée sur le site officiel de l’éditeur, idéalement via une page servie en HTTPS.

Ce script fonctionne-t-il aussi pour vérifier l’intégrité de sauvegardes chiffrées ?
Oui, le hachage s’applique au contenu binaire du fichier, chiffré ou non. Hachez le fichier après chiffrement pour détecter toute altération du conteneur chiffré lui-même.

Faut-il aussi vérifier l’intégrité des dépendances npm du projet ?
Oui, c’est même la priorité. npm vérifie déjà automatiquement l’intégrité des paquets via le champ integrity de package-lock.json (basé sur SHA-512), mais un audit régulier avec npm audit reste recommandé pour détecter les vulnérabilités connues en plus des altérations de contenu.

Sources et documentation officielle : documentation du module crypto de Node.js, norme FIPS 202 (SHA-3) du NIST, projet fonctions de hachage du NIST, OWASP Top 10, et les recommandations de l’ANSSI pour le contexte réglementaire français.