SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-21

Czym są powtarzalne kompilacje i co faktycznie dowodzą

Suma kontrolna potwierdza integralność pliku, lecz powtarzalne kompilacje weryfikują zgodność binariów z kodem źródłowym. Sprawdź, dlaczego to kluczowe dla bezpieczeństwa.

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 plik dotarł bez uszkodzeń. Podpis cyfrowy złożony na tej sumie kontrolnej dowodzi, że pochodzi ona od osoby posiadającej klucz. Żaden z tych elementów nie mówi nic o tym, co wydarzyło się przed powstaniem artefaktu. Jeśli maszyna budująca wydawcy została przejęta, złośliwy plik binarny zostanie opatrzony sumą kontrolną i podpisany dokładnie tak samo jak czysty plik, więc każda weryfikacja w dalszym łańcuchu zakończy się powodzeniem. Jeśli opiekun projektu buduje plik z drzewa roboczego, które nigdy nie zostało wypchnięte do repozytorium, sytuacja jest identyczna.

To jest właśnie ta luka. Można przeczytać kod źródłowy, zweryfikować podpis, zweryfikować 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 za pomocą opublikowanej sumy kontrolnej. Suma kontrolna chroni transfer. Powtórna kompilacja chroni wszystko, co wydarzyło się przed transferem.

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 zakończyła się powodzeniem, ponieważ podpisy zaczynają się od 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 ponad sto 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 cała diagnoza: ten sam rozmiar, ta sama ścieżka, te same uprawnienia, różny czas modyfikacji. W przypadku rzeczywistego pakietu raport jest znacznie dłuższy, 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 konieczności pisania skryptów opakowujących. Na małym serwerze VPS należy zainstalować diffoscope-minimal zamiast diffoscope: pełny pakiet wymusza pobranie dużej liczby bibliotek pomocniczych dla różnych 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 UTC. Narzędzie budujące, które uwzględnia tę zmienną, używa tej wartości wszędzie tam, gdzie w przeciwnym razie pobrałoby aktualny czas z systemu operacyjnego. Należy ustawić ją na podstawie systemu kontroli wersji, aby wartość była powiązana ze źródłem, a nie z momentem budowania:

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

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

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

Obsługa tej zmiennej zależy od konkretnego narzędzia i nie jest globalna. 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 będą jej uwzględniać, dopóki nie 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 aktualnym czasem dla procesu budowania, dlatego nigdy nie należy nadpisywać wartości dostarczonej przez wywołującego.

Obrazy kontenerów mają 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 eksportera, co BuildKit wprowadził w wersji 0.13, a udokumentowana metoda wypycha 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 elementem tego procesu. Domyślnie narzędzie zmienia ścieżkę kompilacji, czas, strefę czasową, ustawienia regionalne (locale), umask, nazwę hosta, użytkownika i grupę, liczbę procesorów, katalog domowy oraz kolejność plików.

sudo apt install -y reprotest
reprotest . -- null

Wszystko po -- określa backend środowiska budowania, a null oznacza system, na którym aktualnie pracujesz. Dodaj -vv -d, aby zachować katalogi tymczasowe 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ę błędem, jeśli 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ą później publicznie zgłosiłby system przebudowy (rebuilder), 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?

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"
  }
]

W dniu publikacji tego wpisu, gałąź unstable dla architektury amd64 osiągnęła poziom 94.2% powtarzalności dla 41,163 przetestowanych pakietów. W gałęzi experimental wynik wyniósł 67.0% dla znacznie mniejszej i nowszej próbki 588 pakietów, co jest typowe dla oprogramowania, w którym proces naprawczy nie został jeszcze zakończony.

Powyższe dane pochodzą ze strony Debiana w serwisie tests.reproducible-builds.org i zostały odczytane w dniu 2026-08-18, kiedy to strona zawierała adnotację „Last update: 2026-08-18 16:02 UTC”. Wartości te ulegają zmianie. Należy sprawdzać tracker zamiast cytować ten akapit za pół roku.

Istotniejsze od samego odsetka jest jedno zastrzeżenie. Niniejszy framework kompiluje każdy pakiet dwukrotnie na własnym sprzęcie, zmieniając środowisko między budowaniami, a następnie porównuje oba wyniki. Mierzy on, czy pakiet może zostać skompilowany w sposób powtarzalny. Nie jest to weryfikacja, czy plik .deb znajdujący się w archiwum jest zgodny z tym wynikiem; 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, choć często cytuje się pierwszą z nich, traktując ją błędnie 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.

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