Bản build có thể tái lập thực sự chứng minh gì?
Checksum chỉ chứng minh bạn nhận đúng file publisher gửi. Reproducible build chứng minh file khớp với source có thể đọc, tức hai tuyên bố khác nhau.
Bản build có thể tái lập chứng minh điều gì
Bản build có thể tái lập chỉ chứng minh một điều cụ thể: binary bạn nhận được chính là binary được tạo ra từ đúng source code này. Bất kỳ ai cũng có thể dùng cùng source code để build lại rồi so sánh từng byte. Việc xác minh không còn là thao tác chỉ publisher mới thực hiện được.
Dự án Reproducible Builds định nghĩa khái niệm này như sau: "Một bản build có tính tái lập nếu, khi có cùng source code, môi trường build và hướng dẫn build, bất kỳ bên nào cũng có thể tạo lại các bản sao giống hệt nhau theo từng bit của toàn bộ artifact được chỉ định." Bản thân việc so sánh sử dụng hash. Khó khăn nằm ở việc cố định môi trường và hướng dẫn đủ chặt để hai máy khác nhau cho ra kết quả giống nhau.
Vì sao checksum không trả lời được câu hỏi này
Checksum được công bố chứng minh file đã đến nơi mà không bị hỏng. Signature trên checksum đó chứng minh file đến từ người đang giữ key. Không điều nào cho biết chuyện gì đã xảy ra trước khi artifact được tạo. Nếu build machine của publisher bị compromise, binary độc hại vẫn được tính checksum và ký giống hệt binary sạch, nên mọi bước kiểm tra ở downstream đều pass. Điều tương tự cũng xảy ra nếu maintainer build từ một working tree chưa từng được push.
Đó là khoảng trống cần giải quyết. Bạn có thể đọc source, verify signature, verify checksum, nhưng vẫn đang chạy code chưa từng xuất hiện trong repository. Vì vậy, khả năng reproducibility là một chủ đề khác với verify download bằng checksum đã được công bố. Checksum bảo vệ quá trình truyền. Việc rebuild bảo vệ mọi thứ xảy ra trước quá trình truyền.
Đây không phải là một cuộc tấn công trên lý thuyết. Vụ compromise SolarWinds Orion năm 2020 nằm đúng ở vị trí này: build system tạo ra các artifact đã được ký nhưng không tương ứng với source mà bất kỳ ai đã review. Mọi bước kiểm tra signature đều pass vì signature chỉ bắt đầu từ artifact.
Bản build có tính tái lập không chứng minh được điều gì
Đây là phần thường bị quảng bá quá mức, vì vậy cần nêu rõ các giới hạn.
- Bản build không chứng minh source an toàn. Một backdoor được commit công khai vẫn build tái lập được, và mọi bên build lại đều xác nhận kết quả đó vì tất cả đã build cùng một source độc hại. Tính tái lập chuyển mục tiêu kiểm tra sang source tree. Vẫn cần có người đọc source tree đó. Vì vậy, chính sách review, bao gồm các chính sách về code có AI hỗ trợ trong các dự án open source, vẫn là một biện pháp kiểm soát riêng.
- Bản build không chứng minh input của bạn an toàn. Dependencies là một phần của nội dung được build. Một package độc hại được resolve tại thời điểm build sẽ được compile vào artifact, và mọi bên build lại resolve cùng dependency đó sẽ cho cùng kết quả với bạn. Đó là cách một cuộc tấn công supply chain của npm xâm nhập server, và bản build tái lập sẽ tái tạo cuộc tấn công đó một cách chính xác.
- Bản build không chứng minh toolchain trung thực. Nếu compiler bị compromise, mọi bên build lại dùng compiler đó sẽ tạo ra cùng output đã bị compromise, và mọi kết quả kiểm tra đều khớp nhau. Tính tái lập làm tăng chi phí thực hiện kiểu tấn công này. Nó không phát hiện được cuộc tấn công.
- Bản build không cho biết gì về các lỗ hổng. Một library cũ được tái tạo giống từng bit vẫn là library cũ với các lỗi đã được công bố. Vì vậy, hãy tiếp tục kiểm tra server để tìm các CVE đã biết theo lịch riêng.
Tính tái lập loại bỏ một vị trí tấn công cụ thể: build machine và toàn bộ đường đi từ source đến binary. Khi một package chưa có tính tái lập, không ai bên ngoài publisher có thể kiểm tra đường đi đó.
Vì sao cùng một source lại tạo ra các byte khác nhau
Hầu hết phần mềm không có khả năng build reproducible theo mặc định. Nguyên nhân thường rất đơn giản. Compiler và định dạng archive ghi lại các thông tin về máy đã chạy chúng.
- Một timestamp. Các định dạng
tar,arvàziplưu thời gian sửa đổi của file, nên build vào giây khác sẽ làm file output thay đổi. - Một path. Thông tin debug lưu thư mục build tuyệt đối, nên build trong
/home/alice/srcvà build trong/build/pkgsẽ khác nhau dù code giống hệt nhau. - Một thứ tự. Khi đọc một directory, hệ thống trả về các entry theo thứ tự của filesystem, nên dòng lệnh link hoặc thứ tự member trong archive có thể thay đổi giữa các máy.
- Một danh tính. Build script có thể nhúng username, hostname hoặc locale của người thực hiện build.
- Một quyết định tại thời điểm build. Việc phát hiện CPU feature hoặc seed ngẫu nhiên khiến output phụ thuộc vào máy thay vì source.
Bạn có thể quan sát nguyên nhân đầu tiên xảy ra trong khoảng 10 giây:
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.tarHai hash khác nhau vì tar header lưu thời gian sửa đổi của a.txt, và việc ghi lại file đã đẩy thời gian đó lên 2 giây. Nội dung giống nhau từng byte. Cố định metadata sẽ khắc phục vấn đề:
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.tarBây giờ hai hash giống nhau vì không có field nào trong archive header lấy từ trạng thái hiện tại của máy. --sort=name cố định thứ tự, --mtime cố định clock, còn các ownership flag ngăn user id của bạn được ghi vào archive.
Đọc khác biệt bằng diffoscope
Khi hai bản build khác nhau, sha256sum chỉ cho biết chúng khác nhau và không cung cấp thêm thông tin. diffoscope cho biết nguyên nhân bằng cách trình bày theo dạng con người có thể đọc. Công cụ này giải nén đệ quy cả hai phía, chuyển các định dạng binary thành text rồi so sánh phần text. Công cụ hỗ trợ Debian package, ELF binary, archive tar và ZIP, PDF, database SQLite cùng hơn một trăm định dạng khác.
sudo apt install -y diffoscope
diffoscope one.tar two.tarVới cặp tar ở trên, report khá ngắn. Sau khi rút gọn, nội dung như sau:
--- 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Đó là toàn bộ chẩn đoán: cùng kích thước, cùng path, cùng permission nhưng khác modification time. Một package thực tế sẽ tạo report dài hơn nhiều, vì vậy hãy ghi report ra file rồi mở bằng browser:
diffoscope --html report.html build1.changes build2.changesdiffoscope trả về mã thoát 0 khi các input giống hệt nhau, 1 khi chúng khác nhau và 2 khi gặp lỗi. Vì vậy, công cụ này có thể đưa thẳng vào CI job mà không cần wrapper script. Trên một VPS nhỏ, hãy cài diffoscope-minimal thay vì diffoscope: package đầy đủ sẽ kéo theo một bộ lớn các format helper mà bạn có thể sẽ không bao giờ dùng.
SOURCE_DATE_EPOCH khắc phục điều gì và giới hạn ở đâu
SOURCE_DATE_EPOCH là một biến môi trường chứa một số duy nhất: thời điểm sửa đổi cuối cùng của source, được tính bằng số giây kể từ ngày 1 tháng 1 năm 1970 UTC. Build tool hỗ trợ biến này sẽ dùng giá trị đó ở mọi nơi mà lẽ ra nó phải hỏi thời gian hiện tại từ operating system. Hãy lấy giá trị từ version control để giá trị này đi theo source thay vì đi theo lần build:
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)Trong Debian package, debhelper tự export biến này từ changelog. Đặt thủ công trong debian/rules như sau:
export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -STimestamp)Mỗi tool hỗ trợ theo cách riêng, không phải hỗ trợ global. cmake 3.8 trở lên, gcc 7 trở lên, rpm trên 4.13 và Docker buildx 0.10 trở lên đều đọc biến này. Các script của bạn thì không, trừ khi bạn tự viết để chúng đọc biến. Nếu một script gọi date, hãy truyền biến này cho nó:
BUILD_DATE="$(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"Khi triển khai, có một quy tắc quan trọng. Nếu biến đã được set, giá trị đó chính là thời gian hiện tại đối với build của bạn. Vì vậy, không được ghi đè giá trị do caller truyền vào.
Container image gặp cùng vấn đề này nhưng qua một wrapper khác. Docker buildx 0.10 trở lên truyền SOURCE_DATE_EPOCH từ shell vào build dưới dạng build argument. Timestamp của các file bên trong layer cần exporter rewrite lại. BuildKit đã bổ sung tính năng này trong 0.13, và dạng được document sẽ push kết quả lên 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 .Kiểm tra bản build của bạn bằng reprotest
reprotest build cùng một source hai lần và cố ý thay đổi environment giữa hai lần build, sau đó so sánh kết quả. Các thay đổi này là mục đích chính của công cụ. Theo mặc định, công cụ thay đổi build path, thời gian, timezone, locale, umask, hostname, user và group, số CPU, home directory và thứ tự file.
sudo apt install -y reprotest
reprotest . -- nullMọi thứ sau -- đều chọn backend cho build environment, còn null có nghĩa là hệ thống bạn đang sử dụng. Thêm -vv -d để giữ lại các thư mục tạm phục vụ việc kiểm tra, như trong reprotest . -vv -- null -d. Dùng reprotest auto -- null để công cụ tự xác định loại source tree đang được kiểm tra.
Một số thay đổi cần privilege hoặc package bổ sung. Chúng sẽ báo lỗi rõ ràng nếu không thể chạy. Hãy tắt các thay đổi đó thay vì chạy toàn bộ dưới root:
reprotest --vary=-user_group,-domain_host,-fileordering auto -- nullMọi vấn đề reprotest báo cáo đều là những vấn đề mà một rebuilder có thể phát hiện sau đó và công khai với tên project của bạn.
Ý nghĩa của kết luận từ rebuilder
Rebuilder là một máy không thuộc về bên phát hành. Máy này lấy source đã được phát hành và build environment đã được ghi lại, build lại package, rồi so sánh output của chính nó với artifact trong archive. Kết luận chỉ có giá trị vì máy đó độc lập.
Debian ghi lại environment trong file .buildinfo do dpkg-buildpackage ghi cạnh .deb. Các field mới là phần đáng chú ý. Installed-Build-Depends liệt kê mọi package đã cài có thể ảnh hưởng đến quá trình build, kèm version chính xác. Build-Path ghi lại nơi quá trình build đã chạy. Environment ghi lại các biến môi trường được biết là có ảnh hưởng. Checksums-Sha256 bao quát các output. File này là công thức cho lần thử thứ hai:
sudo apt install -y devscripts mmdebstrap
debrebuild --buildresult=./artifacts --builder=mmdebstrap hello_2.10-2_amd64.buildinfodebrebuild đọc buildinfo và lấy đúng các version dependency được nêu trong đó từ snapshot.debian.org, nên lần rebuild hôm nay có thể dùng các version package từng tồn tại vào ngày build ban đầu. Builder mmdebstrap không cần thiết lập chroot và không cần quyền superuser. Dùng diffoscope để so sánh các artifact mà nó tạo ra với bản copy trong archive.
Arch Linux chạy rebuilderd liên tục và công bố các kết luận:
rebuildctl -H https://reproducible.archlinux.org pkgs ls --name rebuilderdCác status là GOOD, BAD và UNKWN, nhưng cách hiểu đơn giản về từng status đều sai theo một hướng khác nhau. GOOD có nghĩa là một bên độc lập tạo ra đúng cùng một byte, đây là khẳng định mạnh về quá trình build nhưng hoàn toàn không khẳng định gì về source. BAD hầu như không bao giờ là một cuộc tấn công: nguyên nhân thường gặp là timestamp hoặc path mà quá trình đóng gói chưa cố định, vì vậy rebuilderd có thể đính kèm báo cáo diffoscope vào lỗi. UNKWN có nghĩa là chưa có ai kiểm thử package đó, và package chưa được kiểm thử không phải là package đạt.
Vì vậy, quy tắc vận hành rất ngắn gọn. Kết luận BAD là lý do để đọc báo cáo. Nếu báo cáo cho thấy timestamp, build path hoặc thứ tự member, hãy mở packaging bug. Nếu báo cáo cho thấy executable code khác nhau mà không có lời giải thích như trên, hãy dừng triển khai bản build đó và chuyển việc xử lý lên cấp cao hơn.
Debian hiện tái lập được đến mức nào?
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"
}
]Vào ngày bài viết này được viết, unstable trên amd64 có 94.2% package tái lập được, trong tổng số 41,163 package đã kiểm thử. Experimental ở mức 67.0%, trên một mẫu nhỏ hơn nhiều và mới hơn nhiều gồm 588 package. Đây là kết quả có thể dự đoán ở những package mà chưa ai sửa xong.
Các số liệu này lấy từ trang Debian trên tests.reproducible-builds.org, được đọc vào 2026-08-18, khi trang hiển thị dấu thời gian "Last update: 2026-08-18 16:02 UTC". Các số liệu sẽ thay đổi. Hãy đọc tracker thay vì trích lại đoạn này sau sáu tháng.
Có một điểm cần lưu ý quan trọng hơn tỷ lệ phần trăm. Framework này build mỗi package 2 lần trên chính hardware của nó, thay đổi môi trường giữa 2 lần build, rồi so sánh 2 kết quả của chính nó. Nó đo xem một package có thể build theo cách tái lập được hay không. Nó không kiểm tra xem .deb đang nằm trong archive có khớp hay không. Đó là công việc riêng của rebuilder, bằng cách so sánh với artifact đã được publish. Cả 2 con số đều hữu ích. Chúng trả lời 2 câu hỏi khác nhau, nhưng người ta thường trích con số đầu tiên như thể đó là con số thứ hai.
Việc cần làm trên server của bạn
Bạn không cần rebuild cả một distribution. Những phần có thể áp dụng cho một server thông thường ít hơn và không tốn nhiều chi phí.
- Pin toolchain. Base image được tham chiếu bằng tag có thể thay đổi mà không cảnh báo. Hãy tham chiếu image bằng digest và ghi lại digest cùng với release.
- Ghi lại các input. Lưu lockfile, image digest và phiên bản compiler cùng với artifact. Một build có môi trường không thể tái tạo thì không thể rebuild, nên cũng không thể kiểm tra.
- Build hai lần trong CI và fail job khi output khác nhau. Việc này chỉ tốn thêm một lần build, đồng thời phát hiện nondeterminism ngay khi có người đưa nó vào, thay vì một năm sau trong lúc xử lý incident.
- Loại bỏ các path mà compiler nhúng vào. Với Go,
go build -trimpath -buildvcs=falseloại bỏ build directory và version control stamp, còngo version -m ./appin ra chính xác những gì đã được đưa vào binary. - Lưu hash của artifact đã deploy. Khi cần biết binary đang chạy có tương ứng với một source revision hay không, record này là thứ duy nhất có thể trả lời.
CI check gồm bốn dòng:
set -eu
./build.sh && mv dist/app app.1
./build.sh && mv dist/app app.2
diffoscope --text - app.1 app.2diffoscope trả về mã thoát khác 0 khi hai artifact khác nhau, nên job tự fail và để lại phần giải thích dễ đọc trong log. Đó là toàn bộ ý tưởng, được thu gọn cho một repository: tuyên bố rằng một binary được tạo từ một source tree phải là điều mà một máy thứ hai có thể kiểm tra.
FAQ
Một bản build có thể tái lập có nghĩa là phần mềm an toàn không?
Không. Nó chỉ chứng minh binary tương ứng với source, không chứng minh điều gì khác. Một backdoor được commit vào source tree công khai vẫn build tái lập được, và mọi bên rebuild đều xác nhận điều đó vì họ đã build cùng source độc hại. Một package có CVE đã biết vẫn tái lập hoàn toàn và vẫn dễ bị tấn công. Tính tái lập loại bỏ một vị trí mà attacker có thể tấn công: build machine và đường đi từ source đến binary. Đọc source và theo dõi lỗ hổng là các công việc riêng mà tính tái lập không thể làm thay bạn.
Vì sao 2 bản build của tôi khác nhau khi source không thay đổi?
Gần như luôn là do timestamp, path hoặc thứ tự. Các format archive như tar và zip lưu thời gian sửa đổi của file, nên checkout được tạo ở một giây khác sẽ tạo ra các byte khác nhau. Debug information ghi lại build directory tuyệt đối, nên /home/alice/src và /build/pkg tạo ra các binary khác nhau từ cùng một code. Việc đọc directory trả về các entry theo thứ tự của filesystem, nên các object file trên link line có thể được sắp xếp khác nhau trên máy khác. Chạy diffoscope build1 build2 để report cho biết nguyên nhân là gì, thay vì để bạn phải đoán.
SOURCE_DATE_EPOCH là gì và tôi có phải đặt nó không?
Đây là một environment variable tiêu chuẩn chứa 1 số: thời gian sửa đổi cuối cùng của source, tính bằng giây kể từ 1 January 1970 UTC. Các tool hỗ trợ biến này sẽ dùng giá trị đó ở mọi nơi mà nếu không chúng sẽ đọc system clock. Đặt giá trị này từ version control bằng export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). Biến này không tự động được đặt và không phải cách khắc phục chung. Chỉ các tool có implement biến này mới đọc nó. Các build script của bạn phải tự đọc biến này, nên script gọi date vẫn tiếp tục ghi timestamp hiện tại cho đến khi bạn sửa script.
Tôi nên làm gì khi rebuilder báo BAD?
Đọc report trước khi làm bất kỳ việc gì khác. Kết luận BAD có nghĩa là một lần rebuild độc lập không tạo ra cùng các byte, và nguyên nhân thông thường là tính không xác định trong packaging chứ không phải một cuộc tấn công. rebuilderd có thể tạo report diffoscope chính vì lý do này. Nếu khác biệt nằm ở timestamp, build path hoặc thứ tự file, đó là packaging bug đáng để tạo issue. Nếu khác biệt nằm trong executable code mà không có cách giải thích như trên, hãy dừng deploy build đó, giữ lại các artifact và chuyển vụ việc cho publisher.