TT Lab
开始
学习 学习路径 课程

构建是绿灯,可那个库是谁加进来的

签名是对的,可我们从没许可过那个构建器

在 TT Lab 中继续学习

目标

给发布包附上来源证明并签名,并用程序制作一个在发布前拦截证据不足的发布包的 关卡。亲手制造三种会被拦截的情况并加以确认。

为什么重要

签名回答“是谁做的”,SBOM 回答“里面有什么”。然而,事故 报告中留下最多的一句话,并不是这两者——而是“没有人知道这些字节是从哪里来的”。 来源证明就是在构建时记下这个问题的答案。用什么作为 输入,来自哪个提交,由哪个构建者,在什么时候生成。而这份记录,如果在发布前 没有被读取,就等于不存在。 关卡做的就是这件事,关卡的价值不在于拦住了什么, 而在于人能不能读懂它为什么拦住。请务必 亲手制造最后一种情况——签名正确、摘要也正确,但从未允许过那个构建者的发布包。

步骤

  1. 把制品和 SBOM 收集到 /root/gate/release 中,并写下 /root/gate/subject.json。
  2. 在 /root/gate/release/provenance.json 中写入 in-toto Statement。
  3. 补上材料的摘要和构建时间。
  4. 生成密钥对,并签名为 /root/gate/release/provenance.json.sig。
  5. 用 /root/gate/allowed-builders.txt 和 /root/gate/release-gate.sh 制作关卡。
  6. 把正常发布包的判定留在 /root/gate/06-pass.json 中。
  7. 创建 /root/gate/unsigned、/root/gate/tampered、/root/gate/rogue,并把结果留在 /root/gate/07-reject.json 中。
  8. 把四个判定作为审计记录留在 /root/gate/08-audit.jsonl 中。

参考

把发布包收集到一处

把 /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 的实际哈希。

关卡拦住了这件事,只会在那一刻留在屏幕上。要想日后追问“当时为什么被拦住”,就必须把判定和它的依据一起记下来。