Cosa dimostrano davvero le build riproducibili
Un checksum verifica il file pubblicato; una build riproducibile dimostra che il binario corrisponde al codice sorgente leggibile. Sono due verifiche diverse.
Cosa dimostrano le build riproducibili
Le build riproducibili dimostrano un fatto preciso: il binario che hai ricevuto è il binario prodotto esattamente da questo codice sorgente. Chiunque può prendere lo stesso sorgente, ricompilarlo e confrontare i byte. La verifica non è più un'operazione che solo il publisher può eseguire.
Il progetto Reproducible Builds lo definisce in questo modo: "Una build è riproducibile se, forniti lo stesso codice sorgente, lo stesso ambiente di build e le stesse istruzioni di build, qualsiasi soggetto può ricreare copie identiche bit per bit di tutti gli artefatti specificati." Il confronto consiste in un hash. La difficoltà sta nel definire l'ambiente e le istruzioni con un livello di precisione sufficiente affinché due macchine diverse producano lo stesso risultato.
Perché un checksum non risponde a questa domanda
Un checksum pubblicato dimostra che il file è arrivato senza corruzione. Una firma applicata a quel checksum dimostra che il file proviene dalla persona che detiene la chiave. Nessuno dei due fornisce informazioni su ciò che è accaduto prima che l'artefatto esistesse. Se la macchina di build del publisher è compromessa, il binario malevolo viene sottoposto a checksum e firmato esattamente come quello pulito, quindi tutti i controlli successivi vanno a buon fine. Lo stesso accade se un maintainer esegue la build da un working tree che non è mai stato sottoposto a push.
Questa è la lacuna. È possibile leggere il codice sorgente, verificare la firma, verificare il checksum e continuare a eseguire codice che non è mai comparso nel repository. La riproducibilità è quindi un argomento diverso dalla verifica di un download rispetto al checksum pubblicato. Il checksum protegge il trasferimento. La ricompilazione protegge tutto ciò che è accaduto prima del trasferimento.
L'attacco non è teorico. La compromissione di SolarWinds Orion del 2020 si è verificata esattamente in questo punto: il sistema di build ha prodotto artefatti firmati che non corrispondevano al codice sorgente esaminato da nessuno. Tutti i controlli delle firme sono andati a buon fine, perché le firme iniziano dall'artefatto.
Che cosa non dimostra una build riproducibile
Questa è la parte che viene spesso presentata in modo eccessivo, quindi è importante chiarirne i limiti.
- Non dimostra che il codice sorgente sia sicuro. Un backdoor inserito apertamente nel codice viene compilato in modo riproducibile e ogni ricompilatore lo conferma, perché tutti hanno compilato lo stesso codice sorgente ostile. La riproducibilità sposta il controllo sull'albero dei sorgenti. Qualcuno deve comunque esaminare quell'albero. Per questo la policy di revisione, comprese le policy per il codice assistito dall'AI nei progetti open source, resta un controllo separato.
- Non dimostra che gli input siano sicuri. Le dipendenze fanno parte di ciò che viene compilato. Un pacchetto malevolo risolto durante la build viene incorporato nell'artefatto e ogni ricompilatore che risolve la stessa dipendenza ottiene lo stesso risultato. È così che un attacco alla supply chain di npm raggiunge un server, e una build riproducibile lo riproduce fedelmente.
- Non dimostra che la toolchain sia affidabile. Se il compilatore è compromesso, ogni ricompilatore che usa quel compilatore genera lo stesso output compromesso e tutte le verifiche concordano. La riproducibilità aumenta il costo di questo attacco. Non lo rileva.
- Non fornisce informazioni sulle vulnerabilità. Una libreria obsoleta riprodotta bit per bit resta una libreria obsoleta con le vulnerabilità pubblicate. Per questo continua a verificare il server alla ricerca di CVE note secondo una pianificazione separata.
La riproducibilità elimina una specifica posizione dell'attaccante: la macchina di build e l'intero percorso dal codice sorgente al binario. Finché un pacchetto non è riproducibile, nessuno al di fuori del publisher può esaminare quel percorso.
Perché lo stesso codice sorgente produce byte diversi
La maggior parte del software non è riproducibile per impostazione predefinita e le cause sono poco interessanti. I compilatori e i formati di archivio registrano informazioni sulla macchina che li ha eseguiti.
- Un timestamp. I formati
tar,arezipmemorizzano gli orari di modifica dei file, quindi una compilazione eseguita in un secondo diverso modifica il file di output. - Un percorso. Le informazioni di debug registrano la directory di compilazione assoluta, quindi una compilazione in
/home/alice/srce una compilazione in/build/pkgproducono risultati diversi anche se il codice è identico. - Un ordinamento. La lettura di una directory restituisce le voci nell'ordine del filesystem, quindi la riga di linking o l'ordine dei membri dell'archivio può cambiare tra macchine diverse.
- Un'identità. Gli script di compilazione includono il nome utente, il nome host o la locale dell'utente che ha eseguito la compilazione.
- Una decisione presa durante la compilazione. Il rilevamento delle funzionalità della CPU o l'inizializzazione casuale di un componente rende l'output dipendente dalla macchina anziché dal codice sorgente.
Puoi osservare la prima causa in circa dieci secondi:
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.tarI due hash sono diversi perché l'header tar memorizza l'orario di modifica di a.txt e la riscrittura del file ha fatto avanzare questo orario di due secondi. Il contenuto è identico byte per byte. Fissare i metadati risolve il problema:
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.tarOra gli hash coincidono, perché nessun campo dell'header dell'archivio proviene dallo stato corrente della macchina. --sort=name fissa l'ordinamento, --mtime fissa l'orologio e i flag di ownership impediscono la registrazione dell'ID utente.
Lettura delle differenze con diffoscope
Quando due build differiscono, sha256sum indica soltanto che sono diverse. Non fornisce altre informazioni. diffoscope serve a spiegare il motivo della differenza in termini leggibili. Estrae ricorsivamente entrambi gli elementi, converte i formati binari in testo e confronta il testo. Gestisce pacchetti Debian, binari ELF, archivi tar e ZIP, PDF, database SQLite e oltre cento altri formati.
sudo apt install -y diffoscope
diffoscope one.tar two.tarPer la coppia tar precedente, il report è breve. In forma ridotta, è simile al seguente:
--- 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.txtQuesta è l'intera diagnosi: stessa dimensione, stesso percorso, stessi permessi, orario di modifica diverso. Un pacchetto reale produce un report molto più lungo. Scrivilo quindi su file e aprilo in un browser:
diffoscope --html report.html build1.changes build2.changesdiffoscope restituisce 0 quando gli input sono identici, 1 quando differiscono e 2 quando rileva un problema. Può quindi essere inserito direttamente in un job CI, senza uno script wrapper. Su un VPS di piccole dimensioni, installa diffoscope-minimal invece di diffoscope: il pacchetto completo installa numerosi helper per i formati, che probabilmente non utilizzerai mai.
Che cosa corregge SOURCE_DATE_EPOCH e dove non è sufficiente
SOURCE_DATE_EPOCH è una variabile d'ambiente che contiene un numero: l'ora dell'ultima modifica del codice sorgente, espressa in secondi dal 1 gennaio 1970 UTC. Uno strumento di build che la supporta usa quel valore ogni volta che altrimenti chiederebbe al sistema operativo l'ora corrente. Impostala usando il sistema di controllo versione, in modo che il valore segua il codice sorgente e non la build:
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)In un pacchetto Debian, debhelper la esporta automaticamente dal changelog. Per impostarla manualmente in debian/rules:
export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -STimestamp)Il supporto dipende dal singolo strumento e non è globale. cmake 3.8 e versioni successive, gcc 7 e versioni successive, rpm oltre la 4.13 e Docker buildx 0.10 e versioni successive la leggono. I tuoi script non la leggono, a meno che non siano scritti per farlo. Se uno script chiama date, passagli la variabile:
BUILD_DATE="$(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"Quando implementi questo comportamento, devi rispettare una regola. Se la variabile è già impostata, quel valore rappresenta l'ora corrente per la build. Non sovrascrivere mai il valore fornito dal chiamante.
Le immagini dei container presentano lo stesso problema attraverso un livello di astrazione diverso. Docker buildx 0.10 e versioni successive trasferisce SOURCE_DATE_EPOCH dalla shell alla build come argomento di build. Per modificare i timestamp dei file contenuti nei layer, l'exporter deve riscriverli. BuildKit ha aggiunto questa funzione nella versione 0.13. La forma documentata invia il risultato a 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 .Verificare la propria build con reprotest
reprotest compila due volte lo stesso codice sorgente e modifica intenzionalmente l'ambiente tra una compilazione e l'altra, quindi confronta i risultati. Le variazioni sono lo scopo principale dello strumento. Per impostazione predefinita, varia il percorso di compilazione, l'ora, il fuso orario, la localizzazione, la umask, il nome host, l'utente e il gruppo, il numero di CPU, la directory home e l'ordinamento dei file.
sudo apt install -y reprotest
reprotest . -- nullTutto ciò che segue -- seleziona il backend dell'ambiente di compilazione, mentre null indica il sistema in uso. Aggiungere -vv -d per conservare le directory temporanee e poterle esaminare, come in reprotest . -vv -- null -d. Usare reprotest auto -- null per consentire allo strumento di determinare il tipo di albero dei sorgenti analizzato.
Alcune variazioni richiedono privilegi o pacchetti aggiuntivi e producono un errore esplicito quando non possono essere applicate. Disabilitarle invece di eseguire l'intera procedura come root:
reprotest --vary=-user_group,-domain_host,-fileordering auto -- nullOgni elemento segnalato da reprotest sarebbe stato segnalato in seguito da chi ricompila il progetto, pubblicamente e con il nome del progetto associato.
Che cosa significa il verdetto di un rebuilder
Un rebuilder è una macchina che non appartiene al publisher. Riceve il codice sorgente pubblicato e l’ambiente di build registrato, ricompila il pacchetto e confronta il proprio output con l’artefatto presente nell’archivio. Il verdetto ha valore solo perché la macchina è indipendente.
Debian registra l’ambiente in un file .buildinfo che dpkg-buildpackage scrive accanto al file .deb. I campi sono la parte più importante. Installed-Build-Depends elenca tutti i pacchetti installati che possono influire sulla build, con le versioni esatte. Build-Path registra il percorso in cui è stata eseguita la build. Environment registra le variabili d’ambiente note per essere rilevanti. Checksums-Sha256 riguarda gli output. Quel file è la ricetta per un secondo tentativo:
sudo apt install -y devscripts mmdebstrap
debrebuild --buildresult=./artifacts --builder=mmdebstrap hello_2.10-2_amd64.buildinfodebrebuild legge il file buildinfo e recupera da snapshot.debian.org le versioni esatte delle dipendenze indicate, così una rebuild eseguita oggi può usare le versioni dei pacchetti disponibili il giorno della build originale. Il builder mmdebstrap non richiede una configurazione chroot né privilegi da superuser. Confronta gli artefatti prodotti con la copia nell’archivio usando diffoscope.
Arch Linux esegue rebuilderd, che svolge continuamente questa attività e pubblica i verdetti:
rebuildctl -H https://reproducible.archlinux.org pkgs ls --name rebuilderdGli stati sono GOOD, BAD e UNKWN, ma il significato letterale di ciascuno può essere interpretato in modo errato. GOOD significa che un soggetto indipendente ha ottenuto gli stessi byte. È un’indicazione forte sulla build, ma non dice nulla sul codice sorgente. BAD è quasi mai un attacco: nella maggior parte dei casi la causa è un timestamp o un percorso che il packaging non ha fissato. Per questo rebuilderd può allegare un report di diffoscope al fallimento. UNKWN significa che nessuno ha eseguito il test. Un pacchetto non testato non è un pacchetto con esito positivo.
La regola operativa è quindi semplice. Un verdetto BAD richiede la lettura del report. Se il report mostra timestamp, percorsi di build o un ordinamento diverso dei membri, apri un bug relativo al packaging. Se mostra codice eseguibile diverso senza una spiegazione di questo tipo, interrompi il deployment di quella build e fai escalation.
Quanto è riproducibile Debian in questo momento?
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"
}
]Nel giorno in cui è stato scritto questo articolo, unstable su amd64 risultava riproducibile al 94.2% nei 41,163 pacchetti testati. Experimental si attestava al 67.0%, su un campione molto più piccolo e molto più recente di 588 pacchetti, come è normale per pacchetti la cui correzione non è ancora terminata.
Questi dati provengono dalla pagina Debian su tests.reproducible-builds.org, consultata il 2026-08-18, quando riportava l'indicazione "Last update: 2026-08-18 16:02 UTC". I valori cambiano. Tra sei mesi consultate il tracker invece di citare questo paragrafo.
Una precisazione è più importante della percentuale. Questo framework compila ogni pacchetto due volte sul proprio hardware, variando l'ambiente tra le due compilazioni, e confronta i due risultati ottenuti. Misura se un pacchetto può essere compilato in modo riproducibile. Non verifica che il .deb presente nell'archivio corrisponda al risultato pubblicato: questo è il compito separato di un rebuilder, che confronta il risultato con l'artefatto pubblicato. Entrambi i valori sono utili. Rispondono a domande diverse, ma spesso si cita il primo come se fosse il secondo.
Cosa fare sul proprio server
Non è necessario ricostruire una distribuzione. Le procedure applicabili a un normale server sono più semplici e richiedono poche risorse.
- Fissare la toolchain. Un'immagine di base referenziata tramite tag può cambiare senza preavviso. Referenziarla tramite digest e registrare il digest insieme alla release.
- Registrare gli input. Conservare il lockfile, il digest dell'immagine e la versione del compilatore insieme all'artefatto. Una build il cui ambiente non può essere ricostruito non può essere ripetuta e quindi non può essere verificata.
- Eseguire due build in CI e interrompere il job quando gli output differiscono. Questa operazione richiede una build aggiuntiva e rileva la non determinismo nel momento in cui viene introdotto, invece di farlo un anno dopo durante un incidente.
- Rimuovere i percorsi incorporati dal compilatore. Per Go,
go build -trimpath -buildvcs=falserimuove la directory di build e il contrassegno del sistema di controllo versione;go version -m ./appmostra invece ciò che è stato effettivamente inserito nel binario. - Conservare l'hash di ciò che è stato distribuito. Quando occorre verificare se il binario in esecuzione corrisponde a una revisione del codice sorgente, questo record è l'unico elemento in grado di fornire una risposta.
Il controllo CI richiede quattro righe:
set -eu
./build.sh && mv dist/app app.1
./build.sh && mv dist/app app.2
diffoscope --text - app.1 app.2diffoscope restituisce un codice diverso da zero quando i due artefatti differiscono. Il job fallisce quindi automaticamente e lascia nel log una spiegazione leggibile. Questo è il concetto completo, applicato a un solo repository: l'affermazione che un binario provenga da un albero del codice sorgente deve poter essere verificata da una seconda macchina.
FAQ
Una build riproducibile significa che il software è sicuro?
No. Dimostra che il binario corrisponde al codice sorgente, e nulla di più. Una backdoor inserita nel repository pubblico del codice sorgente viene compilata in modo riproducibile e ogni ricompilatore la conferma, perché tutti hanno compilato lo stesso codice sorgente ostile. Un pacchetto con una CVE nota viene riprodotto perfettamente e rimane vulnerabile. La riproducibilità elimina una possibile posizione dell'attaccante: la macchina di build e il percorso dal codice sorgente al binario. L'analisi del codice sorgente e il monitoraggio delle vulnerabilità sono attività separate che la riproducibilità non esegue al posto tuo.
Perché le mie due build sono diverse se il codice sorgente non è cambiato?
Quasi sempre la causa è un timestamp, un percorso o un ordinamento. I formati di archivio come tar e zip memorizzano gli orari di modifica dei file, quindi un checkout eseguito in un secondo diverso produce byte diversi. Le informazioni di debug registrano la directory di build assoluta, quindi /home/alice/src e /build/pkg producono binari diversi a partire da codice identico. Le letture delle directory restituiscono le voci nell'ordine del filesystem, quindi i file oggetto nella riga di linking possono avere un ordine diverso su un'altra macchina. Esegui diffoscope build1 build2: il report indica quale di questi elementi è la causa, senza lasciarti procedere per tentativi.
Che cos'è SOURCE_DATE_EPOCH e devo impostarlo?
È una variabile d'ambiente standard che contiene un solo numero: l'ora dell'ultima modifica del codice sorgente, espressa in secondi dal 1 gennaio 1970 UTC. Gli strumenti che la supportano usano quel valore ogni volta che altrimenti leggerebbero l'orologio di sistema. Impostala dal sistema di controllo versione con export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). Non viene impostata automaticamente e non costituisce una soluzione generale. La leggono solo gli strumenti che la implementano, e i tuoi script di build devono leggerla esplicitamente; quindi uno script che chiama date continua a registrare l'ora corrente finché non lo modifichi.
Cosa devo fare quando un ricompilatore segnala BAD?
Leggi il report prima di fare qualsiasi altra cosa. Un verdetto BAD significa che una ricompilazione indipendente non ha prodotto gli stessi byte; la causa ordinaria è una build non deterministica nel processo di packaging, non un attacco. rebuilderd può generare un report di diffoscope proprio per questo motivo. Se le differenze riguardano timestamp, percorsi di build o ordinamento dei file, si tratta di un bug di packaging che conviene segnalare. Se la differenza riguarda codice eseguibile e non esiste una spiegazione di questo tipo, interrompi la distribuzione di quella build, conserva gli artefatti e inoltra il problema al publisher.