SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor

Czym są powtarzalne kompilacje i co faktycznie dowodzą

Suma kontrolna potwierdza jedynie integralność pliku, natomiast powtarzalne kompilacje gwarantują zgodność binariów z kodem źródłowym. Poznaj różnice między tymi pojęciami.

Co potwierdzają powtarzalne kompilacje

Powtarzalne kompilacje potwierdzają jeden konkretny fakt: otrzymany plik binarny jest tym samym plikiem, który powstaje z danego kodu źródłowego. Każdy może pobrać ten sam kod, skompilować go ponownie i porównać bajty. Weryfikacja przestaje być czynnością dostępną wyłącznie dla wydawcy.

Projekt Reproducible Builds definiuje to w następujący sposób: „Kompilacja jest powtarzalna, jeśli przy użyciu tego samego kodu źródłowego, środowiska budowania oraz instrukcji kompilacji, każda ze stron może odtworzyć identyczne co do bitu kopie wszystkich określonych artefaktów”. Samo porównanie opiera się na sumie kontrolnej. Cała trudność polega na wystarczająco precyzyjnym określeniu środowiska i instrukcji, aby dwie różne maszyny wygenerowały zgodny wynik.

Dlaczego suma kontrolna nie odpowiada na to pytanie

Opublikowana suma kontrolna dowodzi, że pobrany plik jest zgodny z wartością referencyjną, co oznacza, że dotarł bez uszkodzeń. Nie mówi ona jednak nic o tym, kto wyprodukował dany plik. Przewodnik weryfikacji Apache wprost odnosi się do skrótów: „Nie zapewniają one żadnych gwarancji co do autentyczności pliku”. Referencyjna suma kontrolna musi pochodzić z zaufanego źródła, w przeciwnym razie jej zgodność niczego nie dowodzi. Podpis cyfrowy złożony na sumie kontrolnej potwierdza, że pochodzi ona od osoby posiadającej klucz, pod warunkiem wcześniejszego sprawdzenia, czy klucz faktycznie należy do wydawcy. Żaden z tych mechanizmów nie informuje o tym, co działo się przed powstaniem artefaktu. Jeśli maszyna budująca wydawcy zostanie przejęta, złośliwy plik binarny otrzyma sumę kontrolną i podpis identyczny jak w przypadku czystej wersji, dzięki czemu każda weryfikacja w dalszym etapie przebiegnie pomyślnie. To samo dotyczy sytuacji, w której opiekun projektu buduje plik z drzewa roboczego, które nigdy nie zostało wypchnięte do repozytorium.

Na tym polega luka. Można przejrzeć kod źródłowy, zweryfikować podpis i sumę kontrolną, a mimo to uruchamiać kod, który nigdy nie pojawił się w repozytorium. Powtarzalność kompilacji jest zatem zagadnieniem odrębnym od weryfikacji pobranego pliku względem opublikowanej sumy kontrolnej. Suma kontrolna chroni proces przesyłania. Powtórna kompilacja chroni wszystko, co wydarzyło się przed przesłaniem.

Ten atak nie jest teoretyczny. Przejęcie SolarWinds Orion w 2020 roku miało miejsce dokładnie w tym punkcie: system budowania wygenerował podpisane artefakty, które nie odpowiadały kodowi źródłowemu, który ktokolwiek zweryfikował. Każda kontrola podpisu przebiegła pomyślnie, ponieważ podpisy są weryfikowane dopiero na etapie gotowego artefaktu.

Czego nie dowodzi powtarzalna kompilacja

To zagadnienie jest często wyolbrzymiane, dlatego należy precyzyjnie określić jego ograniczenia.

  • Nie oznacza to, że kod źródłowy jest bezpieczny. Backdoor wprowadzony jawnie kompiluje się w sposób powtarzalny, a każdy, kto przeprowadzi ponowną kompilację, potwierdzi ten fakt, ponieważ wszyscy budują ten sam szkodliwy kod. Powtarzalność przenosi punkt ciężkości na drzewo źródeł. Ktoś nadal musi dokonać przeglądu tego drzewa, dlatego polityka przeglądu kodu, w tym zasady dotyczące kodu wspomaganego przez AI w projektach open source, pozostaje odrębnym mechanizmem kontrolnym.
  • Nie oznacza to, że dane wejściowe są bezpieczne. Zależności stanowią część procesu budowania. Złośliwy pakiet pobrany w czasie kompilacji zostaje skompilowany do postaci artefaktu, a każdy, kto przeprowadzi ponowną kompilację z tą samą zależnością, potwierdzi poprawność procesu. W ten sposób atak na łańcuch dostaw npm dociera do serwera, a powtarzalna kompilacja wiernie go odtwarza.
  • Nie oznacza to, że łańcuch narzędzi jest uczciwy. Jeśli kompilator jest zainfekowany, każdy, kto przeprowadzi ponowną kompilację przy użyciu tego kompilatora, wygeneruje ten sam zainfekowany wynik, a wszystkie werdykty będą zgodne. Powtarzalność zwiększa koszt takiego ataku, ale go nie wykrywa.
  • Nie mówi nic o podatnościach. Stara biblioteka, odtworzona bit po bicie, nadal pozostaje starą biblioteką z opublikowanymi błędami, dlatego należy sprawdzać serwer pod kątem znanych CVE zgodnie z własnym harmonogramem.

Powtarzalność eliminuje jedną konkretną pozycję atakującego: maszynę budującą oraz całą ścieżkę od kodu źródłowego do pliku binarnego. Dopóki pakiet nie jest powtarzalny, nikt poza wydawcą nie może w ogóle zweryfikować tej ścieżki.

Dlaczego to samo źródło generuje różne bajty

Większość oprogramowania domyślnie nie jest odtwarzalna, a przyczyny tego stanu rzeczy są prozaiczne. Kompilatory oraz formaty archiwów zapisują informacje o maszynie, na której zostały uruchomione.

  • Znacznik czasu. Formaty tar, ar oraz zip przechowują czas modyfikacji plików, więc budowanie w innej sekundzie zmienia plik wyjściowy.
  • Ścieżka. Informacje debugowania zapisują bezwzględną ścieżkę katalogu budowania, więc kompilacja w /home/alice/src i w /build/pkg różni się, mimo że kod jest identyczny.
  • Kolejność. Odczyt katalogu zwraca wpisy w kolejności zależnej od systemu plików, więc kolejność linkowania lub elementów w archiwum zmienia się między maszynami.
  • Tożsamość. Skrypty budowania osadzają nazwę użytkownika, nazwę hosta lub ustawienia regionalne osoby budującej pakiet.
  • Decyzja podjęta w czasie budowania. Wykrywanie funkcji procesora lub losowe inicjowanie danych sprawia, że wynik zależy od maszyny, a nie tylko od kodu źródłowego.

Pierwszą przyczynę można zaobserwować w około dziesięć sekund:

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

Oba hasze różnią się, ponieważ nagłówek tar przechowuje czas modyfikacji a.txt, a nadpisanie pliku przesunęło ten czas o dwie sekundy do przodu. Zawartość jest identyczna co do bajta. Ustalenie metadanych rozwiązuje ten problem:

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

Teraz hasze są zgodne, ponieważ żadne pole w nagłówku archiwum nie pochodzi z bieżącego stanu maszyny. --sort=name naprawia kolejność, --mtime naprawia zegar, a flagi własności zapobiegają zapisaniu identyfikatora użytkownika.

Analiza różnic za pomocą diffoscope

Gdy dwa buildy różnią się od siebie, sha256sum informuje jedynie o samym fakcie wystąpienia różnicy. Narzędzie diffoscope służy do wyjaśnienia przyczyn w sposób czytelny dla człowieka. Rozpakowuje ono rekurencyjnie oba obiekty, konwertuje formaty binarne na tekst i porównuje wynik. Obsługuje pakiety Debian, pliki binarne ELF, archiwa tar i ZIP, pliki PDF, bazy danych SQLite oraz dziesiątki innych formatów.

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

Dla wspomnianej wcześniej pary archiwów tar raport jest krótki. W wersji skróconej wygląda następująco:

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

To pełna diagnoza: ten sam rozmiar, ta sama ścieżka, te same uprawnienia, różny czas modyfikacji. Rzeczywisty pakiet generuje znacznie dłuższy raport, dlatego należy zapisać go do pliku i otworzyć w przeglądarce:

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

Narzędzie diffoscope zwraca kod 0, gdy dane wejściowe są identyczne, 1, gdy się różnią, oraz 2 w przypadku wystąpienia problemów, dzięki czemu można je bezpośrednio wdrożyć w zadaniach CI bez dodatkowych skryptów opakowujących. Na małym serwerze VPS należy zainstalować diffoscope-minimal zamiast diffoscope: pełny pakiet pobiera duży zestaw pomocników formatów, które prawdopodobnie nigdy nie zostaną użyte.

Co naprawia SOURCE_DATE_EPOCH i gdzie kończy się jego działanie

SOURCE_DATE_EPOCH to zmienna środowiskowa przechowująca jedną liczbę: czas ostatniej modyfikacji źródła, liczony w sekundach od 1 stycznia 1970 roku w formacie UTC. Narzędzie budujące, które uwzględnia tę zmienną, używa tej liczby wszędzie tam, gdzie w przeciwnym razie odpytywałoby system operacyjny o bieżący czas. Należy ustawić ją na podstawie systemu kontroli wersji, aby wartość podążała za źródłem, a nie za procesem budowania:

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

W pakietach Debian narzędzie debhelper eksportuje tę wartość automatycznie z pliku changelog. Ręczne ustawienie w debian/rules wygląda następująco:

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

Wsparcie zależy od konkretnego narzędzia i nie jest globalne. Obsługują ją cmake w wersji 3.8 i nowszych, gcc w wersji 7 i nowszych, rpm powyżej 4.13 oraz Docker buildx w wersji 0.10 i nowszych. Własne skrypty nie odczytują jej automatycznie, chyba że zostaną do tego przystosowane. Jeśli skrypt wywołuje date, należy przekazać mu tę zmienną:

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

Podczas implementacji obowiązuje jedna kluczowa zasada. Jeśli zmienna jest już ustawiona, jej wartość jest bieżącym czasem dla procesu budowania. Należy używać jej wszędzie tam, gdzie odczytywany byłby zegar systemowy, oraz pozostawić ją ustawioną dla każdego procesu uruchamianego w ramach budowania. Specyfikacja określa to jasno: "Procesy budowania NIE MOGĄ usuwać tej zmiennej dla procesów potomnych, jeśli jest ona już obecna".

Obrazy kontenerów napotykają ten sam problem w innej formie. Docker buildx w wersji 0.10 i nowszych przekazuje SOURCE_DATE_EPOCH z powłoki do procesu budowania jako argument budowania. Znaczniki czasu plików wewnątrz warstw wymagają nadpisania przez eksporter, co BuildKit wprowadził w wersji 0.13, a udokumentowana metoda przesyła wynik do rejestru:

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 .

Testowanie własnej kompilacji za pomocą reprotest

Narzędzie reprotest buduje to samo źródło dwukrotnie, celowo zmieniając środowisko między kompilacjami, a następnie porównuje wyniki. Zmienność jest kluczowym założeniem tego procesu. Domyślnie narzędzie modyfikuje zmienne środowiskowe, ścieżkę budowania, wersję jądra, randomizację przestrzeni adresowej, liczbę procesorów, czas, użytkownika i grupę, kolejność plików, nazwę domeny i hosta, katalog domowy, ustawienia regionalne (locale), ścieżkę plików wykonywalnych, strefę czasową oraz umask.

sudo apt install -y reprotest
reprotest . -- null

Wszystko po -- określa backend środowiska budowania, a null oznacza system, w którym aktualnie pracujesz. Dodaj -vv -d, aby zachować tymczasowe katalogi do inspekcji, tak jak w reprotest . -vv -- null -d. Użyj reprotest auto -- null, aby pozwolić narzędziu na automatyczne rozpoznanie typu drzewa źródłowego.

Niektóre warianty wymagają uprawnień lub dodatkowych pakietów i kończą się wyraźnym błędem, gdy nie mogą zostać uruchomione. Zamiast uruchamiać całość z uprawnieniami root, należy je wyłączyć:

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

Wszystko, co raportuje reprotest, jest informacją, którą osoba przeprowadzająca ponowną kompilację (rebuilder) zgłosiłaby później publicznie, przypisując ją do nazwy Twojego projektu.

Co oznacza werdykt rebuilder

Rebuilder to maszyna inna niż ta należąca do wydawcy. Pobiera ona opublikowane źródła oraz zarejestrowane środowisko budowania, ponownie kompiluje pakiet i porównuje własny wynik z artefaktem znajdującym się w archiwum. Werdykt jest wartościowy tylko dlatego, że maszyna ta jest niezależna.

Debian rejestruje środowisko w pliku .buildinfo, który dpkg-buildpackage zapisuje obok .deb. Najistotniejszą częścią są pola. Installed-Build-Depends zawiera listę każdego zainstalowanego pakietu, który mógł wpłynąć na proces budowania, wraz z dokładnymi wersjami. Build-Path rejestruje miejsce, w którym przeprowadzono budowanie. Environment rejestruje zmienne środowiskowe, które mają znaczenie. Checksums-Sha256 obejmuje dane wyjściowe. Ten plik stanowi przepis na drugą próbę:

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

debrebuild odczytuje plik buildinfo i pobiera dokładne wersje zależności wymienione w nim z serwisu snapshot.debian.org, dzięki czemu ponowne budowanie dzisiaj może wykorzystać wersje pakietów, które istniały w dniu oryginalnego budowania. Builder mmdebstrap nie wymaga konfiguracji chroot ani uprawnień superużytkownika. Porównaj artefakty, które wytwarza, z kopią w archiwum za pomocą narzędzia diffoscope.

Arch Linux uruchamia rebuilderd, który wykonuje to zadanie w sposób ciągły i publikuje werdykty:

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

Statusy to GOOD, BAD oraz UNKWN, a dosłowne odczytanie każdego z nich prowadzi do błędnych wniosków. GOOD oznacza, że niezależna strona uzyskała identyczne bajty, co jest silnym stwierdzeniem na temat procesu budowania, ale nie mówi nic o samym źródle. BAD prawie nigdy nie oznacza ataku: zazwyczaj przyczyną jest znacznik czasu lub ścieżka, których pakujący nie zablokował, dlatego rebuilderd może dołączyć raport diffoscope do błędu. UNKWN oznacza, że nikt nie przeprowadził testu, a nieprzetestowany pakiet nie jest pakietem poprawnym.

Zasada operacyjna jest zatem krótka. Werdykt BAD jest powodem do zapoznania się z raportem. Jeśli raport wskazuje na znaczniki czasu, ścieżki budowania lub kolejność elementów, należy zgłosić błąd pakowania. Jeśli raport wykazuje inny kod wykonywalny bez takiego wyjaśnienia, należy wstrzymać wdrażanie tej kompilacji i eskalować problem.

Jaki jest obecny poziom powtarzalności kompilacji w Debianie?

WykresDebian packages reproducible in the test framework, amd64
Dane, na których opiera się ten wykres
[
  {
    "label": "unstable",
    "percent_reproducible": 94.8,
    "tested_count": "41,490"
  },
  {
    "label": "forky",
    "percent_reproducible": 95.7,
    "tested_count": "39,529"
  },
  {
    "label": "experimental",
    "percent_reproducible": 72.5,
    "tested_count": "619"
  }
]

W dniu 2026-10-15, w gałęzi unstable dla architektury amd64 poziom powtarzalności wynosił 94.8% dla 41,490 przetestowanych pakietów. W gałęzi experimental wskaźnik ten wynosił 72.5% dla znacznie mniejszej i nowszej próbki 619 pakietów, co jest typowe dla oprogramowania, którego proces naprawy jeszcze się nie zakończył.

Powyższe dane pochodzą ze strony Debiana w serwisie tests.reproducible-builds.org, odczytane w dniu 2026-10-15, kiedy to strona zawierała adnotację „Last update: 2026-10-15 13:02 UTC”. Wartości te ulegają zmianie. Należy sprawdzać tracker zamiast cytować ten akapit po sześciu miesiącach.

Istotniejsze od samego odsetka jest jedno zastrzeżenie. Niniejszy mechanizm buduje każdy pakiet dwukrotnie na własnym sprzęcie, zmieniając środowisko między kompilacjami, a następnie porównuje oba wyniki. Mierzy on, czy pakiet może zostać zbudowany w sposób powtarzalny. Nie jest to weryfikacja, czy plik .deb znajdujący się w archiwum jest zgodny z oryginałem; to zadanie należy do systemu rebuilder, który porównuje wynik z opublikowanym artefaktem. Obie wartości są użyteczne. Odpowiadają one na różne pytania, jednak często cytuje się pierwszą z nich, traktując ją jako drugą.

Co należy zrobić na własnym serwerze

Nie ma potrzeby przebudowywania dystrybucji. Elementy, które można przenieść na zwykły serwer, są niewielkie i tanie.

  • Przypnij łańcuch narzędzi (toolchain). Obraz bazowy, do którego odwołujesz się za pomocą tagu, może zmienić się bez ostrzeżenia. Odwołuj się do niego za pomocą skrótu (digest) i zapisuj ten skrót wraz z wydaniem.
  • Rejestruj dane wejściowe. Przechowuj plik blokady (lockfile), skrót obrazu oraz wersję kompilatora obok artefaktu. Kompilacji, której środowiska nie da się odtworzyć, nie można powtórzyć, co uniemożliwia jej weryfikację.
  • Buduj dwukrotnie w CI i przerywaj zadanie, jeśli wyniki się różnią. Kosztuje to jedną dodatkową kompilację, ale pozwala wykryć niedeterminizm w dniu jego wprowadzenia, a nie rok później podczas awarii.
  • Usuwaj ścieżki osadzane przez kompilator. W przypadku Go, go build -trimpath -buildvcs=false usuwa katalog budowania oraz znacznik kontroli wersji, a go version -m ./app wyświetla to, co faktycznie znalazło się w pliku binarnym.
  • Przechowuj skrót tego, co zostało wdrożone. Gdy zajdzie potrzeba sprawdzenia, czy uruchomiony plik binarny odpowiada danej wersji źródłowej, ten zapis jest jedynym źródłem odpowiedzi.

Sprawdzenie w CI składa się z czterech linii:

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

Narzędzie diffoscope kończy działanie z kodem wyjścia innym niż zero, gdy dwa artefakty różnią się od siebie, więc zadanie przerywa się automatycznie i pozostawia czytelne wyjaśnienie w dzienniku. Na tym polega cała idea, przeskalowana do jednego repozytorium: twierdzenie, że plik binarny pochodzi z określonego drzewa źródeł, powinno być możliwe do zweryfikowania przez drugą maszynę.

FAQ

Czy powtarzalna kompilacja oznacza, że oprogramowanie jest bezpieczne?

Nie. Dowodzi ona jedynie, że plik binarny odpowiada kodowi źródłowemu. Tylna furtka wprowadzona do publicznego drzewa źródeł skompiluje się w sposób powtarzalny, a każdy system budujący potwierdzi ten fakt, ponieważ każdy z nich przetworzył ten sam szkodliwy kod. Pakiet z wykrytą luką CVE kompiluje się idealnie i pozostaje podatny na ataki. Powtarzalność eliminuje jeden wektor ataku: możliwość manipulacji na maszynie budującej oraz na ścieżce od kodu źródłowego do pliku binarnego. Analiza kodu źródłowego i śledzenie podatności to odrębne zadania, których powtarzalność nie wykonuje za użytkownika.

Dlaczego dwa procesy budowania dają różne wyniki, mimo że kod źródłowy się nie zmienił?

Prawie zawsze przyczyną jest znacznik czasu, ścieżka lub kolejność. Formaty archiwów, takie jak tar i zip, przechowują czasy modyfikacji plików, więc pobranie kodu w innym momencie spowoduje wygenerowanie innych bajtów. Informacje debugowania rejestrują bezwzględną ścieżkę budowania, dlatego /home/alice/src i /build/pkg wygenerują różne pliki binarne z identycznego kodu. Odczyt katalogów zwraca wpisy w kolejności zależnej od systemu plików, więc pliki obiektowe w linii konsolidacji mogą być uporządkowane inaczej na innej maszynie. Uruchom diffoscope build1 build2, a raport wskaże przyczynę, zamiast pozostawiać pole do domysłów.

Czym jest SOURCE_DATE_EPOCH i czy muszę to ustawiać?

Jest to standardowa zmienna środowiskowa przechowująca jedną liczbę: czas ostatniej modyfikacji źródła w sekundach od 1 stycznia 1970 roku UTC. Narzędzia, które ją obsługują, używają tej wartości wszędzie tam, gdzie w przeciwnym razie odczytałyby zegar systemowy. Ustaw ją na podstawie systemu kontroli wersji za pomocą export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). Nie dzieje się to automatycznie i nie jest to uniwersalne rozwiązanie. Tylko narzędzia, które implementują ten standard, odczytują tę zmienną; własne skrypty budujące muszą ją uwzględniać samodzielnie. Skrypt wywołujący date będzie nadal wstawiał bieżący czas, dopóki nie zostanie to zmienione.

Co należy zrobić, gdy system budujący zgłasza status BAD?

Przed podjęciem jakichkolwiek działań przeczytaj raport. Werdykt BAD oznacza, że niezależna kompilacja nie wygenerowała identycznych bajtów, a najczęstszą przyczyną jest niedeterminizm w pakowaniu, a nie atak. Narzędzie rebuilderd może wygenerować raport diffoscope właśnie w tym celu. Jeśli różnice dotyczą znaczników czasu, ścieżek budowania lub kolejności plików, jest to błąd pakowania, który należy zgłosić. Jeśli różnica dotyczy kodu wykonywalnego i nie ma takiego wyjaśnienia, wstrzymaj wdrażanie tej kompilacji, zachowaj artefakty i przekaż sprawę wydawcy.

Czy ten artykuł był przydatny?

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