署名は合っているが、そのビルダーを許可した覚えがない
目標
リリースのバンドルに来歴証明を付けて署名し、証拠が足りないバンドルをデプロイ前に止めるゲートを、プログラムで作ります。止まる場合を3つ、自分で作って確認します。
なぜ重要なのか
署名は「誰が作ったか」に答え、SBOMは「何が入っているか」に答えます。ところが、事故のレポートに最も多く残る文は、その3つのどれでもありません。「このバイト列がどこから来たのか、誰も知らない」です。来歴証明は、その質問の答えを、ビルドの時点で書いておくことです。何を入力に使い、どのコミットから、どのビルダーが、いつ作ったのか。そして、その記録は、デプロイ前に読まれなければ存在しないのと同じです。ゲートがやることがそれで、ゲートの価値は、何を止めたかではなく、なぜ止めたかを人が読めるかから生まれます。最後の場合を必ず自分で作ってみてください。署名も合っていて、ダイジェストも合っているのに、そのビルダーを許可した覚えがないバンドルです。
ステップ
/root/gate/releaseにアーティファクトとSBOMを集めて、/root/gate/subject.jsonを書きます。/root/gate/release/provenance.jsonにin-toto Statementを書きます。- 材料のダイジェストとビルド時刻を加えます。
- 鍵ペアを作って、
/root/gate/release/provenance.json.sigとして署名します。 /root/gate/allowed-builders.txtと/root/gate/release-gate.shでゲートを作ります。- 正常なバンドルの判定を
/root/gate/06-pass.jsonに残します。 /root/gate/unsigned・/root/gate/tampered・/root/gate/rogueを作って、/root/gate/07-reject.jsonに残します。- 4つの判定を
/root/gate/08-audit.jsonlに監査記録として残します。
参考
- 作業はすべて
/root/gateの下で行います。最初にmkdir -p /root/gate/releaseを実行してください。 - フィールド名は、SLSA Provenance v1をそのまま使います。
predicateTypeは、URLバーに見えるアドレスではなく、ドキュメントが書いているhttps://slsa.dev/provenance/v1をそのまま入れます。 - 理由コードは、
sbom_missing・signature_missing・signature_invalid・subject_mismatch・builder_not_allowedの5つで、reasonsは辞書順に重複なしで入れます。 - 3つ目のバンドル(
rogue)は、provenanceを直したあとで、もう一度署名する必要があります。そうしないと、署名のほうで先に引っかかって、このステップが教えようとしていることが表に出ません。 - よくあるミスは、ゲートがどんな場合でも0で終了するようにしておくこと(そうするとパイプラインが止まりません)と、
subjectのnameを絶対パスで書いて、バンドルの中でファイルが見つからなくなることです。 - ラボのPodにはボリュームがありません。セッションが終わると、
/root/gateはまるごと消えます。
リリースのバンドルを1か所に集める
/root/gate/release/の下に、/opt/fixtures/sbom/release/paygate-1.4.2.jsをpaygate-1.4.2.jsとして、/opt/fixtures/sbom/sbom/paygate-1.4.2.cdx.jsonをsbom.cdx.jsonとしてコピーしてください。そのあと/root/gate/subject.jsonに、in-totoのsubject配列を保存してください。項目は1つで、nameはファイル名、digestは{"sha256": 計算した値}です。
来歴証明は、いつも「何に対する」証明です。subjectがその「何」であり、名前ではなくダイジェストで指して初めて、あとですり替えを見つけられます。
ビルドが何を何から作ったかを書く
/root/gate/release/provenance.jsonにin-toto Statementを保存してください。_typeは"https://in-toto.io/Statement/v1"、subjectはステップ1の配列、predicateTypeは"https://slsa.dev/provenance/v1"です。predicate.buildDefinitionには、buildType "https://labhub.example/buildtypes/npm-bundle/v1"とexternalParametersを置きますが、externalParametersには、source(uri "git+https://git.internal/payments/paygate"、digest {"gitCommit": "9f2c1d7a4b6e8035c1a2d4f6b8093e5a7c1d2f40"})とentryPoint "npm run build"を入れます。predicate.runDetails.builder.idは"https://labhub.example/builders/paygate-ci@v3"です。
必須はbuildDefinitionとrunDetailsで、その中でbuildType・externalParameters・builderが必須です。buildTypeがURIなのは、「この欄をどう読むべきか」を指すアドレスだからです。
材料と時刻を書く
/root/gate/release/provenance.jsonのpredicate.buildDefinitionにresolvedDependenciesを加えてください。項目はuriとdigestを持ち、2つの材料はfile:///opt/fixtures/sbom/release/package-lock.jsonとfile:///opt/fixtures/sbom/sbom/paygate-1.4.2.cdx.jsonで、digestは{"sha256": そのファイルのハッシュ}です。uriの辞書順に入れてください。そして、predicate.runDetails.metadataに、invocationId "build-2026-09-11-0007"、startedOn "2026-09-11T03:40:00Z"、finishedOn "2026-09-11T03:52:00Z"を書きます。
材料にダイジェストを書いておけば、「同じ入力でもう一度作れば同じものが出るか」を、あとで問えます。名前だけを書くと、その質問を投げられません。
来歴証明に署名してバンドルに入れる
prime256v1の鍵ペアを/root/gate/release-2026.keyと/root/gate/release-2026.pubに作り、その秘密鍵で/root/gate/release/provenance.jsonにSHA-256で署名して、/root/gate/release/provenance.json.sigに保存してください。
署名されていない来歴証明は、SLSAのドキュメントがBuild L1に置いている段階です。ミスは防げますが、偽造は防げません。署名が付いて初めて、「ビルドのあとに手を加えたか」を問えます。
ゲートをプログラムで書く
/root/gate/allowed-builders.txtに許可するビルダーを1行に1つずつ書き(今回は次の1つだけ: https://labhub.example/builders/paygate-ci@v3)、/root/gate/release-gate.shを作ってください。bash /root/gate/release-gate.sh <묶음 디렉터리>で呼び出すと、bundle・verdict・reasonsを入れたJSON1つを標準出力に出し、止めるものがあれば1で終了します(プレースホルダーはバンドルのディレクトリです)。見るのは4つです。sbom.cdx.jsonのcomponentsが空でないか(なければsbom_missing)、provenance.json.sigがあって/root/gate/release-2026.pubで検証できるか(なければsignature_missing、違えばsignature_invalid)、provenance.jsonのsubjectダイジェストが、バンドル内のそのファイルの実際のハッシュと同じか(違えばsubject_mismatch)、builder.idが許可リストにあるか(なければbuilder_not_allowed)。reasonsは辞書順に重複なしで入れます。
ゲートの価値は、「何を止めたか」ではなく、「なぜ止めたかを人が読めるか」から生まれます。理由をコードで残せば、ダッシュボードも振り返りも、そのコードで数えます。
正常なバンドルが通ることを確認する
bash /root/gate/release-gate.sh /root/gate/releaseを実行して、その出力を/root/gate/06-pass.jsonに保存してください。verdictはpass、reasonsは空の配列である必要があります。
先に通ることを確認せず、止めるほうから試すと、ゲートが「常に止める」ものでも、成功したように見えます。
証拠が足りないバンドルを3つ作って止める
/root/gate/releaseをコピーして、/root/gate/unsigned(署名ファイルだけを消したもの)・/root/gate/tampered(paygate-1.4.2.jsの内容を変えたもの)・/root/gate/rogue(provenance.jsonのbuilder.idを"https://labhub.example/builders/laptop@v1"に変えて、同じ鍵でもう一度署名したもの)を作ってください。3つをそれぞれゲートに通して、/root/gate/07-reject.jsonにunsigned・tampered・rogueを保存します。各値には、exit(終了コード)とreasons(ゲートが出した理由の配列)を入れます。
3つ目がこのラボの核心です。署名は問題なく、ダイジェストも合っているのに、あのビルダーを私たちが許可した覚えがありません。署名だけを見るゲートは、これを通してしまいます。
判定を監査記録に残す
4つの判定を/root/gate/08-audit.jsonlに、1行ずつJSONとして残してください。行はbundle名の辞書順(release・rogue・tampered・unsigned)で、各行はbundle(ディレクトリ名だけ)・verdict・reasons・builder・artifact_sha256・checked_at("2026-09-11T04:00:00Z")を入れます。artifact_sha256は、そのバンドル内のpaygate-1.4.2.jsの実際のハッシュです。
ゲートが止めたという事実は、その瞬間に画面にだけ残ります。あとで「なぜあのとき止まったのか」を問うには、判定とその根拠を一緒に書いておく必要があります。