SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-21

O que uma compilação reprodutível realmente comprova

Entenda a diferença: um checksum confirma o arquivo publicado; uma compilação reprodutível mostra que o binário corresponde ao código-fonte auditável, byte a byte.

O que as compilações reprodutíveis comprovam

As compilações reprodutíveis comprovam um facto específico: o binário que recebeu é o binário produzido exatamente por este código-fonte. Qualquer pessoa pode usar o mesmo código-fonte, compilá-lo novamente e comparar os bytes. A verificação deixa de ser algo que apenas o publicador consegue fazer.

O projeto Reproducible Builds define o conceito desta forma: "Uma compilação é reprodutível quando, dado o mesmo código-fonte, ambiente de compilação e instruções de compilação, qualquer entidade consegue recriar cópias idênticas bit a bit de todos os artefactos especificados." A comparação em si é um hash. Toda a dificuldade está em fixar o ambiente e as instruções com precisão suficiente para que duas máquinas diferentes produzam o mesmo resultado.

Por que um checksum não responde a esta pergunta

Um checksum publicado prova que o ficheiro chegou sem corrupção. Uma assinatura desse checksum prova que ele veio da pessoa que detém a chave. Nenhum dos dois diz algo sobre o que aconteceu antes de o artefacto existir. Se a máquina de compilação do editor estiver comprometida, o binário malicioso recebe um checksum e uma assinatura exatamente como um binário limpo, por isso todas as verificações posteriores passam. O mesmo acontece se um responsável compilar a partir de uma working tree que nunca foi enviada para o repositório.

Essa é a lacuna. Pode ler o código-fonte, verificar a assinatura e verificar o checksum, mas ainda assim estar a executar código que nunca apareceu no repositório. A reprodutibilidade é, portanto, um assunto diferente de verificar um download com base no checksum publicado. O checksum protege a transferência. A recompilação protege tudo o que aconteceu antes da transferência.

O ataque não é apenas teórico. O comprometimento do SolarWinds Orion em 2020 ocorreu exatamente nessa posição: o sistema de compilação produziu artefactos assinados que não correspondiam ao código-fonte analisado por ninguém. Todas as verificações de assinatura passaram, porque as assinaturas começam no artefacto.

O que uma compilação reprodutível não prova

Esta é a parte que costuma ser exagerada, por isso os limites devem ser claros.

  • Não significa que o código-fonte seja seguro. Uma backdoor incluída abertamente no código compila de forma reprodutível, e todos os responsáveis por repetir a compilação confirmam isso, porque todos compilaram o mesmo código-fonte malicioso. A reprodutibilidade desloca o alvo para a árvore do código-fonte. Alguém ainda precisa de analisar essa árvore. Por isso, a política de revisão, incluindo políticas para código assistido por IA em projetos open source, continua a ser um controlo separado.
  • Não significa que as suas entradas sejam seguras. As dependências fazem parte do que é compilado. Um pacote malicioso resolvido durante a compilação é incluído no artefacto, e todos os responsáveis por repetir a compilação que resolvam a mesma dependência chegam à mesma conclusão. É assim que um ataque à cadeia de fornecimento do npm chega a um servidor, e uma compilação reprodutível reproduz esse ataque fielmente.
  • Não significa que a toolchain seja honesta. Se o compilador estiver comprometido, todos os responsáveis por repetir a compilação que utilizem esse compilador produzem a mesma saída comprometida, e todas as verificações chegam à mesma conclusão. A reprodutibilidade aumenta o custo desse ataque. Não o deteta.
  • Não diz nada sobre vulnerabilidades. Uma biblioteca antiga reproduzida bit a bit continua a ser uma biblioteca antiga com as falhas publicadas. Por isso, mantenha a verificação do seu servidor quanto a CVEs conhecidas num calendário próprio.

A reprodutibilidade elimina uma posição específica do atacante: a máquina de compilação e todo o percurso entre o código-fonte e o binário. Até um pacote ser reprodutível, ninguém fora do publicador consegue inspecionar esse percurso.

Por que o mesmo código-fonte produz bytes diferentes

A maioria dos softwares não é reprodutível por predefinição, e as causas são simples. Os compiladores e os formatos de arquivo registam informações sobre a máquina onde são executados.

  • Um carimbo de data e hora. Os formatos tar, ar e zip armazenam as horas de modificação dos ficheiros, por isso compilar num segundo diferente altera o ficheiro de saída.
  • Um caminho. As informações de depuração registam o diretório absoluto da compilação, por isso uma compilação em /home/alice/src e outra em /build/pkg são diferentes, embora o código seja idêntico.
  • Uma ordenação. A leitura de um diretório devolve as entradas pela ordem do sistema de ficheiros, por isso a linha de ligação ou a ordem dos membros do arquivo pode variar entre máquinas.
  • Uma identidade. Os scripts de compilação incorporam o nome de utilizador, o nome do host ou a localidade de quem executou a compilação.
  • Uma decisão tomada durante a compilação. Detetar funcionalidades da CPU ou inicializar algo de forma aleatória faz com que a saída dependa da máquina, e não do código-fonte.

Pode observar a primeira causa em cerca de dez 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.tar

Os dois hashes são diferentes porque o cabeçalho tar armazena a hora de modificação de a.txt, e reescrever o ficheiro avançou essa hora em dois segundos. O conteúdo é exatamente igual byte a byte. Fixar os metadados resolve o 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.tar

Agora os hashes coincidem, porque nenhum campo do cabeçalho do arquivo é obtido do estado atual da máquina. --sort=name fixa a ordenação, --mtime fixa o relógio, e as flags de propriedade impedem que o ID do utilizador seja registado.

Lendo as diferenças com diffoscope

Quando duas compilações diferem, sha256sum informa apenas que são diferentes. Não informa mais nada. O diffoscope existe para explicar o motivo em termos que uma pessoa possa ler. A ferramenta descompacta os dois lados recursivamente, converte formatos binários em texto e compara o texto. Ela processa pacotes Debian, binários ELF, arquivos tar e ZIP, PDFs, bases de dados SQLite e mais de uma centena de outros formatos.

sudo apt install -y diffoscope
diffoscope one.tar two.tar

Para o par de arquivos tar acima, o relatório é curto. Depois de reduzido, fica assim:

--- 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.txt

Esse é o diagnóstico completo: mesmo tamanho, mesmo caminho, mesmas permissões e horários de modificação diferentes. Um pacote real produz um relatório muito mais longo. Por isso, grave o relatório num arquivo e abra-o num navegador:

diffoscope --html report.html build1.changes build2.changes

O diffoscope termina com o código 0 quando as entradas são idênticas, com 1 quando diferem e com 2 quando encontra um problema. Assim, pode ser integrado diretamente num job de CI sem um script wrapper. Num VPS pequeno, instale diffoscope-minimal em vez de diffoscope: o pacote completo instala um conjunto grande de auxiliares de formatos que provavelmente nunca irá utilizar.

O que SOURCE_DATE_EPOCH corrige e onde deixa de atuar

SOURCE_DATE_EPOCH é uma variável de ambiente que contém um número: a hora da última modificação do código-fonte, contada em segundos desde 1 January 1970 UTC. Uma ferramenta de compilação que a respeite usa esse número sempre que, de outro modo, consultaria a hora atual no sistema operativo. Defina-a a partir do controlo de versões, para que o valor acompanhe o código-fonte, e não a compilação:

export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)

Num pacote Debian, o debhelper exporta-a a partir do changelog. Para a definir manualmente em debian/rules, use:

export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -STimestamp)

O suporte é específico de cada ferramenta, nunca global. cmake 3.8 e versões posteriores, gcc 7 e versões posteriores, rpm acima de 4.13 e Docker buildx 0.10 e versões posteriores leem esta variável. Os seus próprios scripts não a leem, a menos que os prepare para isso. Se um script chamar date, passe-lhe a variável:

BUILD_DATE="$(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"

Há uma regra importante ao implementar este comportamento. Se a variável já estiver definida, esse valor é a hora atual para a sua compilação. Portanto, nunca substitua o valor fornecido pelo processo chamador.

As imagens de contentores têm o mesmo problema, mas através de outra camada. O Docker buildx 0.10 e versões posteriores passa SOURCE_DATE_EPOCH da sua shell para a compilação como um argumento de compilação. Os carimbos de data e hora dos ficheiros dentro das camadas precisam que o exportador os reescreva. O BuildKit adicionou esse suporte na versão 0.13, e o formato documentado envia o resultado para um 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 .

Testar a sua própria compilação com reprotest

O reprotest compila o mesmo código-fonte duas vezes e altera deliberadamente o ambiente entre as duas compilações. Depois, compara os resultados. As variações são o objetivo principal. Por predefinição, ele varia o caminho da compilação, a hora, o fuso horário, a localidade, o umask, o hostname, o utilizador e o grupo, o número de CPUs, o diretório home e a ordenação dos ficheiros.

sudo apt install -y reprotest
reprotest . -- null

Tudo depois de -- seleciona o backend do ambiente de compilação, e null significa o sistema onde está a executar o comando. Adicione -vv -d para manter os diretórios temporários e poder inspecioná-los, como em reprotest . -vv -- null -d. Use reprotest auto -- null para permitir que a ferramenta determine o tipo de árvore de código-fonte que está a analisar.

Algumas variações exigem privilégios ou pacotes adicionais e falham claramente quando não podem ser executadas. Desative-as em vez de executar tudo como root:

reprotest --vary=-user_group,-domain_host,-fileordering auto -- null

Tudo o que o reprotest indicar seria posteriormente indicado por um rebuilder, publicamente, com o nome do seu projeto.

O que significa o veredito de um rebuilder

Um rebuilder é uma máquina que não pertence ao publisher. Obtém o código-fonte publicado e o ambiente de build registado, compila o pacote novamente e compara o resultado com o artefacto existente no arquivo. O veredito só tem valor porque a máquina é independente.

O Debian regista o ambiente num ficheiro .buildinfo, que dpkg-buildpackage grava junto do .deb. Os campos são a parte mais importante. Installed-Build-Depends lista todos os pacotes instalados que podem afetar o build, com as versões exatas. Build-Path regista onde o build foi executado. Environment regista as variáveis de ambiente conhecidas por serem relevantes. Checksums-Sha256 abrange os resultados. Esse ficheiro é a receita para uma segunda tentativa:

sudo apt install -y devscripts mmdebstrap
debrebuild --buildresult=./artifacts --builder=mmdebstrap hello_2.10-2_amd64.buildinfo

debrebuild lê o buildinfo e obtém do snapshot.debian.org as versões exatas das dependências nele indicadas. Assim, um rebuild feito hoje pode usar as versões dos pacotes que existiam no dia do build original. O builder mmdebstrap não precisa de configurar um chroot nem de privilégios de superuser. Compare os artefactos produzidos com a cópia existente no arquivo usando diffoscope.

O Arch Linux executa o rebuilderd, que faz isto continuamente e publica os vereditos:

rebuildctl -H https://reproducible.archlinux.org pkgs ls --name rebuilderd

Os estados são GOOD, BAD e UNKWN. A interpretação literal de cada um está errada de uma forma diferente. GOOD significa que uma entidade independente obteve os mesmos bytes. Isto é uma afirmação forte sobre o build, mas não afirma nada sobre o código-fonte. BAD quase nunca indica um ataque. A causa habitual é um timestamp ou um caminho que o empacotamento não fixou. Por isso, o rebuilderd pode anexar um relatório do diffoscope à falha. UNKWN significa que ninguém o testou. Um pacote não testado não é um pacote aprovado.

A regra operacional é simples. Um veredito BAD é motivo para ler o relatório. Se o relatório mostrar timestamps, caminhos de build ou a ordenação dos membros, abra um bug de empacotamento. Se mostrar código executável diferente sem essa explicação, pare de fazer deploy dessa build e escale o caso.

Quão reproduzível está o Debian atualmente?

ChartDebian packages reproducible in the test framework, amd64
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"
  }
]

No dia em que este texto foi escrito, o unstable em amd64 apresentava 94.2% de reprodutibilidade nos 41,163 pacotes testados. O Experimental apresentava 67.0%, com uma amostra muito menor e mais recente, de 588 pacotes. Isto é esperado em pacotes que ainda não foram completamente corrigidos.

Estes valores vêm da página do Debian em tests.reproducible-builds.org, consultada em 2026-08-18, quando a página apresentava a indicação "Last update: 2026-08-18 16:02 UTC". Os valores variam. Consulte o tracker em vez de citar este parágrafo daqui a seis meses.

Há uma ressalva mais importante do que a percentagem. Este framework compila cada pacote duas vezes no seu próprio hardware e altera o ambiente entre as duas compilações. Depois, compara os dois resultados obtidos. Mede se um pacote pode ser compilado de forma reproduzível. Não verifica se o .deb presente no arquivo corresponde ao resultado esperado. Essa é a função separada de um rebuilder, que compara o resultado com o artefacto publicado. Ambos os valores são úteis. Respondem a perguntas diferentes, mas é comum citar o primeiro como se fosse o segundo.

O que fazer no seu próprio servidor

Você não vai recompilar uma distribuição. As partes que se aplicam a um servidor comum são menores e baratas.

  • Fixe a toolchain. Uma imagem base referenciada por tag pode mudar sem aviso. Referencie-a pelo digest e registe o digest junto com o release.
  • Registe as entradas. Mantenha o lockfile, o digest da imagem e a versão do compilador junto do artefacto. Uma build cujo ambiente não pode ser reconstruído não pode ser repetida, portanto nunca pode ser verificada.
  • Faça duas builds no CI e faça o job falhar quando os resultados forem diferentes. Isto custa uma build adicional e deteta a não determinismo no dia em que alguém o introduz, em vez de um ano mais tarde, durante um incidente.
  • Remova os caminhos incorporados pelo compilador. No Go, go build -trimpath -buildvcs=false remove o diretório de build e o carimbo do controlo de versões, e go version -m ./app mostra o que foi efetivamente incluído no binário.
  • Guarde o hash do que foi implementado. Quando precisar de saber se o binário em execução corresponde a uma revisão do código-fonte, esse registo é a única coisa que pode responder.

A verificação no CI tem quatro linhas:

set -eu
./build.sh && mv dist/app app.1
./build.sh && mv dist/app app.2
diffoscope --text - app.1 app.2

diffoscope termina com um código diferente de zero quando os dois artefactos diferem, portanto o job falha automaticamente e deixa uma explicação legível no log. Essa é toda a ideia, aplicada a um único repositório: a afirmação de que um binário vem de uma árvore de código-fonte deve poder ser verificada por uma segunda máquina.

FAQ

Uma compilação reproduzível significa que o software é seguro?

Não. Isto prova que o binário corresponde ao código-fonte, e nada mais. Um backdoor introduzido na árvore pública do código-fonte é compilado de forma reproduzível, e todos os rebuilders o confirmam, porque todos compilaram o mesmo código-fonte malicioso. Um pacote com um CVE conhecido é reproduzido perfeitamente e continua vulnerável. A reprodutibilidade elimina uma posição do atacante: a máquina de compilação e o percurso entre o código-fonte e o binário. Ler o código-fonte e acompanhar as vulnerabilidades são tarefas separadas que a reprodutibilidade não executa por si só.

Por que as minhas duas compilações são diferentes quando nada mudou no código-fonte?

Quase sempre por causa de um timestamp, de um caminho ou de uma ordenação. Formatos de arquivo como tar e zip armazenam as horas de modificação dos arquivos, por isso um checkout feito num segundo diferente produz bytes diferentes. As informações de depuração registram o diretório absoluto de compilação, por isso /home/alice/src e /build/pkg produzem binários diferentes a partir de código idêntico. As leituras de diretórios retornam as entradas na ordem do sistema de arquivos, por isso os arquivos objeto numa linha de link podem ficar em ordem diferente noutra máquina. Execute diffoscope build1 build2 e o relatório indicará qual destes casos ocorreu, em vez de o deixar a adivinhar.

O que é SOURCE_DATE_EPOCH e preciso de defini-lo?

É uma variável de ambiente padrão que contém um número: a última hora de modificação do código-fonte, em segundos desde 1 January 1970 UTC. As ferramentas que a suportam usam esse valor onde, de outra forma, leriam o relógio do sistema. Defina-a a partir do controlo de versões com export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). Isto não é automático nem constitui uma correção geral. Apenas as ferramentas que a implementam leem essa variável, e os seus próprios scripts de compilação também têm de a ler. Portanto, um script que chama date continua a registrar a hora atual até ser alterado.

O que devo fazer quando um rebuilder reporta BAD?

Leia o relatório antes de fazer qualquer outra coisa. Um veredito BAD significa que uma compilação independente não produziu os mesmos bytes, e a causa normal é a não determinismo no empacotamento, não um ataque. O rebuilderd pode gerar um relatório do diffoscope precisamente por esse motivo. Se as diferenças forem timestamps, caminhos de compilação ou ordenação de arquivos, isso é um erro de empacotamento que deve ser reportado. Se a diferença estiver no código executável e não houver essa explicação, pare de distribuir essa compilação, preserve os artefatos e encaminhe o caso ao publicador.

#reproducible-builds#supply-chain#diffoscope#debian#verification