SLSA Provenance 与构建等级:从制品摘要追到可信构建
一次生产发布的镜像扫描、签名和部署门禁全部显示绿色,值班人员却在事故回溯时发现:镜像来自临时自托管 runner,而不是批准的托管构建池。攻击者没有伪造镜像摘要,也没有破坏签名;他只是让一条低信任流水线生成制品,再用合法发布身份给它附上了一份字段齐全的 provenance。验证器只检查“签名有效”,没有检查签名者是否有权代表该 builder.id,更没有检查仓库、引用和构建模板是否符合生产预期。
另一次故障更隐蔽。制品仓库里保存了同名 tag、SBOM 和 provenance,晋级作业却按 tag 下载镜像、按文件名查找证据。tag 被重新指向后,作业验证的是旧摘要对应的 provenance,部署的却是新摘要对应的镜像。所有文件都真实存在,关联关系却已经断裂。供应链信任必须从不可变 subject.digest 开始,再验证声明由谁作出、构建在哪里发生、外部参数是什么,以及这些事实是否满足消费端策略。
先把等级、轨道和验证属性分开
SLSA 是供应链完整性规范,不是需要安装的扫描器,也不是给产品颁发一次后永久有效的证书。当前 SLSA v1.2 Tracks 把不同威胁面放在独立轨道中。任何等级声明都要带轨道和规范版本,例如 SLSA Build L3 (v1.2) 或 SLSA Source L4 (v1.2);旧规范里的无轨道“SLSA 1 到 4”不能用来描述当前能力。
Build Track 从 L0 到 L3。L1 要求构建平台自动生成描述制品如何产生的 provenance,但它可以不完整或未认证;L2 要求托管构建平台自己生成并认证 provenance,消费端验证真实性;L3 再要求强化构建平台,使构建运行彼此不能影响,并让用户定义步骤无法接触 provenance 签名秘密。官方的 Build Track 等级说明 明确指出,等级代表递进保证,不代表“装过某个 Action”。
Source Track 从 L1 到 L4,分别关注版本控制、变更历史与 Source Provenance、持续执行的组织技术控制以及 two-party review。一个项目可以达到较高 Build Level,却仍从缺少审查的 source ref 构建;也可以有严格代码审查,却让开发者工作站直接上传发布包。生产策略应分别记录 Build 与 Source 结论,不取一个数字覆盖两条链。
VSA 与 Verified Property 又是另外两层。受信 VSA issuer 可以在 verifiedLevels 中写入 SLSA_BUILD_LEVEL_3,表示它已按某个 SLSA 版本和策略验证该制品达到对应轨道等级;这仍是等级验证结果,不是 Verified Property。SLSA_BUILD_REPRODUCED 才是独立属性,只有被引用制品具有至少两个由该 issuer 信任且独立运营的 Build Platforms 所生成的 build provenance 时才能签发。它不是 Build L3 的附赠条件,也不是“同一 CI 重跑两次”。Verification Summary Attestation 与 SLSA Verified Properties 分别定义了结果载体和额外属性,消费策略应把轨道等级与额外属性分开判断。
Statement、Predicate 与 Envelope 各守一层
SLSA Build Provenance 是 in-toto Statement 中的一种 predicate。Statement 负责把业务声明绑定到不可变 subject;Predicate 负责表达某类声明的字段;Envelope 负责序列化后的认证。把三者都叫“签名文件”,排障时就无法区分是摘要不匹配、schema 不符合,还是签名和信任链失败。
DSSE 或其他合规 Envelope
└─ payload: in-toto Statement/v1
├─ _type: https://in-toto.io/Statement/v1
├─ subject[]: 输出制品及 digest
├─ predicateType: https://slsa.dev/provenance/v1
└─ predicate
├─ buildDefinition
│ ├─ buildType
│ ├─ externalParameters
│ ├─ internalParameters
│ └─ resolvedDependencies[]
└─ runDetails
├─ builder
├─ metadata
└─ byproducts[]subject[].digest 是验证关联的安全主键,name、URI、tag 和文件名只能辅助阅读。Statement 的 predicateType 必须是 https://slsa.dev/provenance/v1;URL 中的 v1 是 predicate 主版本,不能因为规范页面是 v1.2 就改成 /v1.2。Build Provenance schema 还规定,小版本以向后兼容方式演进,同一个 TypeURI 解析到该主版本的当前语义。
DSSE 把 payload 原始字节与 payloadType 一起纳入签名,避免类型混淆,但它不提供身份签发、密钥保护、透明日志、撤销、可信时间或业务策略。keyid 只是未认证的查钥提示,授权结论必须来自真正通过验签的公钥或证书身份与外部策略。签名有效只能证明某身份认证过这些字节,不能证明 predicate 所述事实正确,更不能证明该身份有权代表某个 builder。
读懂构建定义和运行详情
buildDefinition.buildType 标识参数化构建模板。它应能解析到模板的字段定义、参数 schema、启动方式和完整示例,使验证者知道怎样解释 externalParameters。若平台把“受保护发布工作流”和“任意脚本执行”都映射成同一个 buildType,消费端便无法建立精确预期。
externalParameters 是租户或外部调用者控制的顶层接口,例如 repository、ref、target、发布模式。到 Build L3,这些参数必须完整枚举,不能还有租户可控的隐藏请求头、runner 环境变量或手工输入绕过记录去改变构建。验证者应拒绝未知字段,并检查仓库、引用和目标是否在允许集合中。internalParameters 由 builder.id 所代表的平台控制,可支持调试、重建和事件响应;它们不因为放在内部字段就适合公开,仍要做敏感信息治理。
resolvedDependencies 记录初始化或执行期间解析出的依赖资源及摘要,例如 source ref 最终解析出的 commit。它到 Build L3 仍是 best effort,数组存在不等于完整依赖闭包。构建脚本二次下载的工具、控制面下发的模板和影响结果的缓存若没有被记录,验证者不能从漂亮的 JSON 推导出“所有输入已经固定”。
runDetails.builder.id 代表忠实执行构建并记录 provenance 所必需的硬件、软件、人员和控制面的传递信任闭包,不是 runner 标签或编译器名称。安全模式不同的构建池必须使用不同 builder identity,并宜使用不同签名身份。metadata.invocationId 用于关联日志,startedOn 与 finishedOn 用于调查;byproducts 只保留有诊断价值的额外制品,不应把全量日志、环境变量或中间文件原样公开。
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{"name": "app.tar", "digest": {"sha256": "${ARTIFACT_SHA256}"}}],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://build.example.invalid/types/release/v1",
"externalParameters": {
"repository": "https://scm.example.invalid/payments/app",
"ref": "refs/tags/${RELEASE_TAG}"
},
"internalParameters": {},
"resolvedDependencies": [{
"uri": "git+https://scm.example.invalid/payments/app@refs/tags/${RELEASE_TAG}",
"digest": {"gitCommit": "${SOURCE_COMMIT}"}
}]
},
"runDetails": {
"builder": {"id": "https://build.example.invalid/builders/release-isolated/v1"},
"metadata": {"invocationId": "${INVOCATION_ID}"}
}
}
}启用生产端与消费端工具
启用 SLSA 的第一步不是寻找 install slsa,而是选择目标轨道和等级,评估 source control 与 build platform 的安全模型,再让可信控制面生成证据。用户脚本在自己的构建步骤末尾拼 JSON,天然只能提供较弱保证:同一个租户既能改制品,也能改声明。目标是让构建平台从实际调度状态、解析后的 source 和输出摘要生成 provenance,并把签名能力隔离在用户步骤之外。
消费端可以采用生态原生验证器或 Generic SLSA Verifier。名称中的 Generic 不表示它能验证任意 provenance:其官方支持矩阵限定了可识别的 builder 和生态,--source-uri 等参数也有实现约束。版本、支持范围和参数会随实现变化,安装时从 slsa-verifier 官方仓库 选择审核过的 release,固定版本与二进制校验和,并以该版本的 --help 和支持矩阵形成流水线契约。下面只给出官方 Go module 的安装形态,${VERIFIER_VERSION} 必须由依赖治理流程提供,禁止在生产使用 @latest。
set -eu
test -n "${VERIFIER_VERSION:?pin an approved verifier version}"
go install "github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@${VERIFIER_VERSION}"
slsa-verifier version
slsa-verifier verify-artifact --help验证工作负载只需要读取待验证制品、provenance、公开信任材料和策略;它不应持有 provenance 签名密钥、制品仓库删除权或生产部署管理员权限。生产端签发身份只允许平台控制面调用,用户构建容器不能挂载私钥、KMS 签名凭据或可代表高等级 builder 的 token。安装完成的验收不是 CLI 能打印帮助,而是正例通过、受控反例稳定拒绝,并且拒绝发生在预期层。
用正反实验验证完整消费链
先准备由该 verifier 明确支持的 builder 所生成的固定制品 artifact.bin 和 provenance,再准备批准的 GitHub source URI、tag 与 builder ID。其他 builder 或 source 类型不能照抄这组参数,必须选择其官方验证器或实现独立的 envelope、Statement 与策略验证链。下面的正向实验建立真实性、subject、source 与 builder 预期,而不是只解析 JSON。
# 正向实验:全部预期与实际证据一致
set -eu
slsa-verifier verify-artifact ./artifact.bin \
--provenance-path ./artifact.intoto.jsonl \
--source-uri "github.com/${ORG}/${REPO}" \
--source-tag "${EXPECTED_TAG}" \
--builder-id "${EXPECTED_BUILDER_ID}"
# 预期证据:退出码为 0,且输出明确包含签名、builder 与 artifact verification 通过事件。
sha256sum ./artifact.bin这些参数不通用校验任意 buildType、任意 externalParameters 或 VSA 中的 Verified Properties。需要这些门禁时,应在真实性验证成功后取得已认证的同一 Statement payload,交给理解目标 buildType schema 的策略引擎;策略测试必须分别保存 predicate type、build type、未知参数、仓库/ref 和所需 VSA 属性的允许或拒绝结果。不得对未验签 JSON 先做策略判断,也不得在验签后重新读取另一份 payload。
反向实验逐次只改变一个变量。先篡改制品一个字节,provenance 保持不变,应在 subject digest 处失败;恢复制品后,把 source 或 builder 预期改错,应在策略匹配处失败。再复制并修改 envelope 内已认证的 payload,不重签时应在真实性校验处失败。每次都保存退出码和结构化错误类别,但不要把证书、完整内部 URI 或 payload 中的秘密打进公开日志。
# 反向实验一:制品摘要不再匹配
set -eu
cp artifact.bin artifact.tampered.bin
printf 'x' >> artifact.tampered.bin
if slsa-verifier verify-artifact ./artifact.tampered.bin \
--provenance-path ./artifact.intoto.jsonl \
--source-uri "github.com/${ORG}/${REPO}" \
--source-tag "${EXPECTED_TAG}" \
--builder-id "${EXPECTED_BUILDER_ID}"; then
echo "ERROR: tampered artifact was accepted" >&2
exit 1
fi
# 反向实验二:真实证据不满足消费端 builder 预期
if slsa-verifier verify-artifact ./artifact.bin \
--provenance-path ./artifact.intoto.jsonl \
--source-uri "github.com/${ORG}/${REPO}" \
--source-tag "${EXPECTED_TAG}" \
--builder-id "https://build.example.invalid/builders/not-approved/v1"; then
echo "ERROR: unexpected builder was accepted" >&2
exit 1
fi还有一种反例不会自动报错:省略业务必需的 builder、source 或 external parameter 约束,验证器可能仍然成功验签。它证明的只是策略过宽,不是制品可信。测试仓库应保留这类“本应拒绝却被宽策略接受”的 fixture,确保策略变更不会静默降低门槛。
把证据生产和晋级策略接入流水线
发布流水线应按不可变摘要传递制品。构建阶段输出 artifact digest 和 provenance;上传阶段检查二者绑定后写入支持不可变引用的存储;晋级阶段重新按 digest 拉取,并先验证 envelope 与 subject,再把已认证 Statement 交给策略层检查签名者、builder、buildType、external parameters、source revision 与所需 VSA 属性。部署系统只接收已验证 digest,不接收可漂移 tag 作为最终身份。下面是策略输入的数据模型,不是任何现成验证器可直接加载的配置文件。
releasePolicy:
subject:
digestAlgorithm: sha256
provenance:
predicateType: https://slsa.dev/provenance/v1
signerBuilderPairs:
- signer: ${TRUSTED_SIGNER_IDENTITY}
builder: https://build.example.invalid/builders/release-isolated/v1
buildTypes:
- https://build.example.invalid/types/release/v1
externalParameters:
repository: https://scm.example.invalid/payments/app
refPattern: refs/tags/release-*
source:
requiredTrack: Source
minimumLevel: ${SOURCE_LEVEL_POLICY}
decision:
failClosed: true生产者与消费者之间需要明确 schema owner。平台团队维护 builder identity、安全模型、签名和生成器;项目团队维护允许的 repository、ref、buildType 和参数;安全团队维护 trust root 与例外审批;发布系统保存最终 decision、policy revision、subject digest 和证据摘要。任何一方都不能通过扩大自己掌管的通配符绕过另一方。
证据服务不可用时,生产晋级通常应 fail closed;开发预览可以按风险使用降级策略,但必须与生产 builder 和 signer 分离。紧急发布不能改成“只要有签名就过”,而应使用限时、双人批准、限定 digest 的例外,并在事后补齐验证和撤销例外。观察指标包括生成失败率、验证错误类别、未知 builder、意外参数、证据查找延迟和按策略版本统计的拒绝量。
保护权限、元数据与信任边界
provenance 经常暴露私有仓库路径、项目和租户名称、分支、工具版本、基础镜像、内部域名、构建时间、员工或服务身份。internalParameters、dependency URI、annotations、byproducts.content 与日志引用尤其敏感。DSSE 的 base64 不是加密,签名也不提供机密性;发布 envelope 等同于发布 payload。
生成器应使用字段 allowlist,从源头禁止采集 token、环境变量全集、命令 stdout/stderr 和带凭据 URL。内部详细 provenance 可以受控保留,外部消费者只得到满足验证所需的证据或经过授权签发的 VSA;但摘要化证明不能成为删除原始调查证据的借口。日志记录 digest、builder ID、策略版本和错误类别即可,完整 payload 放入按角色授权、加密和有保留期限的证据库。
权限上至少分离四个身份:构建工作负载读取固定输入并写临时输出;平台控制面读取运行事实并请求 provenance 签名;发布器按 digest 写制品与证据;验证器只读证据并输出决策。签名者与 builder.id 必须组成允许对,不能让一个通用组织签名身份替任意 builder 作证。删除、覆盖和保留策略由独立存储权限约束,避免构建服务账号在事故后清除证据。
计算容量、可用性与证据保留成本
provenance 生成本身通常比编译便宜,但规模化后会增加序列化、签名/KMS、证据上传、Registry 或对象存储、索引、验证查询、审计和跨区域复制成本。容量模型应以构建次数、每次 subject 数、predicate 与 byproduct 大小、签名调用峰值、验证 QPS、保留周期和复制因子估算,而不是只看单份 JSON 的大小。
控制面高可用需要覆盖构建调度状态、签名服务、证据存储、索引和信任根分发。签名服务故障时不能退回用户步骤自签;索引故障时可以按已知 digest 直查原始证据,但不能把“搜索不到”解释为“证据不存在”;复制延迟期间,晋级策略应区分暂时不可用与验证失败。构建高峰和故障恢复会放大 KMS 与上传流量,客户端要有上限退避、随机抖动和幂等写入。
保留时要把制品、provenance、签名 bundle、policy decision、builder 安全模型版本和必要日志作为关联集合。只保留 provenance 而让制品被垃圾回收,无法重验;只保留制品而删除旧信任材料,无法解释当时为何放行。高基数 invocationId 不应直接成为无界指标标签,适合进入日志或检索索引。费用、服务配额与区域能力变化较快,部署时从所选存储、KMS 和构建平台的官方入口建立预算与告警。
升级、迁移、回滚与退出都围绕可验证身份
从 provenance v0.2 迁移到 v1 时,不能只改 predicateType。旧 invocation.parameters 对应 buildDefinition.externalParameters,invocation.environment 对应 internalParameters,materials 迁到 resolvedDependencies,运行元数据进入 runDetails.metadata;metadata.completeness 和 metadata.reproducible 已从 v1 移除。迁移器应同时保留原始证据,在候选环境把旧新解析结果映射为同一策略输入,再用正反 fixture 比对决策。
构建平台升级可能改变 builder 安全属性、buildType、签名身份、参数完整性或依赖采集语义。安全模式变化时发行新 builder.id,新旧验证策略短期双接受,但分别统计;先让候选 builder 生成影子制品和 provenance,比较摘要、字段、验证结果与敏感信息,再逐批切换生产者。回滚必须恢复生成器、签名身份、builder 文档和消费策略的匹配,不能只回滚 runner 镜像。
退出一个构建平台时,先停止新构建,撤销它的签发权限和上传权限,再观察是否还有 invocation;保留旧证据、验证信任和 builder 安全模型直到相关制品退出保留期。随后从生产策略删除 signer-builder 对,销毁专用密钥,删除临时缓存和孤立证据副本,并用旧 builder 新生成的测试 provenance 做反向验证,预期不能再获得生产放行。
最终可核销证据应回答:哪些 digest 仍依赖旧 builder,旧签名是否只能验证历史而不能签发新证据,旧 source/buildType 是否还在允许集合,存储副本和备份何时销毁,谁对历史审计负责。这样,SLSA 才从一张等级标签变成持续运行的制品信任合同。
SLSA v1.2 规范入口。SLSA Build Track 等级。SLSA Source Track 生产要求
SLSA Build Provenance。SLSA 制品验证要求。SLSA Verification Summary Attestation
