Aller au contenu

MD5 et SHA-1

Pour l'auditeur, ou le scanner, qui demande pourquoi un code de 2026 calcule du MD5.

Une analyse de dépendances de BatleHub signale des usages de MD5 ou de SHA-1 (SonarCloud les lève en rust:S4790, CRITICAL). Cette page est le registre : chacun d'eux, ce à quoi il sert, et — revérifié contre la spécification courante de chaque protocole — si ce protocole accepterait quelque chose de plus fort.

Il y en avait treize. Quatre se sont révélés exigés par rien et ont été supprimés ; neuf restent, et cette page dit pourquoi chacun est là.

La ligne qui les sépare

Aucune de ces valeurs n'est ce à quoi BatleHub fait confiance. L'intégrité d'un artefact, quand elle est une décision de sécurité, repose sur SHA-256, calculé indépendamment à la première écriture des octets et revérifié à chaque service ultérieur, ainsi que sur les signatures OpenPGP couvrant les index des dépôts.

Les empreintes faibles sont toutes l'une de trois choses : un champ qu'un format d'échange nomme MD5 ou SHA-1, un validateur de cache qu'un client compare octet pour octet, ou le côté vérification d'une somme de contrôle qu'un registre amont a annoncée en SHA-1. Une collision contre l'une d'elles n'apporte à un attaquant rien qu'il n'obtiendrait en servant d'autres octets, parce que rien ne s'y adosse.

C'est l'argument pour les neuf qui restent. Ce n'est pas la même chose que dire que chacune est inévitable, ce à quoi servait la revérification — et quatre n'y ont pas survécu.

Le registre

#AlgorithmeVerdict
1core/services/integrity.rssha1_hexSHA-1Imposé par Composer
2core/services/integrity.rsverify, StreamingVerifierSHA-1Vérification, pas émission
3core/…/local_registry/eco_rubygems.rs/versionsMD5Imposé par l'index compact
4web/…/proxy/rubygems/range.rs — ETagMD5Supprimé — l'ETag est un SHA-256 ; voir §4
5adapters/repo/deb.rsMD5, SHA-1Supprimé — facultatif chez Debian
6adapters/repo/pacman.rsMD5Supprimé — retiré du format
7adapters/repo/openpgp.rs — empreinteSHA-1Immuable par définition
8core/services/listing_synthesis.rs — listings composésSHA-1Imposé par le format imité
9web/…/proxy/maven/proxy.rs — fichiers .md5/.sha1MD5, SHA-1L'algorithme est l'extension du fichier
10adapters/repo/apk.rs — le champ C: de l'indexSHA-1Imposé par apk-tools, et vérifié à l'installation

Les entrées 4, 5 et 6 sont barrées parce que le code ne les calcule plus. L'entrée 4 est en gras parce que c'est un choix de compatibilité plutôt qu'une exigence, et la seule qu'il reste à revisiter. Voir Revérifié.

1. Le dist.shasum de Composer

L'objet dist de Composer porte un unique champ de somme de contrôle, shasum, et c'est un SHA-1. Y publier un SHA-256 ne dégrade pas vers « non vérifié » — Composer hache le zip téléchargé en SHA-1 et compare : tous les téléchargements échouent.

Un champ sha256 dans dist est une demande de fonctionnalité ouverte depuis 2017 et n'est pas implémenté. Rien de plus fort n'est disponible.

Précision de portée : sha1_hex est appelé depuis exactement un endroit, le gestionnaire de publication Composer. Il n'est pas employé pour npm — le proxy npm préfère dist.integrity (SSRI, en général SHA-512) à dist.shasum et ne s'y rabat que si l'amont l'omet, et le packument npm local laisse passer le dist envoyé par le client qui publie, en n'y réécrivant que l'URL du tarball.

2. Vérifier ce qu'un amont a annoncé

verify et StreamingVerifier acceptent SHA-1, SHA-256 et SHA-512, et choisissent l'algorithme d'après la somme de contrôle publiée par le registre amont. Quand un registre annonce un SHA-1, hacher en SHA-1 est la seule façon de comparer.

C'est la seule entrée où retirer l'algorithme faible aggrave strictement les choses : l'alternative à vérifier une somme SHA-1 est de ne rien vérifier du tout.

3. L'index compact de RubyGems

Le document /versions de l'index compact est une ligne par gem se terminant par une empreinte, et le format définit cette empreinte comme un MD5 du document /info de la gem :

RUBYGEM [-]VERSION_PLATFORM[,VERSION_PLATFORM],...] MD5

Bundler la recalcule pour décider si sa copie en cache de /info est à jour. C'est un validateur de cache, dans l'algorithme que le format nomme, et il est toujours d'actualité. Rien de plus fort n'est disponible pour ce champ.

Notez que l'en-tête Repr-Digest que BatleHub envoie sur ces mêmes documents est déjà en SHA-256 — c'est l'empreinte moderne, celle de la RFC 9530, et celle que la spécification exige réellement.

4. L'ETag de l'index compact — supprimé

compact_response pose l'ETag sur le corps du document et holds_our_prefix le recalcule sur un préfixe : c'est ce qui rend les requêtes de plage reprenables de Bundler vérifiables. Si le validateur du client égale l'empreinte de nos N premiers octets, sa copie est notre préfixe et ajouter la queue est prouvablement correct (RFC 0009 §13.24).

Ce mécanisme n'a jamais eu besoin de MD5. L'etag est opaque pour le client, qui le relit dans son propre fichier d'etag et le met entre guillemets — bundler 4.0.17, compact_index_client/updater.rb :

ruby
etag = etag_path.read.tap(&:chomp!) if etag_path.file?
headers["If-None-Match"] = %("#{etag}") if etag

La seule raison pour laquelle c'était un MD5 : Bundler avant 2.7 fabriquait lui-même un validateur quand il n'avait pas d'etag stocké — SharedHelpers.digest(:MD5).hexdigest(IO.read(path)) sur son fichier local — donc un etag serveur dans un autre algorithme ne correspondait jamais. Bundler 2.7.0 a retiré ce hachage le 16/07/2025, en changement cassant, et il ne reste aucune trace de MD5 dans l'updater de 4.0.17.

L'etag est donc désormais un SHA-256 du document, et MD5 a disparu de range.rs. Ce qu'obtient un client antérieur à 2.7 : son validateur fabriqué ne correspond à rien, holds_our_prefix répond non, et la réponse est un 200 avec le document entier — exactement le comportement qu'il avait avant que tout ceci existe. Dégradé d'un transfert complet, jamais faux : aucun client ne reçoit un document recollé. C'est le prix de l'abandon de Bundler < 2.7, et il est payé par des clients en retard de trois versions majeures.

5. Les Packages et Release de Debian — supprimé

parse_deb calculait autrefois MD5, SHA-1 et SHA-256 sur chaque .deb ; la strophe Packages émettait les trois, et Release listait chaque index sous une section MD5Sum: en plus de SHA256:.

Le format de dépôt Debian rend les faibles facultatifs et dit clairement ce qu'un client peut en faire :

Clients may not use the MD5Sum and SHA1 fields for security purposes, and must require a SHA256 or a SHA512 field.

Ils n'apportaient donc rien : apt accepte un index qui ne porte que du SHA-256, c'est le SHA-256 qu'il vérifie, et c'est la signature OpenPGP couvrant Release qui rend l'index digne de confiance en premier lieu. DebPackage et ReleaseFile ne portent plus de champ md5 ni sha1, les lignes MD5sum: et SHA1: ont disparu de chaque strophe, et la section MD5Sum: a disparu de Release.

Un .deb envoyé dont le control porte lui-même un champ MD5sum ou SHA1 se le voit toujours retirer — ce sont des champs de niveau dépôt, et un envoi n'a pas à en réintroduire un.

Le coût est la compatibilité avec un apt trop ancien pour gérer SHA-256 — bien plus ancien que tout ce qui reçoit encore des mises à jour de sécurité.

6. Le %MD5SUM% de Pacman — supprimé

Celui-là n'était pas seulement facultatif. pacman 6.1.0 a retiré la prise en charge de md5sum dans les bases de dépôt — repo-add a cessé de l'écrire et libalpm a abandonné sa lecture comme sa validation — et alpm-repo-desc(5) énonce :

The section %MD5SUM% has been removed.

BatleHub écrivait un champ que le pacman courant ignore. PacmanPackage ne porte plus de md5, et desc_entry n'émet plus %MD5SUM% ; %SHA256SUM%, que pacman lit bien, est inchangé.

PacmanPackage est stocké en JSON, et la structure n'a pas de deny_unknown_fields : les métadonnées écrites avant ce changement se désérialisent donc toujours — la clé en trop est ignorée.

7. L'empreinte OpenPGP v4

L'empreinte d'une clé OpenPGP de version 4 est définie comme un SHA-1 sur 0x99 || len16 || pubkey_body (RFC 4880 §12.2), et l'identifiant de clé en est les 64 bits de poids faible. C'est un identifiant à la construction spécifiée, pas un choix d'empreinte : le calculer autrement produit une empreinte qu'aucun client ne reconnaît, et le Signed-By: d'apt comme rpm --import s'y adossent tous deux.

Les clés v6 de la RFC 9580 emploient SHA-256, mais apt et rpm ne consomment pas de clés v6 aujourd'hui. Immuable tant que la clé est en v4.

10. L'identité de paquet d'apk

Le champ C: d'un APKINDEX vaut Q1 suivi du base64 d'un SHA-1, et il n'existe pas de seconde écriture : apk le calcule sous APK_SIGN_VERIFY_AND_GENERATE et le vérifie à l'installation sous APK_SIGN_VERIFY_IDENTITY. Un index dont le C: est dérivé autrement produit des paquets que tout client télécharge puis refuse, ce qui ressemble à de la corruption plutôt qu'à une divergence.

Deux raisons rendent ce choix sûr, les mêmes que pour les sidecars Maven :

  • Ce n'est pas une frontière de sécurité ici. Ce qu'un client installant fait confiance, c'est la signature RSA-2048/SHA-256 sur l'index entier (.SIGN.RSA256), et C: est un champ à l'intérieur de ce document signé. Forger un C: suppose de forger la signature.
  • Ce n'est pas à nous d'en décider. Le champ appartient au format de transport. apk-tools 3.0.8 lit exactement les mêmes entrées que la 2.14, et aucune branche Alpine ne publie d'index v3 (RFC 0026 §2.2, décision 3) : il n'existe donc aucune version de ce protocole en circulation où l'identité serait autre chose.

Résolvez-le dans le scanner, jamais dans le code : un « correctif » ici produit un dépôt qu'aucun apk ne peut installer.

Revérifié le 31 août 2026

Le tri initial enregistrait les treize comme « imposés par un format d'échange ». Trois entrées n'ont pas survécu à la confrontation avec les spécifications, et deux des trois ont ensuite été supprimées :

  • Le %MD5SUM% de Pacman (#6) — le champ a disparu du format. Supprimé.
  • Les MD5 et SHA-1 de Debian (#5) — facultatifs, et la spécification interdit aux clients de s'y fier. Supprimés.
  • L'ETag de l'index compact (#4) — exigé seulement par Bundler antérieur à 2.7. Conservé : il offre encore les requêtes de plage reprises à ces clients, et contrairement aux deux autres, ce n'est pas une sortie morte.

Cela a fait passer treize constats à neuf. Rien ici n'était une vulnérabilité — aucune attaque n'était rendue possible par l'un d'eux, ce pourquoi c'était un registre et non un incident — mais quatre étaient des sorties qu'aucun client courant ne lit, et la différence entre « le format l'exige » et « nous avons toujours émis cela » est exactement ce à quoi sert une revérification.

Comment les suppressions ont été vérifiées

Un vrai apt accepte le dépôt obtenu : apt-get update vérifie la signature OpenPGP Ed25519 sur InRelease, indexe le paquet, et apt-get download le récupère. Corrompre le .deb fait rejeter apt avec une non-correspondance d'empreinte signalée sur SHA256, ce qui est bien le point — le contrôle qui faisait le travail le fait toujours.

.github/workflows/repo-interop.yaml déroule cela de bout en bout pour de vrais apt, dnf et pacman en conteneurs, et se déclenche à tout changement sous crates/adapters/src/repo/.

Revérifié le 15 septembre 2026 — existe-t-il enfin mieux ?

C'est la question que ce registre sert à garder répondable : pour chaque entrée, l'amont qui impose l'algorithme faible a-t-il publié une option plus solide depuis le dernier passage ? La vérification porte sur les amonts eux-mêmes, pas sur un souvenir. Rien n'a bougé côté amont. Aucune entrée ne change pour cette raison — et l'une d'elles n'attendait aucun amont, donc elle a disparu (entrée 4, ci-dessous).

#EntréeÉtat de l'amont au 15/09/2026
1dist.shasum de ComposerToujours SHA-1 uniquement. composer#5940 est ouverte (dernière activité le 22/01/2025), et src/Composer/Downloader/FileDownloader.php sur main lit encore getDistSha1Checksum() puis compare hash_file('sha1', …). Un champ sha256 serait ignoré à l'entrée et fatal à la sortie.
3Somme de contrôle info de /versions (RubyGems)Toujours MD5. L'implémentation de référence, rubygems/compact_index sur master, calcule Digest::MD5.hexdigest(CompactIndex.info(...)). Le format nomme l'algorithme ; il n'y a pas de second champ.
4ETag de l'index compactSupprimé le jour même. Ce n'était pas une question d'amont — Bundler ≥ 2.7 a cessé de hacher ces réponses en juillet 2025 — donc le plancher de compatibilité a dépassé 2.6 et l'etag est un SHA-256. Voir §4.
7Empreinte OpenPGP v4Inchangée, et ce n'est pas un choix d'algorithme. La voie SHA-256 existe — la RFC 9580 définit des clés v6 dont l'empreinte est un SHA-256 — mais il s'agit d'une autre version de clé : l'adopter renomme toutes les clés publiées et dépend de l'acceptation de v6 par les vérificateurs apt, rpm et pacman. C'est une migration de format de clé avec une barrière d'interopérabilité, à suivre ailleurs que dans ce registre si elle est un jour engagée.
9Sidecars .md5/.sha1 de MavenDéjà additif. maven/proxy.rs calcule et sert .sha256 et .sha512 à côté ; la paire faible ne survit que parce qu'un resolver Maven configuré par défaut demande ces deux noms de fichiers. Les retirer ne gagne rien, sinon des clients par défaut cassés.

Le côté npm mérite sa propre ligne, parce que c'est le seul endroit où l'option forte a gagné franchement. pacote — le récupérateur qu'utilisent npm et tout ce qui est bâti dessus — lit d'abord dist.integrity et ne retombe sur dist.shasum que s'il est absent (lib/registry.js : dist.integrity ? ssri.parse(…) : dist.shasum ? ssri.fromHex(dist.shasum, 'sha1')). Ce serveur ne dépend jamais de ce repli : le packument composé par listing_synthesis.rs n'émet que integrity, un SRI SHA-256, et le chemin proxy préfère l'integrity annoncé par l'amont. Le SHA-1 qui subsiste autour de npm est dans les fixtures — l'amont simulé et les deux constructeurs d'amont des suites lourdes — et il y est exprès : leur rôle est de ressembler au registre auquel npm parle vraiment, et un vrai packument porte dist.shasum. Une fixture plus solide que ce qu'elle imite cesse de tester le chemin de repli sur lequel un amont réel peut encore nous placer.

À côté du code produit : les tests et les fixtures

Cinq fichiers de plus sont épinglés, et aucun n'est une décision : chacun calcule une empreinte faible pour vérifier ou pour imiter l'une des entrées ci-dessus, si bien que l'algorithme est choisi par la chose mise à l'épreuve.

AlgorithmeÀ quoi cela sert
web/tests/local_rubygems_compact_index.rsMD5Recalcule l'empreinte de l'entrée 3 pour vérifier que le serveur émet ce que Bundler attend.
web/tests/air_gap.rsMD5, SHA-1Vérifie que les fichiers annexes de l'entrée 9 sont émis, avec les valeurs qu'un client Maven calcule.
tests/heavy/upstream_audit.shSHA-1Recalcule le dist.shasum de npm pour vérifier ce que le serveur a servi.
tests/heavy/upstream_dir.shSHA-1Produit ce dist.shasum : c'est l'amont npm depuis lequel la suite hybride installe.
perf/mock-upstream/src/main.rsSHA-1Idem, pour l'amont de la charge de soak — un shasum faux y donne un 502 sur chaque lecture d'artefact.

Le mock est le plus récent des cinq, et il est la raison de refaire la vérification plutôt que de reprendre le commentaire précédent : il portait deux empreintes faibles, dont une seule était imposée. Son |checksum: d'index compact RubyGems était un SHA-1 synthétique là où le format nomme un SHA-256 du gem — faux sur le protocole autant que faible — et il émet désormais un SHA-256. Seul le dist.shasum de npm y est épinglé.

Comment le scanner les traite

Chacun a une entrée rust:S4790 (ou shell:S4790) dans sonar-project.properties, rapportée au seul fichier qui parle le protocole, avec son raisonnement en ligne. Ce sont des exclusions configurées plutôt que des résolutions par constat dans le tableau de bord, pour que la justification soit versionnée et relisible.

Onze fichiers sont épinglés : les six du registre qui relèvent du code produit, et les cinq ci-dessus. La portée est délibérée — une empreinte faible en dehors d'eux est un vrai constat. N'élargissez pas une resourceKey à un répertoire, et n'ajoutez pas une douzième entrée sans un argument de même nature, ce qui, comme cette page le montre, veut dire vérifier la spécification plutôt que répéter ce que disait le commentaire précédent.

À lire aussi : Security scanning (en anglais) pour la matrice complète des scanners et la position sans suppression du projet sur les alertes de dépendances.

Publié sous licence Apache 2.0. Fait avec ❤️ et beaucoup trop de ☕.