签名是对的,可我们从没许可过那个构建器
目标
给发布包附上来源证明并签名,并用程序制作一个在发布前拦截证据不足的发布包的 关卡。亲手制造三种会被拦截的情况并加以确认。
为什么重要
签名回答“是谁做的”,SBOM 回答“里面有什么”。然而,事故 报告中留下最多的一句话,并不是这两者——而是“没有人知道这些字节是从哪里来的”。 来源证明就是在构建时记下这个问题的答案。用什么作为 输入,来自哪个提交,由哪个构建者,在什么时候生成。而这份记录,如果在发布前 没有被读取,就等于不存在。 关卡做的就是这件事,关卡的价值不在于拦住了什么, 而在于人能不能读懂它为什么拦住。请务必 亲手制造最后一种情况——签名正确、摘要也正确,但从未允许过那个构建者的发布包。
步骤
- 把制品和 SBOM 收集到
/root/gate/release中,并写下/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中。 - 把四个判定作为审计记录留在
/root/gate/08-audit.jsonl中。
参考
- 所有工作都在
/root/gate下进行。请先执行mkdir -p /root/gate/release。 - 字段名称原样使用 SLSA Provenance v1。
predicateType不是地址栏中显示的 地址,而是原样填入文档中写明的https://slsa.dev/provenance/v1。 - 原因代码有
sbom_missing、signature_missing、signature_invalid、subject_mismatch、builder_not_allowed五个,reasons按字典序去重后写入。 - 第三个发布包(
rogue)在修改 provenance 之后必须重新签名。否则 会先在签名这一环被拦住,这个步骤想要教的内容就显现不出来。 - 常见错误——让关卡在任何情况下都以 0 结束(这样流水线就
不会停下),以及把
subject的name写成绝对路径,导致在发布包中找不到文件。 - 实验 Pod 没有卷。会话结束后,
/root/gate会整个消失。
把发布包收集到一处
把 /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/release/ 下。然后把 in-toto 的 subject 数组保存到 /root/gate/subject.json——只有一个条目,name 是文件名,digest 是 {"sha256": 计算出的值}。
证明永远是“针对某个东西”的证明。subject 就是那个“东西”,必须用摘要而不是名称来指向它,以后才能抓住偷梁换柱。
写下构建用什么东西生成了什么
把 in-toto Statement 保存到 /root/gate/release/provenance.json。_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,两项材料是 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"。
如果给材料写下摘要,以后就可以追问“用同样的输入重新构建,会得到同样的东西吗”。如果只写名称,就提不出这个问题。
对证明签名并放入发布包
在 /root/gate/release-2026.key 和 /root/gate/release-2026.pub 中生成 prime256v1 密钥对,并用该私钥对 /root/gate/release/provenance.json 做 SHA-256 签名,保存到 /root/gate/release/provenance.json.sig。
没有签名的证明,在 SLSA 文档中属于 Build L1 的位置——能防止失误,却防不住伪造。加上签名之后,才能追问“构建之后有没有被动过”。
用程序写出关卡
在 /root/gate/allowed-builders.txt 中每行写一个允许的构建者(此次只有 https://labhub.example/builders/paygate-ci@v3 一个),并创建 /root/gate/release-gate.sh。以 bash /root/gate/release-gate.sh <묶음 디렉터리>(占位符为发布包目录)调用时,向标准输出打印一段包含 bundle、verdict、reasons 的 JSON,有要拦截的内容时以 1 结束。要检查的有四项——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 必须是空数组。
如果不先确认通过的情况,而是从拦截开始测试,即使关卡是“总是拦截”的,也会看起来像是成功了。
制造三个证据不足的发布包并拦住它们
复制 /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",并用同一把密钥重新签名)。把三者分别送入关卡,并把 unsigned、tampered、rogue 保存到 /root/gate/07-reject.json,每个值包含 exit(退出码)和 reasons(关卡给出的原因数组)。
第三个是本实验的关键——签名完好,摘要也正确,但我们从未允许过那个构建者。只看签名的关卡会让它通过。
把判定留作审计记录
把四个判定逐行以 JSON 形式写入 /root/gate/08-audit.jsonl。行按 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 的实际哈希。
关卡拦住了这件事,只会在那一刻留在屏幕上。要想日后追问“当时为什么被拦住”,就必须把判定和它的依据一起记下来。