再現可能ビルドで証明できることとは
チェックサムは届いたファイルの一致を示しますが、ソースとの一致は証明しません。再現可能ビルドが保証する範囲と、SolarWinds Orion侵害が示す限界を解説します。
再現可能ビルドで証明できること
再現可能ビルドで証明できるのは、限定された1つの事実です。受け取ったバイナリが、この完全に同じソースコードから生成されたバイナリであることです。同じソースを使って誰でも再度ビルドし、バイト列を比較できます。これにより、検証は公開者だけが実行できるものではなくなります。
Reproducible Builds プロジェクトは、次のように定義しています。「同じソースコード、ビルド環境、ビルド手順を与えた場合に、指定されたすべての成果物について、どの当事者でもビット単位で同一のコピーを再作成できるなら、そのビルドは再現可能です」。比較にはハッシュを使用します。難しいのは、異なるマシンでも一致するように、環境と手順を十分に厳密に固定することです。
チェックサムではこの疑問に答えられない理由
公開されたチェックサムは、ファイルが破損せずに到達したことを証明します。そのチェックサムに対する署名は、鍵を保持する人物が公開元であることを証明します。しかし、どちらもアーティファクトが存在する前に何が起きたかは示しません。公開元のビルドマシンが侵害されている場合、悪意のあるバイナリもクリーンなバイナリと同じようにチェックサムが計算され、署名されます。そのため、下流で行うすべての検証に合格します。メンテナーが、push されていない作業ツリーからビルドした場合も同じです。
ここに問題があります。ソースコードを読み、署名を検証し、チェックサムを検証しても、リポジトリに一度も現れていないコードを実行している可能性があります。したがって、再現可能性は 公開されたチェックサムに対してダウンロードを検証すること とは別のテーマです。チェックサムは転送を保護します。再ビルドは、転送前に起きたすべての事象を保護します。
この攻撃は理論上のものではありません。2020 年の SolarWinds Orion 侵害は、まさにこの位置で発生しました。ビルドシステムが、誰もレビューしていないソースに対応する署名付きアーティファクトを生成していました。署名の検証はすべて成功しました。署名による検証はアーティファクトから始まるためです。
再現可能ビルドで証明できないこと
ここは過大に評価されやすい部分です。限界を正確に理解してください。
- ソースコードが安全だとは限りません。 オープンな状態でバックドアがコミットされていれば、同じ悪意のあるソースコードを全員がビルドするため、ビルドは再現可能になり、すべての再ビルダーがその結果を確認します。再現可能性によって確認対象はソースツリーに移ります。誰かがそのツリーを確認する必要があるため、オープンソースプロジェクトで AI 支援コードを扱うポリシーなどのレビュー方針は、別の対策として維持されます。
- 入力が安全だとは限りません。 依存関係もビルド対象の一部です。ビルド時に解決された悪意のあるパッケージはアーティファクトにコンパイルされ、同じ依存関係を解決したすべての再ビルダーが同じ結果に同意します。これが npm サプライチェーン攻撃がサーバーに到達する仕組みであり、再現可能ビルドはその攻撃を正確に再現します。
- ツールチェーンが正直だとは限りません。 コンパイラーが侵害されている場合、そのコンパイラーを使うすべての再ビルダーが同じ侵害された出力を生成し、判定結果もすべて一致します。再現可能性によって、この攻撃に必要なコストは上がります。しかし、攻撃自体を検出するわけではありません。
- 脆弱性については何も示しません。 古いライブラリがビット単位で再現されても、それは公開済みの欠陥を持つ古いライブラリのままです。そのため、既知の CVE がないかサーバーを確認することは、別のスケジュールで継続してください。
再現可能性によって排除されるのは、特定の攻撃者の立場です。それはビルドマシンと、ソースからバイナリまでの経路全体です。パッケージが再現可能になるまでは、公開者以外の誰もその経路を検査できません。
同じソースから異なるバイト列が生成される理由
ほとんどのソフトウェアは、初期状態では再現可能なビルドになっていません。原因は単純です。コンパイラやアーカイブ形式は、実行されたマシンに関する情報を記録します。
- タイムスタンプ。
tar、ar、zip形式ではファイルの更新時刻が保存されるため、別の秒にビルドすると出力ファイルが変わります。 - パス。デバッグ情報にはビルドディレクトリの絶対パスが記録されるため、コードが同一でも
/home/alice/srcでのビルドと/build/pkgでのビルドは異なります。 - 順序。ディレクトリを読み取るとファイルシステム上の順序でエントリが返されるため、マシンによってリンク行やアーカイブメンバーの順序が変わります。
- 識別情報。ビルドスクリプトに、ビルドしたユーザーのユーザー名、ホスト名、ロケールが埋め込まれることがあります。
- ビルド時の判断。CPU 機能の検出や乱数のシード処理を行うと、出力はソースではなくマシンに依存します。
最初の原因は、約 ten 秒で確認できます。
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.tar2 つのハッシュが異なるのは、tar ヘッダーに a.txt の更新時刻が保存されており、ファイルを書き直したことでその時刻が 2 秒進んだからです。内容はバイト単位で完全に同一です。メタデータを固定すると解決します。
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これでハッシュは一致します。アーカイブヘッダーのどのフィールドにも、現在のマシンの状態が記録されないためです。--sort=name で順序を固定し、--mtime で時刻を固定します。所有者フラグにより、ユーザー ID が記録されることも防げます。
diffoscope による差分の読み方
2 つのビルドが異なる場合、sha256sum が示すのは、異なっているという事実だけです。理由を人が読める形で示すのが diffoscope です。diffoscope は両方の対象を再帰的に展開し、バイナリ形式をテキストに変換してから、そのテキストを比較します。Debian パッケージ、ELF バイナリ、tar および ZIP アーカイブ、PDF、SQLite データベースなど、100 種類を超える形式に対応しています。
sudo apt install -y diffoscope
diffoscope one.tar two.tar先ほどの tar のペアでは、レポートは短くなります。抜粋すると、次のようになります。
--- 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診断結果はこれです。サイズ、パス、権限は同じで、変更時刻だけが異なります。実際のパッケージではレポートが大幅に長くなるため、ファイルに書き出してブラウザーで開きます。
diffoscope --html report.html build1.changes build2.changesdiffoscope は入力が同一の場合に 0、異なる場合に 1、処理中に問題が発生した場合に 2 で終了します。そのため、ラッパースクリプトなしで CI ジョブに組み込めます。小規模な VPS では、diffoscope ではなく diffoscope-minimal をインストールしてください。完全なパッケージをインストールすると、多くの場合は使わない形式ヘルパーが大量に追加されます。
SOURCE_DATE_EPOCH で修正できることと、適用されない範囲
SOURCE_DATE_EPOCH は、1 個の数値を保持する環境変数です。この数値は、ソースの最終更新時刻を 1970 年 1 月 1 日 UTC からの経過秒数で表したものです。これに対応するビルドツールは、オペレーティングシステムに現在時刻を問い合わせる代わりに、その数値を使用します。値はバージョン管理システムから設定し、ビルドではなくソースに追随させます。
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)Debian パッケージでは、debhelper が changelog から値をエクスポートします。debian/rules で手動設定する場合は、次のようになります。
export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -STimestamp)対応状況はツールごとに異なり、全体に適用されるわけではありません。cmake 3.8 以降、gcc 7 以降、rpm 4.13 より新しいバージョン、Docker buildx 0.10 以降は、この変数を読み取ります。独自のスクリプトは、対応するように記述しない限り読み取りません。スクリプトが date を呼び出す場合は、変数を渡します。
BUILD_DATE="$(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"実装時には、1 つのルールが重要です。変数がすでに設定されている場合、その値がビルドにおける現在時刻です。そのため、呼び出し元が指定した値を上書きしてはいけません。
コンテナイメージでも、別のラッパーを介して同じ問題が発生します。Docker buildx 0.10 以降は、シェルの SOURCE_DATE_EPOCH をビルド引数としてビルドに渡します。レイヤー内のファイルのタイムスタンプを書き換えるには exporter の対応が必要です。BuildKit では 0.13 で追加されました。また、文書化されている形式では、結果をレジストリにプッシュします。
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 .独自ビルドをreprotestでテストする
reprotestは同じソースを2回ビルドします。2回のビルドの間で意図的に環境を変更し、その結果を比較します。環境の違いを作ることが目的です。デフォルトでは、ビルドパス、時刻、タイムゾーン、ロケール、umask、ホスト名、ユーザーとグループ、CPU数、ホームディレクトリ、ファイルの順序を変更します。
sudo apt install -y reprotest
reprotest . -- null--以降では、ビルド環境のバックエンドを選択します。nullは、現在使用しているシステムを指定します。確認用に一時ディレクトリを残すには、-vv -dを追加します。例はreprotest . -vv -- null -dのとおりです。ソースツリーの種類を自動判定させるには、reprotest auto -- nullを使用します。
一部の変更には権限または追加パッケージが必要です。実行できない場合は、エラーが明確に表示されます。全体をrootとして実行するのではなく、これらの変更を無効にしてください。
reprotest --vary=-user_group,-domain_host,-fileordering auto -- nullreprotestが報告する内容は、再ビルドを行う人が後で公開の場で報告する内容です。その報告には、あなたのプロジェクト名が記載されます。
リビルダーの判定が意味すること
リビルダーは、公開者が管理するものではないマシンです。公開されたソースと記録済みのビルド環境を使ってパッケージを再度ビルドし、その出力をアーカイブ内の成果物と比較します。この判定に意味があるのは、マシンが独立しているからです。
Debian は環境を .buildinfo ファイルに記録し、dpkg-buildpackage はこれを .deb の隣に書き込みます。重要なのは各フィールドです。Installed-Build-Depends には、ビルドに影響する可能性があるインストール済みパッケージと、その正確なバージョンがすべて記録されます。Build-Path には、ビルドを実行した場所が記録されます。Environment には、影響することが分かっている環境変数が記録されます。Checksums-Sha256 は出力を対象とします。このファイルが、2 回目の試行に使うレシピです。
sudo apt install -y devscripts mmdebstrap
debrebuild --buildresult=./artifacts --builder=mmdebstrap hello_2.10-2_amd64.buildinfodebrebuild は buildinfo を読み取り、そこに記載された正確な依存関係のバージョンを snapshot.debian.org から取得します。そのため、現在再ビルドしても、元のビルド日に存在していたパッケージバージョンを使用できます。mmdebstrap ビルダーには、chroot の設定も superuser 権限も必要ありません。生成された成果物をアーカイブ内のコピーと diffoscope で比較します。
Arch Linux は rebuilderd を実行し、継続的にこの処理を行って判定を公開しています。
rebuildctl -H https://reproducible.archlinux.org pkgs ls --name rebuilderdステータスは GOOD、BAD、UNKWN です。それぞれの単純な読み方は、異なる方向に誤解を招きます。GOOD は、独立した第三者が同じバイト列を生成したことを意味します。これはビルドについての強い証拠ですが、ソースについては何も示しません。BAD は、ほとんどの場合攻撃を意味しません。通常の原因は、パッケージング時に固定されていなかったタイムスタンプやパスです。そのため、rebuilderd は失敗時に diffoscope のレポートを添付できます。UNKWN は、まだ誰もテストしていないことを意味します。テストされていないパッケージは、合格したパッケージではありません。
運用上のルールは簡単です。BAD 判定が出たら、レポートを確認します。レポートにタイムスタンプ、ビルドパス、メンバーの順序が示されている場合は、パッケージングのバグを報告します。そのような説明がないのに実行可能コードが異なる場合は、そのビルドのデプロイを中止し、エスカレーションしてください。
Debian は現在どの程度再現可能か
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"
}
]この記事の執筆時点で、amd64 の unstable では、テストした 41,163 個のパッケージのうち 94.2% が再現可能でした。Experimental は 67.0% でした。対象は、まだ修正が完了していないパッケージが中心となる、より小規模で新しい 588 個のサンプルです。
これらの数値は、tests.reproducible-builds.org の Debian ページから取得したものです。2026-08-18 に確認した時点で、ページには「Last update: 2026-08-18 16:02 UTC」と表示されていました。数値は変動します。6 か月後にこの段落を引用せず、tracker を確認してください。
割合よりも重要な注意点があります。このフレームワークは、各パッケージを専用のハードウェア上で 2 回ビルドし、2 回のビルドで環境を変えて、それぞれの結果を比較します。測定しているのは、パッケージを再現可能な形でビルドできるかどうかです。アーカイブ内の .deb が一致するかどうかを確認するものではありません。後者は、rebuilder が公開された成果物と比較して確認する別の作業です。どちらの数値も有用ですが、答えている質問は異なります。にもかかわらず、前者を後者であるかのように引用する人がいます。
自分のサーバーで行うこと
ディストリビューションを再構築する必要はありません。一般的なサーバーに適用できる対策は、もっと少なく、低コストです。
- ツールチェーンを固定します。タグで参照したベースイメージは、予告なく変更されます。ダイジェストで参照し、そのダイジェストをリリースとともに記録します。
- 入力を記録します。lockfile、イメージのダイジェスト、コンパイラのバージョンをアーティファクトと同じ場所に保持します。環境を再現できないビルドは再構築できないため、検証もできません。
- CI で 2 回ビルドし、出力が異なる場合はジョブを失敗させます。追加のビルド 1 回分のコストで済みます。誰かが非決定性を導入した当日に検出できるため、1 年後のインシデント発生時まで発見できない事態を防げます。
- コンパイラが埋め込むパスを削除します。Go では、
go build -trimpath -buildvcs=falseによりビルドディレクトリとバージョン管理スタンプを削除でき、go version -m ./appにより実際にバイナリへ含まれた情報を表示できます。 - デプロイした対象のハッシュを保持します。実行中のバイナリが特定のソースリビジョンに対応しているか確認する必要がある場合、その記録だけが答えを示せます。
CI のチェックは 4 行です。
set -eu
./build.sh && mv dist/app app.1
./build.sh && mv dist/app app.2
diffoscope --text - app.1 app.22 つのアーティファクトが異なる場合、diffoscope はゼロ以外の終了ステータスを返します。そのため、ジョブは自動的に失敗し、ログには読みやすい説明が残ります。これが、1 つのリポジトリに縮小した場合の要点です。バイナリがソースツリーから生成されたという主張は、別のマシンで検証できるものでなければなりません。
FAQ
再現可能ビルドなら、ソフトウェアは安全ですか?
いいえ。バイナリがソースに対応していることを証明するだけで、それ以上は分かりません。公開ソースツリーにバックドアがコミットされていれば、どの再ビルド担当者も同じ悪意のあるソースをビルドするため、ビルドは再現され、全員がその結果を確認することになります。既知の CVE があるパッケージも完全に再現され、脆弱なままです。再現可能ビルドによって排除できるのは、攻撃者がビルドマシンやソースからバイナリに至る経路へ介入する立場の1つだけです。ソースを読むことと脆弱性を追跡することは別の作業であり、再現可能ビルドが代わりに実行してくれるわけではありません。
ソースを変更していないのに、2回のビルド結果が異なるのはなぜですか?
ほとんどの場合、原因はタイムスタンプ、パス、または順序です。tar や zip などのアーカイブ形式はファイルの更新時刻を保存するため、異なる秒に checkout するとバイト列が変わります。デバッグ情報にはビルドディレクトリの絶対パスが記録されるため、/home/alice/src と /build/pkg は同じコードから異なるバイナリを生成します。ディレクトリの読み取りではファイルシステム上の順序でエントリが返されるため、別のマシンではリンク時のオブジェクトファイルの順序が変わることがあります。diffoscope build1 build2 を実行すると、推測に頼らず、これらのどれが原因かをレポートで確認できます。
SOURCE_DATE_EPOCH とは何ですか。設定する必要はありますか?
SOURCE_DATE_EPOCH は、ソースの最終更新時刻を 1970 年1月1日 UTC からの経過秒数で示す、1つの数値を格納する標準環境変数です。これに対応したツールは、通常ならシステム時計を参照する箇所でこの値を使用します。バージョン管理システムから export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) で設定します。自動設定されるわけではなく、一般的な解決策でもありません。読み取るのは対応したツールだけです。また、独自のビルドスクリプトではスクリプト自身が読み取る必要があります。そのため、date を呼び出すスクリプトは、変更しない限り現在時刻を記録し続けます。
再ビルド担当者が BAD と報告した場合、どうすればよいですか?
まず、ほかの作業をする前にレポートを読みます。BAD という判定は、独立した再ビルドの1つで同じバイト列が生成されなかったことを意味します。通常の原因は攻撃ではなく、パッケージングにおける非決定性です。この目的のため、rebuilderd は diffoscope のレポートを生成できます。差分がタイムスタンプ、ビルドパス、ファイル順序によるものであれば、報告すべきパッケージングのバグです。そのような説明がなく、差分が実行可能コードにある場合は、そのビルドのデプロイを中止し、成果物を保持したうえで、公開元へエスカレーションしてください。