Que prouvent vraiment les builds reproductibles ?
Une checksum prouve que vous avez reçu le fichier publié. Une build reproductible prouve que ce fichier correspond au code source lisible. Deux affirmations distinctes.
Ce que prouvent les builds reproductibles
Les builds reproductibles prouvent un fait précis : le binaire que vous avez reçu est bien celui que produit exactement ce code source. N’importe qui peut reprendre le même code source, le compiler à nouveau et comparer les octets. La vérification ne dépend donc plus uniquement de l’éditeur.
Le projet Reproducible Builds le définit ainsi : « Une build est reproductible si, avec le même code source, le même environnement de build et les mêmes instructions de build, toute partie peut recréer des copies identiques bit à bit de tous les artefacts spécifiés. » La comparaison elle-même repose sur un hash. Toute la difficulté consiste à figer suffisamment précisément l’environnement et les instructions pour que deux machines différentes produisent le même résultat.
Pourquoi une somme de contrôle ne répond pas à cette question
Une somme de contrôle publiée prouve que le fichier est arrivé sans corruption. Une signature calculée sur cette somme de contrôle prouve qu’il provient de la personne qui détient la clé. Aucune des deux ne dit quoi que ce soit de ce qui s’est passé avant la création de l’artefact. Si la machine de build de l’éditeur est compromise, le binaire malveillant reçoit une somme de contrôle et une signature exactement comme un binaire sain, et toutes les vérifications en aval réussissent. Il en va de même si un mainteneur effectue un build depuis un working tree qui n’a jamais été poussé.
C’est là que se situe la lacune. Vous pouvez lire le code source, vérifier la signature et vérifier la somme de contrôle, tout en exécutant du code qui n’est jamais apparu dans le dépôt. La reproductibilité est donc un sujet différent de la vérification d’un téléchargement avec sa somme de contrôle publiée. La somme de contrôle protège le transfert. Le rebuild protège tout ce qui s’est passé avant le transfert.
L’attaque n’est pas théorique. La compromission de SolarWinds Orion en 2020 se situait exactement à ce niveau : le système de build produisait des artefacts signés qui ne correspondaient pas au code source examiné. Toutes les vérifications de signature réussissaient, car les signatures commencent au niveau de l’artefact.
Ce qu’un build reproductible ne prouve pas
C’est la partie qui est souvent présentée de manière excessive. Il faut donc préciser ses limites.
- Il ne prouve pas que le code source est sûr. Une porte dérobée ajoutée ouvertement au dépôt produit un build reproductible, et chaque personne qui reconstruit le logiciel le confirme, parce que toutes ont utilisé le même code source malveillant. La reproductibilité déplace la cible vers l’arbre des sources. Quelqu’un doit toujours examiner cet arbre. C’est pourquoi la politique de revue, notamment les politiques relatives au code assisté par IA dans les projets open source, reste un contrôle distinct.
- Elle ne prouve pas que vos entrées sont sûres. Les dépendances font partie de ce que vous construisez. Un paquet malveillant résolu au moment du build est compilé dans l’artefact, et chaque personne qui reconstruit le logiciel en résolvant la même dépendance obtient le même résultat que vous. C’est ainsi qu’une attaque de la chaîne d’approvisionnement npm atteint un serveur, et un build reproductible la reproduit fidèlement.
- Elle ne prouve pas que la toolchain est fiable. Si le compilateur est compromis, chaque personne qui reconstruit le logiciel avec ce compilateur produit la même sortie compromise, et toutes les vérifications aboutissent au même résultat. La reproductibilité augmente le coût de cette attaque. Elle ne la détecte pas.
- Elle ne dit rien sur les vulnérabilités. Une ancienne bibliothèque reproduite bit par bit reste une ancienne bibliothèque avec ses failles publiées. Continuez donc à rechercher les CVE connues sur votre serveur selon un calendrier distinct.
La reproductibilité élimine une position d’attaque précise : la machine de build et l’ensemble du chemin entre le code source et le binaire. Tant qu’un paquet n’est pas reproductible, personne en dehors de son éditeur ne peut examiner ce chemin.
Pourquoi les mêmes sources produisent des octets différents
La plupart des logiciels ne sont pas reproductibles par défaut, pour des raisons souvent triviales. Les compilateurs et les formats d’archive enregistrent des informations sur la machine qui les a exécutés.
- Un horodatage. Les formats
tar,aretzipenregistrent les dates de modification des fichiers. Compiler à une seconde différente modifie donc le fichier produit. - Un chemin. Les informations de débogage enregistrent le répertoire de compilation absolu. Une compilation dans
/home/alice/srcet une compilation dans/build/pkgdiffèrent donc, même si le code est identique. - Un ordre. La lecture d’un répertoire renvoie les entrées dans l’ordre du système de fichiers. La ligne d’édition de liens ou l’ordre des membres de l’archive peut donc varier d’une machine à l’autre.
- Une identité. Les scripts de compilation intègrent le nom d’utilisateur, le nom d’hôte ou la locale de la personne qui effectue la compilation.
- Une décision prise lors de la compilation. La détection des fonctionnalités du processeur ou l’initialisation aléatoire d’une valeur rend la sortie dépendante de la machine plutôt que des sources.
Vous pouvez observer la première cause en une dizaine de secondes :
mkdir -p /tmp/rb && cd /tmp/rb
echo hello > a.txt && tar cf one.tar a.txt
sleep 2
echo hello > a.txt && tar cf two.tar a.txt
sha256sum one.tar two.tarLes deux hachages diffèrent parce que l’en-tête tar enregistre la date de modification de a.txt. La réécriture du fichier a avancé cette date de deux secondes. Le contenu est identique octet par octet. Fixer les métadonnées corrige le problème :
export SOURCE_DATE_EPOCH=1700000000
tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" --owner=0 --group=0 --numeric-owner -cf three.tar a.txt
touch a.txt
tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" --owner=0 --group=0 --numeric-owner -cf four.tar a.txt
sha256sum three.tar four.tarLes hachages correspondent maintenant, car aucun champ de l’en-tête de l’archive ne provient de l’état actuel de la machine. --sort=name fixe l’ordre, --mtime fixe l’horloge et les options de propriété empêchent l’enregistrement de votre identifiant utilisateur.
Lire les différences avec diffoscope
Lorsque deux builds diffèrent, sha256sum vous indique uniquement qu’ils sont différents. Il n’indique pas pourquoi. diffoscope sert à l’expliquer dans un format lisible. Il décompresse récursivement les deux entrées, convertit les formats binaires en texte, puis compare les textes. Il prend en charge les paquets Debian, les binaires ELF, les archives tar et ZIP, les PDF, les bases de données SQLite et plus d’une centaine d’autres formats.
sudo apt install -y diffoscope
diffoscope one.tar two.tarPour la paire tar précédente, le rapport est court. Une version abrégée ressemble à ceci :
--- one.tar
+++ two.tar
├── file list
│ @@ -1 +1 @@
│ -rw-r--r-- 0/0 6 2026-08-18 10:14:02.000000 a.txt
│ +rw-r--r-- 0/0 6 2026-08-18 10:14:04.000000 a.txtLe diagnostic est complet : même taille, même chemin, mêmes permissions, mais date de modification différente. Un paquet réel produit un rapport beaucoup plus long. Enregistrez-le donc dans un fichier, puis ouvrez-le dans un navigateur :
diffoscope --html report.html build1.changes build2.changesdiffoscope renvoie 0 lorsque les entrées sont identiques, 1 lorsqu’elles diffèrent et 2 lorsqu’il rencontre un problème. Il s’intègre donc directement à une tâche CI, sans script wrapper. Sur un petit VPS, installez diffoscope-minimal plutôt que diffoscope : le paquet complet installe un grand nombre d’outils auxiliaires pour les formats que vous n’utiliserez probablement jamais.
Ce que SOURCE_DATE_EPOCH corrige et ce qu’il ne corrige pas
SOURCE_DATE_EPOCH est une variable d’environnement qui contient un nombre : la date de dernière modification des sources, exprimée en secondes depuis le 1 janvier 1970 UTC. Un outil de build qui la prend en charge utilise cette valeur partout où il demanderait normalement l’heure actuelle au système d’exploitation. Définissez-la à partir du système de gestion de versions afin que sa valeur suive les sources plutôt que le build :
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)Dans un paquet Debian, debhelper l’exporte automatiquement depuis le changelog. Pour la définir manuellement dans debian/rules :
export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -STimestamp)La prise en charge dépend de chaque outil ; elle n’est jamais globale. cmake 3.8 et les versions suivantes, gcc 7 et les versions suivantes, rpm au-delà de 4.13 et Docker buildx 0.10 et les versions suivantes la lisent. Vos propres scripts ne la lisent pas, sauf si vous les programmez pour cela. Si un script appelle date, transmettez-lui la variable :
BUILD_DATE="$(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"Une règle est importante lors de sa mise en œuvre. Si la variable est déjà définie, cette valeur est l’heure actuelle pour votre build. Ne remplacez donc jamais la valeur fournie par l’appelant.
Les images de conteneurs présentent le même problème, avec une couche d’abstraction différente. Docker buildx 0.10 et les versions suivantes transmet SOURCE_DATE_EPOCH depuis votre shell au build en tant qu’argument de build. Pour modifier les horodatages des fichiers à l’intérieur des layers, l’exporter doit les réécrire. BuildKit prend cette fonction en charge depuis la version 0.13. La forme documentée envoie le résultat vers un registry :
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
docker buildx build --output type=image,name=registry.example.com/app:1.0,push=true,rewrite-timestamp=true .Tester votre propre build avec reprotest
reprotest construit deux fois les mêmes sources et modifie volontairement l’environnement entre les deux builds, puis compare les résultats. Ces variations sont précisément l’objectif. Par défaut, il fait varier le chemin de build, l’heure, le fuseau horaire, la locale, l’umask, le hostname, l’utilisateur et le groupe, le nombre de CPU, le répertoire home et l’ordre des fichiers.
sudo apt install -y reprotest
reprotest . -- nullTout ce qui suit -- sélectionne le backend de l’environnement de build, et null désigne le système sur lequel vous êtes connecté. Ajoutez -vv -d pour conserver les répertoires temporaires afin de les inspecter, comme dans reprotest . -vv -- null -d. Utilisez reprotest auto -- null pour laisser reprotest déterminer le type d’arborescence de sources qu’il examine.
Certaines variations nécessitent des privilèges ou des paquets supplémentaires. Elles échouent explicitement si elles ne peuvent pas s’exécuter. Désactivez-les au lieu d’exécuter l’ensemble en tant que root :
reprotest --vary=-user_group,-domain_host,-fileordering auto -- nullTout ce que reprotest signale aurait été signalé plus tard, publiquement, par un rebuilder avec le nom de votre projet.
Ce que signifie le verdict d’une reconstruction
Un rebuilder est une machine qui n’appartient pas à l’éditeur. Il récupère les sources publiées et l’environnement de build enregistré, reconstruit le paquet, puis compare son résultat à l’artefact présent dans l’archive. Le verdict n’a de valeur que parce que la machine est indépendante.
Debian enregistre l’environnement dans un fichier .buildinfo que dpkg-buildpackage écrit à côté du fichier .deb. Les champs sont la partie la plus intéressante. Installed-Build-Depends liste tous les paquets installés susceptibles d’influer sur le build, avec leurs versions exactes. Build-Path indique où le build a été exécuté. Environment enregistre les variables d’environnement connues pour avoir un impact. Checksums-Sha256 décrit les résultats produits. Ce fichier sert de recette pour une seconde tentative :
sudo apt install -y devscripts mmdebstrap
debrebuild --buildresult=./artifacts --builder=mmdebstrap hello_2.10-2_amd64.buildinfodebrebuild lit le fichier buildinfo et récupère sur snapshot.debian.org les versions exactes des dépendances qu’il indique. Une reconstruction effectuée aujourd’hui peut ainsi utiliser les versions de paquets disponibles le jour du build initial. Le builder mmdebstrap n’a besoin ni d’une configuration chroot ni de droits superuser. Comparez les artefacts qu’il produit à la copie présente dans l’archive avec diffoscope.
Arch Linux utilise rebuilderd, qui effectue cette opération en continu et publie les verdicts :
rebuildctl -H https://reproducible.archlinux.org pkgs ls --name rebuilderdLes statuts sont GOOD, BAD et UNKWN. Leur interprétation littérale est incorrecte, mais pour des raisons différentes. GOOD signifie qu’une partie indépendante a obtenu les mêmes octets. C’est une indication forte sur le build, mais cela ne dit rien sur les sources. BAD est presque jamais le signe d’une attaque. La cause habituelle est un timestamp ou un chemin que le packaging n’a pas figé. C’est pourquoi rebuilderd peut joindre un rapport diffoscope à l’échec. UNKWN signifie que personne n’a effectué le test. Un paquet non testé n’est pas un paquet validé.
La règle opérationnelle est donc simple. Un verdict BAD doit vous inciter à lire le rapport. Si le rapport révèle des timestamps, des chemins de build ou un ordre différent des membres, signalez un bug de packaging. S’il montre du code exécutable différent sans fournir une telle explication, cessez de déployer ce build et faites remonter le problème.
Quelle est actuellement la reproductibilité de Debian ?
The data behind this chart
[
{
"label": "unstable",
"percent_reproducible": 94.2,
"tested_count": "41,163"
},
{
"label": "forky",
"percent_reproducible": 93.4,
"tested_count": "39,059"
},
{
"label": "experimental",
"percent_reproducible": 67.0,
"tested_count": "588"
}
]Le jour de la rédaction de cet article, unstable sur amd64 était reproductible à 94.2% pour 41,163 paquets testés. Experimental atteignait 67.0%, sur un échantillon beaucoup plus petit et beaucoup plus récent de 588 paquets. C’est normal pour des paquets dont la correction n’est pas encore terminée.
Ces chiffres proviennent de la page Debian sur tests.reproducible-builds.org, consultée le 2026-08-18, lorsque la page indiquait « Last update: 2026-08-18 16:02 UTC ». Ils évoluent. Consultez plutôt le tracker au lieu de citer ce paragraphe dans six mois.
Une réserve est plus importante que le pourcentage. Ce framework construit chaque paquet deux fois sur son propre matériel, en faisant varier l’environnement entre les deux builds, puis compare ses deux résultats. Il mesure si un paquet peut être construit de manière reproductible. Il ne vérifie pas que le .deb présent dans l’archive correspond, ce qui relève d’un rebuilder comparant le résultat à l’artifact publié. Les deux chiffres sont utiles. Ils répondent à des questions différentes, mais certains citent le premier comme s’il s’agissait du second.
Ce qu’il faut faire sur votre propre serveur
Vous n’allez pas reconstruire une distribution. Les éléments applicables à un serveur ordinaire sont plus limités et peu coûteux.
- Figez la toolchain. Une image de base référencée par un tag peut changer sans avertissement. Référencez-la par son digest et enregistrez ce digest avec la release.
- Enregistrez les entrées. Conservez le lockfile, le digest de l’image et la version du compilateur avec l’artifact. Un build dont vous ne pouvez pas reconstituer l’environnement ne peut pas être reconstruit et donc jamais vérifié.
- Effectuez deux builds dans la CI et faites échouer le job si les outputs diffèrent. Cela coûte un build supplémentaire et détecte la non-déterminisme dès son introduction, plutôt qu’un an plus tard pendant un incident.
- Supprimez les chemins intégrés par le compilateur. Pour Go,
go build -trimpath -buildvcs=falsesupprime le répertoire de build et le stamp du système de gestion de versions, tandis quego version -m ./appaffiche ce qui a réellement été intégré au binaire. - Conservez le hash de ce que vous avez déployé. Lorsque vous devez vérifier que le binaire en cours d’exécution correspond à une révision du code source, cet enregistrement est le seul élément capable de répondre.
Le contrôle de la CI tient en quatre lignes :
set -eu
./build.sh && mv dist/app app.1
./build.sh && mv dist/app app.2
diffoscope --text - app.1 app.2diffoscope renvoie un code différent de zéro lorsque les deux artifacts diffèrent. Le job échoue donc automatiquement et laisse une explication lisible dans le log. C’est toute l’idée, appliquée à un seul repository : l’affirmation qu’un binaire provient d’une arborescence source doit pouvoir être vérifiée par une seconde machine.
FAQ
Une build reproductible signifie-t-elle que le logiciel est sûr ?
Non. Elle prouve que le binaire correspond au code source, rien de plus. Une backdoor introduite dans l’arbre de code source public est générée de manière reproductible, et chaque reproducteur le confirme, car chacun a généré le même code source malveillant. Un paquet concerné par une CVE connue se reproduit parfaitement et reste vulnérable. La reproductibilité élimine une position d’attaque : la machine de build et le chemin entre le code source et le binaire. La lecture du code source et le suivi des vulnérabilités sont deux tâches distinctes que la reproductibilité n’effectue pas à votre place.
Pourquoi mes deux builds diffèrent-ils alors que le code source n’a pas changé ?
Presque toujours à cause d’un horodatage, d’un chemin ou d’un ordre. Les formats d’archive comme tar et zip enregistrent les dates de modification des fichiers. Un checkout effectué à une seconde différente produit donc des octets différents. Les informations de débogage enregistrent le répertoire de build absolu. /home/alice/src et /build/pkg produisent donc des binaires différents à partir d’un code identique. La lecture des répertoires renvoie les entrées dans l’ordre du système de fichiers. Les fichiers objet peuvent donc apparaître dans un ordre différent sur la ligne de link d’une autre machine. Exécutez diffoscope build1 build2. Le rapport indique lequel de ces problèmes est en cause au lieu de vous laisser deviner.
Qu’est-ce que SOURCE_DATE_EPOCH et dois-je le définir ?
Il s’agit d’une variable d’environnement standard qui contient un nombre : la date de dernière modification du code source, exprimée en secondes depuis le 1 January 1970 UTC. Les outils qui la prennent en charge utilisent cette valeur partout où ils liraient sinon l’horloge système. Définissez-la à partir du contrôle de version avec export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). Cette variable n’est pas automatique et ne constitue pas un correctif général. Seuls les outils qui l’implémentent la lisent. Vos propres scripts de build doivent donc aussi la lire. Un script qui appelle date continue ainsi d’enregistrer l’heure actuelle jusqu’à sa modification.
Que dois-je faire lorsqu’un rebuilder signale BAD ?
Lisez le rapport avant toute autre action. Un verdict BAD signifie qu’un rebuild indépendant n’a pas produit les mêmes octets. La cause habituelle est un comportement non déterministe du packaging, et non une attaque. rebuilderd peut générer un rapport diffoscope précisément pour cette raison. Si les différences concernent des horodatages, des chemins de build ou l’ordre des fichiers, il s’agit d’un bug de packaging qui mérite d’être signalé. Si la différence concerne du code exécutable sans explication de ce type, arrêtez de déployer ce build, conservez les artefacts et transmettez le problème à l’éditeur.