签名验证通过了,可那份证明说的是另一个文件
一句话总结
供应链证明材料(SBOM、in-toto Statement、SLSA Provenance)要成为依据,不在于签名是否正确,而在于确认该文档所指向的 subject 的 digest,是否与我手里这个文件的指纹相同。签名只是在说文档没有被改动,而把交付物与文档连在一起的不是签名,而是 digest。
为什么需要它
隔离网络的导入审查,大多是这样运转的:一个介质进来,里面除了交付物,还有几份文档。物料清单(SBOM)、构建履历(Provenance)、测试通过确认书。审查经办人运行一次签名验证命令,盖上“验证通过”的章。流程文件上也是这么写的。
但把这条命令实际确认了什么拆开来看,它确认的只是:某个文档文件自从被签名之后没有变过。交付物根本没有出现在这条命令中。所以,如果把一份签名有效的证明材料,附在上个季度已经批准的旧版本上,再随附一个新的二进制文件一起送来,只看签名的流程就会原样放行。能修改文档的一方也能重新附上签名,这个事实,一旦把签名作为流程的最后一道关卡,就被整个漏掉了。
随着证明材料的增多,这个陷阱反而变大了。文档只有一份时,人会打开看;变成三份、五份,就由工具来扫描,而人只看结果。工具确认了什么、没有确认什么,没有人再去追问。
工作原理
把证明材料分成三层来看,就不会混淆。
- 文档层——这份文档自签名之后有没有被改动。由签名验证来回答。
- 绑定层——这份文档所说的对象,是不是我收到的那个文件。由 subject 的 digest 来回答。
- 主张层——于是它主张了什么。由 predicate 来回答。
in-toto Statement v1 用四个字段原样承载了这个结构。_type 始终是 https://in-toto.io/Statement/v1,subject 是这份证明所适用的交付物的数组,predicateType 是指向主张类型的 URI,predicate 是主张的内容。文档明确规定 subject 的每个元素都必须具有 digest,并另外写明:交付物只靠 digest 来匹配,而不是靠名称。名称只是用来区分的标签,改写了也不会发生任何事。subject 的元素是名为 ResourceDescriptor 的通用格式,uri、digest、content 中至少要有一个。
当 predicateType 为 https://slsa.dev/provenance/v1 时,predicate 就是 SLSA Provenance v1。其中包含 buildDefinition 和 runDetails 两大块。buildDefinition 中有指向构建模板的 buildType、来自外部的输入 externalParameters、由平台自行放入的 internalParameters,以及构建所引用内容的清单 resolvedDependencies。runDetails 中有指向运行构建的平台的 builder.id,以及 metadata 中的 invocationId、startedOn、finishedOn。规范区分了:externalParameters 不可信,必须在下游加以验证;internalParameters 因为已经信任该平台,所以无需验证。这个区分之所以重要,只有一个原因——它意味着文档事先把需要验证的位置和需要信任的位置分开了。
另外,Provenance 有些东西是不会说的。SLSA 等级文档中写道,Build 轨道的目的,是能够验证“交付物是否按预期构建”。它处理的是由谁、通过什么过程制作,但那份源代码是否经过评审,依赖项中是否存在已知漏洞,并不是 Build 轨道的工作。文档自己也说,L1 只要求证明材料存在,伪造也很容易。L2 开始有专用基础设施和签名,L3 则使签名密钥在构建步骤中无法触及。所以,“有 SLSA 证明材料”这句话,如果不带等级,几乎等于什么都没说。
物料清单以 CycloneDX 或 SPDX 的形式送来。CycloneDX 1.6 JSON 中,bomFormat 为 CycloneDX,specVersion 是版本号,components 是组件数组,每个组件都必须有 type 和 name。指纹以 {"alg": "SHA-256", "content": "..."} 的形式放在 hashes 数组中。人们在这里经常被绊住的地方是 metadata.component。它是这份 SBOM 所描述的对象本身,而不是组件清单中的元素。
最后,隔离网络还会夺走一样东西。Sigstore 式的流程以把签名上传到透明度日志、由验证一方查询该日志来确认为前提,而如果没有通向网外的路,这种查询就无法成立。所以,在导入审查报告中,必须把“已确认”和“在这个网络中无法确认”分开写。
문서 층 서명 검증 → 이 문서가 안 바뀌었다
결속 층 subject.digest → 이 문서는 '이 파일' 얘기다 ← 여기가 비면 나머지가 다 헛것
주장 층 predicate → 그래서 이렇게 만들어졌다고 주장한다
在现场相遇的样子
最常见的事故是调换证明材料。签名有效,但 subject 指向了另一个文件。让它指向旧版本,就成了“上次通过的那份证明材料”,反而更容易通过。抓住这种情况的方法只有一个:从收到的文件重新计算指纹,与 subject 比对。流程文件里如果没有这一行,其余的验证都只是装饰。
第二种是只单向比对 SBOM。以文档为基准扫描,文档没有提到的文件会悄悄进来;以目录树为基准扫描,就看不到只存在于文档中的幽灵条目。名称在两边都有但指纹不同的情况又是另一类,只比对清单的做法会把它整个漏掉。
第三种是遇到不认识的谓词(predicate)时。如果 predicateType 是我们读不懂的 URI,那么这份文档不能作为通过的依据,但也不是拒绝的依据。in-toto 的解析规则建议把策略写成单调的——不是“只要有坏的证明材料就拦截”,而是“必须有好的证明材料才通过”。这样写的话,只会因为读不了一份证明材料而使通过被挡住,而不会因为读不了而使通过被放开。
第四种是公钥。如果验证所用的公钥与证明材料随同一个介质送来,那么掌握该介质的一方,就可以把文档、签名和公钥一起换掉。签名验证的结果仍然是通过。所以审查记录中,要把签名有效这一事实,与该密钥属于发包方这一确认,分开书写。后者如果没有通过另外途径收到的指纹,就无法确认。
下一项实验要做什么
你将在网络内重现发包方送来的交付物和三种证明材料,并分八步进行比对:交付物指纹与 subject 的比对,SBOM 的双向比对,区分 Provenance 能回答和不能回答的问题,一次签名验证,然后亲手做出一份签名有效但 subject 指向旧版本的证明材料,从两个层面复现“签名 OK,比对 FAIL”。最后留下一份把已确认和未能确认分开书写的导入审查报告。