供应链证据模型、验证链与工具组合选型手册
SolarWinds Orion 事件说明,受信厂商的更新通道也可能把被篡改的构建结果送到大量消费者手中。CISA 的紧急指令 21-01要求机构识别、隔离并移除受影响产品;困难并不只是找到一个文件名,而是回答哪些版本、哪些摘要、哪些构建来源已经进入哪些环境。发布者身份、签名存在和流水线绿色状态,都不能单独证明某个字节序列来自预期构建过程。
Codecov Bash Uploader 事件则把风险推到构建入口:攻击者修改下载脚本后,可以从 CI 环境导出凭据。即使最终制品后来被正常签名,签名也只能证明某个身份认可了该摘要,不能倒推出材料没有被替换、构建参数没有泄密或 producer 仍在可信边界内。证据系统必须把不可变 subject、声明类型、生产者身份、验证者、信任根、消费政策和保留责任分开,否则一次有效验签很容易被误写成整条供应链可信。
从一个摘要开始,而不是从产品清单开始
供应链证据回答的是一个带主语的问题:关于 registry.example.com/payments/api@sha256:...,谁在什么信任边界内作出了哪类声明,验证者依据哪一版根和政策得出了什么结论? 如果输入仍是 :stable、压缩包文件名或流水线运行号,后续工具再多也只能围绕可变别名建立关系。OCI 场景先解析 manifest digest;普通文件先计算并固定 SHA-256;多架构镜像还要区分 index digest 与平台 manifest digest。
证据不能合并成一个 trusted=true。Provenance 说明构建来源与构建器,SBOM 描述组件图,VEX 对具体产品与漏洞给出状态,测试声明记录某项检查,签名或 DSSE envelope 提供真实性与完整性。它们可共享同一 subject,却有不同 schema、producer、时效和失败语义。验证结果也应保存命中的 evidence digest、identity、issuer、predicate type、policy version 和 trust-root snapshot,使第二个验证者能够复算。
这套顺序直接改变采购问题。先问“需要对什么主体作出什么决定”,再选择格式、签名系统、发现方式和查询平台。反过来按产品功能拼装,常见结果是 Registry 里堆满附件,却没人能解释缺一份 SBOM与拿到一份坏 SBOM为什么会得到不同原因码。
七层对象各自保存什么事实
Subject 是被声明对象的不可变身份。in-toto Statement 的 subject[] 至少包含 digest;OCI descriptor 还带 media type、size 和可选 annotations。名称用于定位和阅读,digest 才是安全主键。算法名是身份的一部分,sha256:abc 不能和裸 abc 或另一个算法的值比较。
Predicate 是声明内容及其 TypeURI。SLSA Build Provenance v1 使用 https://slsa.dev/provenance/v1,Statement 层使用 https://in-toto.io/Statement/v1。验证者应先允许 predicate type,再按对应 schema 和业务约束解析;未知类型不可当成“已有 attestation”。in-toto Statement v1明确了 subject、predicateType 与 predicate 的边界。
Producer 是生成并认证声明的工作负载、构建器或发布者。它不是一个自由文本 author,而是可由证书、KMS key、OIDC issuer 与 subject、workflow identity、builder ID 等材料验证的身份。producer 还要被授权给具体 repository、build type、predicate 和环境,合法证书并不自动拥有生产发布权。
Verifier 负责发现证据、验证 envelope、证书链、透明日志或密钥,再检查字段。Cosign、Notation、slsa-verifier、Ratify verifier 和组织自建服务承担的协议不同。verifier 版本与配置本身也是供应链依赖,输出不能只有布尔值,应有稳定原因码和输入摘要。
Trust root 是验证依据,不是附件。它可能是 Sigstore TrustedRoot、Notation trust store、KMS 公钥、TUF root.json 或组织离线根包。根有版本、有效窗口、阈值与分发路径;旧根过期、元数据过期、历史日志 key 缺失都必须可观察,不能悄悄降级到“只验签名”。
Policy 把“密码学有效”变成“业务允许”。它约束 digest、identity、issuer、builder、source、predicate type、阈值、时间、例外和失败模式。policy-as-code 应有不可变版本或 commit digest,验证日志需记录实际生效版本,而不是只记仓库分支名。
Retention 保证未来仍能复核。至少保存 subject manifest/bytes、原始 envelope 或 bundle、证据 manifest 与 blobs、根快照、政策版本、裁决日志和撤销记录。图谱里的规范化边、Registry referrer 和透明日志条目都不能单独替代完整证据包。
最小工具链先跑通四个进程
一个可工作的开发基线不需要先部署私有 CA 和大型图数据库。准备一个支持 OCI 1.1 Referrers 的测试 Registry、固定版本的 Cosign 3.x 与 ORAS、一个 OPA 进程,再用普通对象存储或版本化目录归档 bundle、根和决策日志。安装包、容器镜像和策略镜像均固定版本及 digest;生产采用前从 Cosign 安装入口和目标 Registry 的官方兼容矩阵核对平台支持。
# 固定测试镜像的不可变主体
IMAGE="registry.example.com/lab/evidence-demo"
docker build -t "$IMAGE:trial" .
docker push "$IMAGE:trial"
DIGEST="$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE:trial")"
# 生成并保留标准 bundle;自动化环境由工作负载身份提供 OIDC token
cosign sign --yes --bundle evidence.sigstore.json "$DIGEST"
cosign verify "$DIGEST" \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER"
# 查看与 subject 关联的 OCI 证据;生产脚本必须处理分页和旧 Registry fallback
oras discover --format json "$DIGEST" > referrers.json
sha256sum evidence.sigstore.json policy.rego trusted-root.json > retention.sha256这段入口只证明签名链能跑通。下一步必须让 verifier 同时检查精确 identity、issuer、predicate type 和 subject digest;OPA 接收结构化 verifier report,而不是自己解析 X.509 或重写 DSSE。测试环境可使用托管 Sigstore;私有部署会额外承担 OIDC、Fulcio、CT log、Rekor/TSA、TUF、密钥托管、HA、备份和轮换,不能用一条 Helm 命令估算全部成本。
产品组合按责任边界取舍
轻量团队通常选择“托管 CI attestation + 托管 Sigstore/平台根 + OCI Registry + CLI verifier + policy-as-code”。优势是签发基础设施少、上线快;代价是身份元数据与透明日志披露、平台绑定、离线材料导出和私有仓库能力受计划限制。此组合适合先证明 digest、identity 和 predicate 门禁,不能把平台品牌当 producer allowlist。
多云或自托管团队可组合“Cosign/Notation + KMS 或 keyless 私有信任域 + OCI Referrers + Ratify/独立验证服务 + OPA”。Cosign 适合 Sigstore bundle、OIDC identity 与透明日志链;Notation 适合 Notary Project trust policy、插件和企业证书体系。二者并非高低排名,真正的选择点是身份来源、是否需要透明日志、离线验证、插件信任、已有 PKI/KMS 和消费者生态。双栈能覆盖迁移期,却会成倍增加根、策略、缓存和回归样本。
需要跨制品、组件、漏洞和构建器做影响分析时,再增加 GUAC;需要标准化元数据 CRUD 与 provider/consumer 项目边界时,可评估 Grafeas。GUAC 的图边和 Grafeas Occurrence 都不自动等于已验证证据,必须保存来源与验证状态。图平台位于查询层,不应进入每次部署的同步密码学关键路径;否则一次图查询抖动就会扩大为所有发布停摆。
Registry 只负责内容与 referrer 发现。OCI subject 是弱关联,OCI Distribution Spec 1.1的 Referrers API不承诺 GC、复制、保留或撤销。若产品不支持原生 API,客户端 fallback index 还有并发覆盖风险。选 Registry 时应实测递归复制、附件保留、分页、权限和 GC,不以“支持 OCI”四个字替代实验。
配置字段会怎样改变信任结果
一份可审计配置至少显式表达以下字段,字段缺失应在加载阶段失败,而不是采用宽松默认值:
apiVersion: trust.example.io/v1alpha1
kind: EvidencePolicy
metadata:
name: production-artifact
spec:
subject:
algorithms: [sha256]
requireDigest: true
repositoryScopes: ["registry.example.com/payments/*"]
predicates:
required:
- type: "https://slsa.dev/provenance/v1"
minimum: 1
- type: "https://cyclonedx.org/bom"
minimum: 1
producers:
identities:
- issuer: "https://token.actions.example"
subject: "repo:payments/api:environment:release"
builders: ["https://builder.example.com/isolated/v3"]
trust:
rootBundleDigest: "sha256:ROOT_BUNDLE_DIGEST"
requireTransparency: true
decision:
onMissingEvidence: deny
onVerifierError: deny
retention:
evidenceDays: 400
decisionDays: 400
legalHoldLabel: "supply-chain-hold"repositoryScopes 防止一个团队的有效签名横向授权到另一仓库;required[].minimum 防止“所有已出现报告均成功”的空集通过;identity 必须同时约束 issuer 与 subject;builder allowlist 要对应受控平台边界而非 runner 标签。rootBundleDigest让每次裁决可回放,requireTransparency改变对无日志签名的接受程度。onMissingEvidence 与 onVerifierError必须分开,前者是业务材料缺失,后者可能是 Registry 401、429、DNS、根更新或解析器故障。
留存天数不是越长越安全。证书 identity、仓库 URI、SBOM、VEX 和构建参数可能包含内部结构与个人信息;对象存储应分级加密、最小权限、审计访问,并支持 legal hold 与到期删除。保留原始证据用于未来重解析,同时保存规范化索引用于查询,二者生命周期与备份都要被明确计费。
第一组实验:错误摘要与错误身份
先为制品 A 生成 bundle,并固定精确 identity 与 issuer。正例必须在输出中看到 subject digest、匹配身份、信任根或透明日志验证结果;只有退出码 0 而没有这些字段,不足以作为审计证据。
# 正向:bundle、制品和身份三者一致
sha256sum artifact-a.bin
cosign verify-blob artifact-a.bin \
--bundle artifact-a.sigstore.json \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER" \
> verify-good.json
jq -e '.critical.identity != null or length > 0' verify-good.json
# 预期:verify 命令退出 0,输出可回指 A 的摘要与预期身份。# 反向 A:错误 digest,用 A 的 bundle 验证不同字节的 B
cp artifact-a.bin artifact-b.bin
printf 'tampered\n' >> artifact-b.bin
! cosign verify-blob artifact-b.bin \
--bundle artifact-a.sigstore.json \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER"
# 反向 B:摘要正确,但把 allowlist 改成另一个合法身份
! cosign verify-blob artifact-a.bin \
--bundle artifact-a.sigstore.json \
--certificate-identity 'repo:other/project:environment:release' \
--certificate-oidc-issuer "$EXPECTED_ISSUER"
# 预期:前者报告内容/摘要不匹配,后者报告证书身份不匹配;二者不能归并成 network_error。如果错误身份仍通过,先检查是否使用 .*、只校验 issuer、把证书 email 当稳定工作负载身份,或 policy engine 实际没有消费 verifier 的 identity 字段。如果错误 digest 仍通过,通常是验证命令仍指向旧文件、部署入口重新解析了 tag,或多架构 index 与平台 manifest 被混用。
第二组实验:缺失声明与过期根
这一组区分“签名有效但声明不全”与“验证依据已经失效”。准备一份同时含 SLSA Provenance 和 CycloneDX SBOM 的 referrer 集合,策略要求两种 predicate 各至少一份;根包固定 digest,并在隔离环境使用相同输入复验。
# 正向:要求的 predicate 都存在,根快照有效
oras discover --format json "$SUBJECT" > referrers-good.json
jq -e '
([.referrers[]? | select(.artifactType=="application/vnd.in-toto+json")] | length) >= 1 and
([.referrers[]? | select(.artifactType=="application/vnd.cyclonedx+json")] | length) >= 1
' referrers-good.json
policy-evaluate \
--subject "$SUBJECT" \
--evidence referrers-good.json \
--root trusted-root.json \
--policy policy.yaml > decision-good.json
# 预期:allow=true,并记录两类 evidence digest、policy digest 和 root digest。# 反向 A:删除 SBOM descriptor,模拟缺 predicate
jq '{referrers: [.referrers[] | select(.artifactType!="application/vnd.cyclonedx+json")]}' \
referrers-good.json > referrers-missing.json
! policy-evaluate --subject "$SUBJECT" --evidence referrers-missing.json \
--root trusted-root.json --policy policy.yaml
# 反向 B:使用过期或不覆盖签名时段的根快照
! policy-evaluate --subject "$SUBJECT" --evidence referrers-good.json \
--root expired-trusted-root.json --policy policy.yaml
# 预期:分别得到 MISSING_REQUIRED_PREDICATE 与 TRUST_ROOT_EXPIRED/NO_VALID_KEY;不得回退成 allow。policy-evaluate代表组织的验证服务或测试适配器,实施时替换为 Ratify、OPA 前置验证器或内部 CLI,并锁定其版本。关键不是命令名,而是反例必须到达真实解析与决策分支。过期根失败后应走受控根更新,不应临时关闭透明日志、跳过证书链或接受 embedded public key。
流水线、Registry 与 GitOps 怎样衔接
流水线在构建结束后立刻解析输出 digest,由受保护的 producer 生成 Provenance、SBOM 和签名;上传成功必须分别确认 formatting、signing 与 Registry push。任何一步部分失败都不能把 job 的最终绿色状态当作证据。测试阶段调用与生产同版本的 verifier,输出 decision record;发布系统只传递 digest 和证据索引,不传递可变 tag 作为信任结论。
Registry 中,subject manifest、evidence manifest、config/layers/blob 和 fallback index构成闭包。跨区域复制、晋级仓库复制、备份恢复与 GC 前后都用 oras discover 和逐 blob 拉取做集合比对。返回 404 可能表示 Registry 不支持原生 Referrers API,而不是“没有证据”;返回空列表、401、429、分页截断和 unsupported media type 要有不同指标。
GitOps 仓库只提交不可变 digest、策略版本引用和必要的 promotion record。调谐器负责把声明状态应用到环境,证据验证服务负责回答该 digest 是否满足该环境政策;不要把大量 bundle 塞进 Git,也不要让调谐器通过 tag 再解析一次不同 digest。证据丢失时,Git 中的历史允许记录不能替代当前可复验材料。
排障先定位是发现、验证还是裁决
“找不到签名”先查 subject 是否为同一 repository 下的同一 digest,再区分 Referrers API 空结果、404 fallback、分页、Registry auth 和 media type过滤。证据 manifest 存在但 blob 拉取失败,通常指向复制不完整或 GC;descriptor 有结果但没有匹配 verifier,则是 artifact type 或插件配置问题。
“签名有效却拒绝”依次查看 identity、issuer、repository scope、builder、predicate type、source ref、阈值和根有效窗口。把 verifier 原始报告与 policy decision 分开保存,可避免在 OPA deny message里丢失证书链原因。时钟偏差、TUF 元数据过期和历史日志 key缺失应归到 trust-root 类错误,而不是 producer 未授权。
“同一制品不同环境结论不一致”不一定是故障。先比较 subject digest,再比较 policy digest、root snapshot、evidence set和例外记录。若这些输入相同而结论仍不同,才调查 verifier 版本、缓存、规范化算法和并发读取。所有缓存以 digest 为 key,并在撤销、根轮换和策略更新后提供定向失效。
权限、敏感信息与成本账单
producer 只拥有写入自己 repository 证据的权限和最小 OIDC token 权限;verifier 只读 subject、referrers、公开验证 key 与策略;policy approver 不能同时修改 producer allowlist并批准自己的例外;retention operator 能归档却不应获得签名私钥。Registry 删除、legal hold、根发布和 break-glass 分别使用独立角色并进入审计。
证据可能暴露私有依赖、源码 URI、workflow path、builder 版本、个人邮箱、未公开漏洞和补偿措施。公有透明日志具有长期可发现性,上传前应做字段分级。SBOM/VEX 对外裁剪会产生新的衍生文档,需要新 digest、新签名和来源映射,不能修改原文件后沿用旧签名。
容量估算从“每个 subject 的证据数量 × 平均证据与 bundle 大小 × 副本数 × 保留期”开始,再加 Registry index、对象存储版本、图谱规范化膨胀和决策日志。验证成本还包括每次发布的 Registry GET、KMS/CA/TSA、透明日志、策略 CPU、缓存和跨区流量。高频单体仓库不要为每个步骤复制完整 SBOM;可保存一次内容寻址文档并由多个声明引用,但引用闭包必须可恢复。
HA、升级、迁移与退出要一起设计
同步发布链中的 verifier 至少跨故障域部署,设置有界 timeout、请求合并和 digest 缓存;Registry、DNS、根分发、KMS 与策略仓库都是依赖。HA 不是把副本数改成 3:还要演练 Registry 429、根源不可达、策略加载失败、单区丢失和缓存污染。图谱与离线分析可异步降级,生产阻断点则要有明确 fail-open/closed 语义和独立恢复通道。
升级按“格式解析器、verifier、policy schema、root bundle、Registry 行为”组成兼容集合。先在影子环境重放固定正反样本,比较原因码与 evidence set;新旧 verifier 双读,只有结果差异被解释后才切换。OCI fallback 升级到原生 Referrers API 时,先比较两边 descriptor 集合,再停止旧索引写入,不能先删 fallback tag。
迁移时导出 subject、原始 evidence、bundle、根历史、policy commits、decision logs和撤销记录,以 digest 清单校验后导入目标平台。双写期间由两个验证器对同一输入出具结果,旧平台先转只读历史验证,再停止签发。退出完成的判断是目标平台能在不访问旧 API 的隔离环境复验历史样本,旧 producer、旧根和旧写权限均已撤除。
清理与回滚保留调查价值
开发实验结束时删除测试 identity 的授权、吊销或停用测试 KMS key、移除临时 Registry repository、清理 OPA 测试策略和本地 OIDC 缓存。删除前先保存实验所需的非敏感输出与 digest;不要把真实 token、私钥、Docker config 或个人证书提交到仓库。透明日志通常不能像普通数据库一样删除,测试身份必须从一开始就可公开。
策略发布失败时回滚到上一版已签 policy bundle与对应 root snapshot,随后重放正反样本。不要只回滚 Rego 文件而保留新 schema,也不要回滚 TUF 元数据版本号造成更新攻击保护触发。Registry GC误删证据时,从版本化对象存储恢复完整闭包,重新执行 verifier并生成新的恢复事件记录,不能伪造原来的“首次验证时间”。
用决策问题收束机制选择
当团队只需要验证少量 CI 产物,托管 attestation、Cosign/平台 CLI 和严格 policy-as-code 通常足够;当身份来自企业 PKI、插件和离线环境是核心约束,Notation/KMS 与明确 trust store更自然;当 Kubernetes 入口需要统一发现多类 referrer,可引入 Ratify,但仍要治理零报告、缓存和 webhook 故障;当问题变成跨成千上万制品追踪组件与漏洞,再增加 GUAC 或元数据 API。
停止继续堆工具的条件也很具体:对任一 digest,团队能指出需要的 predicate、授权 producer、实际 verifier、根版本、政策版本、证据保留位置和拒绝原因;能稳定复现错误 digest、错误 identity、缺 predicate 与过期 root;能从备份恢复后离线复验;能在迁移后撤除旧信任。达不到这些条件时,应修复对象和责任边界,而不是再采购一个总览看板。
SLSA Attestation Model。in-toto Attestation Framework。Sigstore Bundle 格式
OCI Distribution Spec 1.1。Ratify Framework Overview。GUAC Components
