Qué demuestran realmente las compilaciones reproducibles
Una suma de comprobación confirma el archivo publicado. Una compilación reproducible demuestra que coincide con el código fuente disponible. Son afirmaciones distintas.
Qué demuestran las compilaciones reproducibles
Las compilaciones reproducibles demuestran un hecho concreto: el binario que recibió es el binario que produce exactamente este código fuente. Cualquiera puede tomar el mismo código fuente, compilarlo de nuevo y comparar los bytes. La verificación deja de ser una tarea que sólo puede realizar el editor.
El proyecto Reproducible Builds lo define así: "Una compilación es reproducible si, dado el mismo código fuente, el mismo entorno de compilación y las mismas instrucciones de compilación, cualquier parte puede recrear copias idénticas bit a bit de todos los artefactos especificados." La comparación se realiza mediante un hash. Toda la dificultad consiste en fijar el entorno y las instrucciones con suficiente precisión para que dos máquinas distintas obtengan el mismo resultado.
Por qué una suma de comprobación no responde a esta pregunta
Una suma de comprobación publicada demuestra que el archivo llegó sin corrupción. Una firma sobre esa suma de comprobación demuestra que procedía de la persona que tenía la clave. Ninguna de las dos dice nada sobre lo que ocurrió antes de que existiera el artefacto. Si la máquina de compilación del editor está comprometida, el binario malicioso se calcula y se firma exactamente igual que uno limpio, por lo que todas las comprobaciones posteriores pasan. Lo mismo ocurre si un mantenedor compila desde un árbol de trabajo que nunca se subió al repositorio.
Esa es la brecha. Puede leer el código fuente, verificar la firma y verificar la suma de comprobación, y aun así ejecutar código que nunca apareció en el repositorio. Por tanto, la reproducibilidad es un tema distinto de verificar una descarga con su suma de comprobación publicada. La suma de comprobación protege la transferencia. La recompilación protege todo lo que ocurrió antes de la transferencia.
El ataque no es teórico. El compromiso de SolarWinds Orion de 2020 se produjo exactamente en ese punto: el sistema de compilación generó artefactos firmados que no correspondían al código fuente que nadie había revisado. Todas las comprobaciones de firma pasaron porque las firmas empiezan en el artefacto.
Lo que no demuestra una compilación reproducible
Esta es la parte que suele exagerarse, así que hay que precisar los límites.
- No demuestra que el código fuente sea seguro. Una puerta trasera incorporada abiertamente en el código se compila de forma reproducible, y cada reconstrucción la confirma porque todas compilaron el mismo código fuente malicioso. La reproducibilidad desplaza el objetivo al árbol de código fuente. Alguien todavía tiene que revisar ese árbol, por lo que la política de revisión, incluidas las políticas para código asistido por IA en proyectos de código abierto, sigue siendo un control independiente.
- No demuestra que las entradas sean seguras. Las dependencias forman parte de lo que se compila. Un paquete malicioso resuelto durante la compilación se integra en el artefacto, y cada reconstrucción que resuelva la misma dependencia obtiene el mismo resultado. Así es como un ataque a la cadena de suministro de npm llega a un servidor, y una compilación reproducible lo reproduce fielmente.
- No demuestra que la cadena de herramientas sea fiable. Si el compilador está comprometido, cada reconstrucción que use ese compilador genera la misma salida comprometida, y todos los resultados coinciden. La reproducibilidad aumenta el coste de ese ataque. No lo detecta.
- No dice nada sobre las vulnerabilidades. Una biblioteca antigua reproducida bit a bit sigue siendo una biblioteca antigua con sus fallos publicados, así que mantenga la comprobación independiente de CVE conocidas en el servidor según su propio calendario.
La reproducibilidad elimina una posición concreta del atacante: la máquina de compilación y toda la ruta desde el código fuente hasta el binario. Hasta que un paquete es reproducible, nadie fuera del editor puede inspeccionar esa ruta.
Por qué el mismo código fuente produce bytes diferentes
La mayoría del software no es reproducible de forma predeterminada, y las causas suelen ser poco interesantes. Los compiladores y los formatos de archivo registran datos de la máquina donde se ejecutaron.
- Una marca de tiempo. Los formatos
tar,aryzipalmacenan las horas de modificación de los archivos, por lo que compilar en un segundo diferente cambia el archivo de salida. - Una ruta. La información de depuración registra el directorio de compilación absoluto, por lo que una compilación en
/home/alice/srcy otra en/build/pkgproducen resultados diferentes aunque el código sea idéntico. - Un orden. Leer un directorio devuelve las entradas en el orden del sistema de archivos, por lo que la línea de enlazado o el orden de los miembros del archivo cambia entre máquinas.
- Una identidad. Los scripts de compilación incorporan el nombre de usuario, el nombre de host o la configuración regional de quien realizó la compilación.
- Una decisión tomada durante la compilación. Detectar características de la CPU o inicializar algo con un valor aleatorio hace que el resultado dependa de la máquina y no del código fuente.
Puede observar cómo aparece la primera causa en unos diez segundos:
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.tarLos dos hashes son diferentes porque la cabecera de tar almacena la hora de modificación de a.txt, y al volver a escribir el archivo esa hora avanzó dos segundos. El contenido es idéntico byte a byte. Fijar los metadatos lo corrige:
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.tarAhora los hashes coinciden, porque ningún campo de la cabecera del archivo procede del estado actual de la máquina. --sort=name fija el orden, --mtime fija el reloj y las opciones de propiedad impiden que se registre el identificador de usuario.
Lectura de las diferencias con diffoscope
Cuando dos compilaciones difieren, sha256sum sólo indica que son diferentes, sin explicar nada más. diffoscope existe para explicar el motivo en términos que una persona pueda leer. Desempaqueta ambos lados de forma recursiva, convierte los formatos binarios en texto y compara el texto. Admite paquetes Debian, binarios ELF, archivos tar y ZIP, archivos PDF, bases de datos SQLite y más de cien formatos adicionales.
sudo apt install -y diffoscope
diffoscope one.tar two.tarPara el par de archivos tar anterior, el informe es breve. Una versión recortada tiene este aspecto:
--- 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.txtEse es todo el diagnóstico: mismo tamaño, misma ruta, mismos permisos y distinta hora de modificación. Un paquete real produce un informe mucho más largo, así que escríbalo en un archivo y ábralo en un navegador:
diffoscope --html report.html build1.changes build2.changesdiffoscope devuelve 0 cuando las entradas son idénticas, 1 cuando difieren y 2 cuando encuentra un problema. Por tanto, puede integrarse directamente en un trabajo de CI sin un script envoltorio. En un VPS pequeño, instale diffoscope-minimal en lugar de diffoscope: el paquete completo incorpora muchos asistentes de formato que probablemente nunca utilizará.
Qué corrige SOURCE_DATE_EPOCH y dónde deja de aplicarse
SOURCE_DATE_EPOCH es una variable de entorno que contiene un número: la hora de la última modificación del código fuente, expresada en segundos desde el 1 de enero de 1970 UTC. Una herramienta de compilación que la admite usa ese número cuando, de otro modo, consultaría la hora actual al sistema operativo. Establézcala a partir del control de versiones para que el valor siga al código fuente y no a la compilación:
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)En un paquete Debian, debhelper la exporta a partir del changelog. Para establecerla manualmente en debian/rules, use:
export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -STimestamp)La compatibilidad depende de cada herramienta y no es global. cmake 3.8 y posteriores, gcc 7 y posteriores, rpm posteriores a 4.13 y Docker buildx 0.10 y posteriores la leen. Sus propios scripts no la leen, a menos que los programe para hacerlo. Si un script llama a date, pásale la variable:
BUILD_DATE="$(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"Al implementarla, hay una regla importante. Si la variable ya está establecida, ese valor es la hora actual para la compilación. Por tanto, nunca sobrescriba el valor proporcionado por quien la invoca.
Las imágenes de contenedor tienen el mismo problema con otra capa de abstracción. Docker buildx 0.10 y posteriores pasa SOURCE_DATE_EPOCH desde el shell a la compilación como argumento de compilación. Para cambiar las marcas de tiempo de los archivos dentro de las capas, el exportador debe reescribirlas. BuildKit añadió esta capacidad en 0.13, y el formato documentado envía el resultado a un registro:
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 .Probar su propia compilación con reprotest
reprotest compila el mismo código fuente dos veces y cambia deliberadamente el entorno entre ambas compilaciones. Después compara los resultados. Las variaciones son precisamente el objetivo. De forma predeterminada, cambia la ruta de compilación, la hora, la zona horaria, la configuración regional, la máscara umask, el nombre de host, el usuario y el grupo, el número de CPU, el directorio personal y el orden de los archivos.
sudo apt install -y reprotest
reprotest . -- nullTodo lo que aparece después de -- selecciona el backend del entorno de compilación. null indica el sistema en el que se está ejecutando. Añada -vv -d para conservar los directorios temporales y poder inspeccionarlos, como en reprotest . -vv -- null -d. Use reprotest auto -- null para que determine el tipo de árbol de código fuente que está analizando.
Algunas variaciones requieren privilegios o paquetes adicionales. Si no pueden ejecutarse, muestran un error explícito. Desactívelas en lugar de ejecutar todo como root:
reprotest --vary=-user_group,-domain_host,-fileordering auto -- nullTodo lo que reprotest notifique es algo que otro encargado de reconstruir el paquete habría notificado más adelante, públicamente y con el nombre de su proyecto.
Qué significa el veredicto de un rebuilder
Un rebuilder es una máquina que no pertenece al editor. Toma el código fuente publicado y el entorno de compilación registrado, vuelve a compilar el paquete y compara su salida con el artefacto del archivo. El veredicto sólo tiene valor porque la máquina es independiente.
Debian registra el entorno en un archivo .buildinfo que dpkg-buildpackage escribe junto al archivo .deb. Los campos son la parte importante. Installed-Build-Depends enumera todos los paquetes instalados que podrían afectar a la compilación, con sus versiones exactas. Build-Path registra dónde se ejecutó la compilación. Environment registra las variables de entorno que se sabe que son relevantes. Checksums-Sha256 cubre las salidas. Ese archivo es la receta para un segundo intento:
sudo apt install -y devscripts mmdebstrap
debrebuild --buildresult=./artifacts --builder=mmdebstrap hello_2.10-2_amd64.buildinfodebrebuild lee el archivo buildinfo y obtiene de snapshot.debian.org las versiones exactas de las dependencias que especifica. Así, una recompilación actual puede usar las versiones de los paquetes que existían el día de la compilación original. El builder mmdebstrap no necesita configurar un chroot ni disponer de permisos de superusuario. Compare los artefactos que produce con la copia del archivo usando diffoscope.
Arch Linux ejecuta rebuilderd, que realiza este proceso de forma continua y publica los veredictos:
rebuildctl -H https://reproducible.archlinux.org pkgs ls --name rebuilderdLos estados son GOOD, BAD y UNKWN. La interpretación literal de cada uno es incorrecta en un sentido diferente. GOOD significa que una parte independiente obtuvo los mismos bytes. Es una afirmación sólida sobre la compilación, pero no afirma nada sobre el código fuente. BAD casi nunca indica un ataque. La causa habitual es una marca de tiempo o una ruta que el empaquetado no fijó. Por eso rebuilderd puede adjuntar un informe de diffoscope al fallo. UNKWN significa que nadie lo ha probado. Un paquete no probado no es un paquete aprobado.
Por tanto, la regla operativa es sencilla. Un veredicto BAD es un motivo para leer el informe. Si el informe muestra marcas de tiempo, rutas de compilación u ordenación de miembros, registre un error de empaquetado. Si muestra código ejecutable diferente sin una explicación de ese tipo, deje de desplegar esa compilación y escale el problema.
¿Hasta qué punto es reproducible Debian ahora?
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"
}
]El día en que se escribió esta publicación, unstable en amd64 tenía un 94.2% de compilaciones reproducibles entre los 41,163 paquetes analizados. Experimental se situaba en el 67.0%, con una muestra mucho menor y más reciente de 588 paquetes. Es lo esperado en paquetes que todavía no se han corregido por completo.
Esas cifras proceden de la página de Debian en tests.reproducible-builds.org, consultada el 2026-08-18, cuando mostraba la marca "Last update: 2026-08-18 16:02 UTC". Las cifras cambian. Consulte el tracker en lugar de citar este párrafo dentro de seis meses.
Hay una salvedad más importante que el porcentaje. Este framework compila cada paquete dos veces en su propio hardware y varía el entorno entre ambas compilaciones. Después compara sus propios resultados. Mide si un paquete puede compilarse de forma reproducible. No comprueba que el .deb almacenado en el archivo coincida. Esa es la tarea independiente de un rebuilder, que compara el resultado con el artefacto publicado. Ambas cifras son útiles. Responden a preguntas distintas, pero se cita la primera como si fuera la segunda.
Qué hacer en su propio servidor
No va a reconstruir una distribución. Las partes aplicables a un servidor normal son más pequeñas y económicas.
- Fije la cadena de herramientas. Una imagen base referenciada por etiqueta puede cambiar sin previo aviso. Referénciela por su digest y registre el digest junto con la versión.
- Registre las entradas. Guarde el lockfile, el digest de la imagen y la versión del compilador junto al artefacto. Una compilación cuyo entorno no se puede reconstruir no puede repetirse, por lo que nunca se puede verificar.
- Compile dos veces en CI y haga que el trabajo falle cuando las salidas difieran. Esto cuesta una compilación adicional y detecta la falta de determinismo el día que alguien la introduce, en lugar de un año después durante un incidente.
- Elimine las rutas que el compilador incrusta. En Go,
go build -trimpath -buildvcs=falseelimina el directorio de compilación y la marca del sistema de control de versiones, ygo version -m ./appmuestra lo que terminó realmente en el binario. - Conserve el hash de lo que implementó. Cuando necesite saber si el binario en ejecución corresponde a una revisión del código fuente, ese registro es lo único que puede responder.
La comprobación de CI ocupa cuatro líneas:
set -eu
./build.sh && mv dist/app app.1
./build.sh && mv dist/app app.2
diffoscope --text - app.1 app.2diffoscope devuelve un código distinto de cero cuando los dos artefactos difieren, por lo que el trabajo falla automáticamente y deja una explicación legible en el registro. Esa es toda la idea, aplicada a un solo repositorio: la afirmación de que un binario procede de un árbol de código fuente debería poder comprobarla una segunda máquina.
FAQ
¿Una compilación reproducible significa que el software es seguro?
No. Demuestra que el binario corresponde al código fuente, pero nada más. Una puerta trasera incorporada al árbol de código fuente público se compila de forma reproducible y todos los reconstruidores la confirman, porque todos compilaron el mismo código fuente malicioso. Un paquete con una CVE conocida se reproduce perfectamente y sigue siendo vulnerable. La reproducibilidad elimina una posible posición del atacante: la máquina de compilación y el proceso que transforma el código fuente en binario. Leer el código fuente y hacer seguimiento de las vulnerabilidades son tareas independientes que la reproducibilidad no realiza por usted.
¿Por qué difieren mis dos compilaciones si no ha cambiado nada en el código fuente?
Casi siempre se debe a una marca de tiempo, una ruta o un orden. Los formatos de archivo como tar y zip almacenan las horas de modificación de los archivos, por lo que un checkout realizado en otro segundo produce bytes diferentes. La información de depuración registra el directorio de compilación absoluto, por lo que /home/alice/src y /build/pkg producen binarios diferentes a partir de código idéntico. Las lecturas de directorios devuelven las entradas en el orden del sistema de archivos, por lo que los archivos objeto de una línea de enlace pueden quedar ordenados de forma diferente en otra máquina. Ejecute diffoscope build1 build2. El informe indicará cuál de estos casos se produce y no tendrá que adivinarlo.
¿Qué es SOURCE_DATE_EPOCH y tengo que configurarlo?
Es una variable de entorno estándar que contiene un número: la hora de la última modificación del código fuente, expresada en segundos desde el 1 de enero de 1970 UTC. Las herramientas que la admiten usan ese valor cuando, de otro modo, leerían el reloj del sistema. Establézcala desde el control de versiones con export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). No se configura automáticamente y no es una solución general. Sólo la leen las herramientas que la implementan. Además, sus propios scripts de compilación deben leerla por separado. Por tanto, un script que llama a date seguirá registrando la hora actual hasta que lo modifique.
¿Qué debo hacer si un reconstruidor informa de un resultado BAD?
Lea el informe antes de hacer cualquier otra cosa. Un veredicto BAD significa que una reconstrucción independiente no produjo los mismos bytes. La causa habitual es la falta de determinismo en el empaquetado, no un ataque. rebuilderd puede generar un informe de diffoscope precisamente por este motivo. Si las diferencias se deben a marcas de tiempo, rutas de compilación o al orden de los archivos, se trata de un error de empaquetado que conviene notificar. Si la diferencia está en el código ejecutable y no existe una explicación de ese tipo, deje de desplegar esa compilación, conserve los artefactos y escale el caso al editor.