in-toto Statement、Attestation 与 Layout:把供应链步骤变成可验证证据
一次发布事故中,编译、打包和签名三个步骤都有各自的成功记录,最终归档包的签名也完全有效,但调查人员发现打包人员在两个步骤之间替换了二进制文件。签名只能证明打包人员签过最终字节,无法证明这些字节就是编译步骤交出的产品。验证端没有比较上一步 products 与下一步 materials 的摘要,三张真实记录没有形成连续供应链。
另一次事故发生在供应链密钥轮换之后。验证服务仍接受一份尚未过期、签名有效的旧 Layout,攻击者重放旧策略以及与它匹配的历史 Link,绕过了新 Layout 增加的双人阈值。in-toto 能检查 Layout 签名和过期时间,却不天然知道“当前应该使用哪一版 Layout”;若分发层没有版本单调性、撤销或可信更新机制,形式正确的旧策略仍可能被重新使用。
两套模型解决的是两类问题
in-toto Attestation Framework 用于表达“某个主体被作出了什么机器可读声明”。Predicate 定义业务字段,Statement 把 Predicate 绑定到一个或多个不可变 subject,Envelope 对序列化后的 Statement 做认证,Bundle 再组合多份 Attestation。SLSA Provenance、SBOM、漏洞扫描结论都可以成为不同的 Predicate,但它们不能因为都装进 DSSE 就被当成同一种事实。
经典 in-toto Layout/Link 工作流用于表达“供应链应由谁按什么步骤执行,以及实际发生了什么”。项目 owner 签署 Layout;functionary 为自己执行的步骤生成 Link;验证者按 Layout 检查授权、阈值、命令和材料/产物规则,并运行 Inspection。Attestation 的 Statement 不是 Layout,Predicate 不是 Link,Envelope 的多签数组也不是某一步的 threshold。
Attestation Framework Layout/Link workflow
Envelope signed Layout
└─ Statement ├─ steps + authorized keyids
├─ subject[digest] ├─ threshold + artifact rules
├─ predicateType ├─ signed Links
└─ Predicate └─ local Inspections这种分离决定了排障顺序:先确认正在验证哪组字节和哪个 subject,再确认认证身份,最后判断声明或步骤是否满足策略。只问“签名是否有效”会跳过最重要的两层。
Statement、Predicate 与 DSSE Envelope 的边界
Statement/v1 的必要字段是 _type、subject 和 predicateType,predicate 可以省略。每个 subject 必须有 digest;匹配只认摘要,不感知文件名、媒体类型或包坐标。策略若关心镜像仓库、包名或平台架构,必须显式检查相应字段,不能从相同摘要之外推断业务身份。
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{
"name": "dist/app.tar.gz",
"digest": {"sha256": "${ARTIFACT_SHA256}"}
}],
"predicateType": "https://example.invalid/predicate/release-review/v1",
"predicate": {
"sourceCommit": "${SOURCE_COMMIT}",
"reviewed": true
}
}Predicate 的 TypeURI 是解析和策略分派入口。验证器必须先验证 Envelope,再从已经验签的同一 payload bytes 解析 Statement,检查 _type 和 predicateType,最后校验 Predicate schema 与业务约束。不能验签一份 payload,随后重新从未认证输入取另一份 JSON 交给策略。
DSSE 对 PAE(payloadType, payload) 签名,payloadType 因此受认证并能抵抗类型混淆。Envelope 中的 payload 与 sig 使用 Base64 只是为了传输二进制,不是加密;任何拿到 Envelope 的人都能解码 Statement。keyid 更容易被误用:它是未受签名认证的查钥提示,攻击者可以改写它而不破坏原签名。授权必须来自实际通过验签的可信公钥或证书身份,不能用 keyid == release-key 直接放行。
Layout、Link、Inspection 与 threshold 怎样协作
Layout 由 owner 签名,包含过期时间、functionary 公钥、步骤顺序、授权 keyids、期望命令、材料与产物规则以及 Inspections。Link 是某个 functionary 对实际步骤的签名记录,常见字段包括 step name、command、materials、products、byproducts 和环境信息。Layout 表达期望,Link 表达观察结果;一个签名有效的 Link 仍可能因 step name、授权身份或 artifact rules 不匹配而被拒绝。
threshold: 2 表示同一步至少要有两个不同的授权 functionary 提供有效 Link。验证器应按实际通过验签的不同可信 key 去重,拒绝同一身份复制两份 Link。DSSE Envelope 里有两个 signatures 只表示同一 payload 携带两个签名,它不会自动满足 Layout 某一步的阈值,更不会证明两个独立执行者真的运行过该步骤。
Inspection 在验证端本地执行命令,再把它产生的材料/产物纳入规则检查。它适合检查解包后文件、测试最终制品或生成二次观察,却也是代码执行入口。不可信 Layout 等同于潜在远程代码执行输入:验证服务必须固定 Layout 来源,在无生产凭据、只读输入、受限网络和受限文件系统的隔离环境运行 Inspection,并限制 CPU、内存、进程数和超时。
从隔离环境安装并建立第一条链
Python 参考实现的版本与 in-toto 规范版本是两件事。安装前从 in-toto 安装文档 和发行页选择经审核版本,固定依赖哈希;不要把 pip install 当成信任建立。实验目录不能放生产私钥,官方 demo 密钥也不能复制到真实流水线。
set -euo pipefail
python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "in-toto==${IN_TOTO_VERSION}"
in-toto-run --version
in-toto-verify --help >/dev/null
mkdir -p work keys metadata
chmod 700 keys接着由 owner 生成并离线保管 Layout 签名密钥,为每个 functionary 使用独立密钥;编译、打包、审阅不应共享一个身份。由 owner 定义步骤及 artifact rules,functionary 用 in-toto-run 或 in-toto-record start/stop 包裹真实命令,最后把 Link 与制品交给独立验证环境。命令的确切 key type 和参数随实现版本核对,生产基线应保存 --version、锁文件和工具自身摘要。
正向实验:证明材料与产物连续
官方 in-toto demo 提供了可运行的多步骤样例。下面的实验强调验收证据,而不是只看最终一行 PASSING。先在干净目录执行各步骤,保存每个 Link,然后验证 owner 签名、Layout 有效期、functionary 授权、阈值、artifact rules 和 Inspection。
set -euo pipefail
python run_demo.py
in-toto-verify \
--layout root.layout \
--verification-keys alice.pub \
2>&1 | tee verify-positive.log
grep -q "PASSING" verify-positive.log预期退出码为 0。进一步检查 compile Link 的 products 与 package Link 的 materials:同一路径摘要必须一致;Layout 中 MATCH ... WITH PRODUCTS FROM compile 一类规则才是真正把两步连起来的条件。最终归档摘要还要与发布系统使用的 digest 一致,不能在验证后重新按可变 tag 或文件名取件。
将两名授权审阅者配置到同一 review step,threshold 设为 2,分别生成 Link 后再次验证。预期两个不同可信身份均通过;复制同一人的 Link、只改变文件名不得满足阈值。日志应保留 Layout digest、有效 Link digest、实际验签 key、匹配规则和最终 subject digest,避免只保留布尔结果。
反向实验:断链、伪身份与重放
先在 update-version 与 package 之间修改一个材料文件,再重新打包,但不要重新生成上一步 Link。最终包可以被打包者合法签名,验证仍必须在摘要连续性规则处失败。
set -euo pipefail
cp foo.py foo.py.before
printf '\n# unauthorized change\n' >> foo.py
if in-toto-verify --layout root.layout --verification-keys alice.pub \
>verify-tampered.log 2>&1; then
echo "ERROR: broken material/product chain was accepted" >&2
exit 1
fi
grep -Ei "MATCH|material|product|rule|verification" verify-tampered.log
mv foo.py.before foo.py预期是非零退出,证据指向材料/产物规则,而不是笼统“签名错误”。再删除阈值所需的一份 Link,应在 threshold 检查失败;换未授权密钥重签 Link,应在 functionary 授权失败。两者都不能通过增加 Envelope 签名数量来修复。
重放实验要准备 layout-old 与 layout-current:旧版允许一人签署,当前版要求两人;两份都由 owner 合法签名且旧版尚未过期。若验证入口允许调用者自行提交任意 Layout,旧版及其历史 Links 可能再次通过。修复不是改 Link,而是让验证端从受控更新仓库按项目与版本解析唯一当前 Layout,并保存已接受版本或 digest;回退到旧 digest 必须拒绝或进入显式应急流程。TUF 等可信更新机制可提供版本、阈值与回滚/冻结防护,DSSE 本身不提供重放防护。
DSSE 实验:keyid 改了,签名为什么还有效
取一份合法 DSSE Envelope,先解码 payload,确认它就是待解析 Statement;再仅改 keyid,不改 payload、payloadType 和 sig。使用“遍历本地可信公钥并实际验签”的验证器时,密码学验证仍可能成功,因为 DSSE 的签名输入没有包含 keyid。
set -euo pipefail
jq -r '.payload' statement.dsse.json | base64 -d > statement.json
jq '.signatures[0].keyid = "release-admin"' \
statement.dsse.json > keyid-tampered.dsse.json
# 使用项目固定的 DSSE 验证器验证两份 envelope;两者可能都验签成功。
dsse-verify --key trusted.pub statement.dsse.json
dsse-verify --key trusted.pub keyid-tampered.dsse.json
# 授权决策读取实际命中的 trusted.pub 身份,而不是外层 keyid。
jq -e '.predicateType == "https://example.invalid/predicate/release-review/v1"' statement.json反过来,只改 payloadType 或 payload 任一字节,PAE 输入发生变化,验签必须失败。若工具在 keyid 变化后把签名者身份改成管理员,说明它把未认证提示混进了授权;若 Base64 解码后出现凭据、内部 URL 或客户数据,说明采集策略泄密,不能用“已经签名”解释为机密。
接入流水线时让生成与验证分权
流水线中的 functionary 步骤负责记录自己的 materials、products 和 command,owner Layout 的签发应在独立受控仓库或审批流程完成。构建租户不能同时修改 Layout、替换验证公钥、生成 Link 和决定最终放行,否则所有对象格式都正确也没有独立信任边界。发布阶段只接收已经验证的不可变 digest,并保存 Layout digest 与 Link 集合的关联。
推荐把链路拆成四个权限域:构建身份只能读声明材料并写工作区;Link 签名身份只能签对应 step;owner 身份只能签 Layout;验证身份只读 Layout、Links、公钥和制品并输出决策。Inspection 使用第五个隔离身份,无 Registry push、KMS sign 或生产 Secret 权限。私钥由 KMS、硬件密钥或受控 Secret 提供,日志只记录 key reference 和实际验证身份,不输出私钥、token 或完整环境。
Predicate、Link 的 command、stdout/stderr、byproducts、路径和环境都可能暴露内部仓库与凭据。生成端应采用字段允许列表和脱敏,验证日志按 digest 与低基数原因码索引;DSSE payload 公开时就按公开数据处理。
排障要按字节、认证、授权、规则和执行分层
“找不到 Link”先检查文件命名、step name、工作目录和归档是否完整;“签名无效”检查验证的是哪组原始 bytes、实际公钥、算法和序列化边界;“签名有效但未授权”检查 Layout 的授权 keyids 与实际验签身份;“规则失败”比较每步 materials/products 的路径规范化与 digest;“Inspection 失败”再看隔离环境、依赖命令、退出码、超时和网络策略。
Layout 过期与验证节点时钟错误会表现相似,应同时记录可信时间来源和 Layout digest。一个节点通过、另一个节点失败,优先比较公钥集、当前 Layout 解析结果、工具版本、工作目录内容和 locale,而不是重新签名掩盖差异。不要把 --ignore、降低 threshold 或删除 artifact rules 当成排障动作;它们改变的是安全策略。
容量、成本与高可用围绕验证工作量设计
验证成本近似由步骤数、每步 Link 数、阈值签名数、材料/产物文件数和 Inspection 资源消耗共同决定。大仓库逐文件哈希会增加 I/O,重复 Link 与长 byproducts 会增加对象存储和审计成本,Inspection 还可能成为 CPU、网络与第三方服务的放大器。容量测试应记录 p50/p95/p99 验证延迟、哈希字节数、规则数、Inspection 超时率、失败原因与内存峰值。
高可用验证器应是无状态副本,共享只读且版本固定的 Layout、公钥和策略快照;缓存键至少包含 Layout digest、Link digest、artifact digest、验证器版本和策略版本。只按项目名缓存成功结果会在 Layout 更新后复用旧决策。副本数不能修复错误 owner key、被重放的 Layout 或共享存储损坏,必须另有可信分发、备份恢复和跨副本一致性探针。
成本评估还包括 KMS 验签/签名调用、证据存储、跨区传输、日志保留、Inspection 沙箱和密钥轮换演练。低频供应链可离线批量验证;高频发布可缓存摘要计算,但策略撤销时必须能按 Layout/key/policy 版本主动清缓存。
清理、回滚、升级迁移与安全退出
实验清理先撤销测试发布权限,归档需要保留的 Layout、Links、公钥和验证日志,再删除工作区、测试私钥和临时制品。不得先删唯一证据再声称完成验证。若测试 key 曾进入共享信任集,应显式移除并用该 key 的旧 Link 做负向验证,证明它已经失效。
升级参考实现时,把规范 TypeURI、Python 包、CLI 行为、key backend 和 DSSE 支持当作不同版本面。先在隔离环境用历史正例、断链、错误身份、缺 Link、过期 Layout 和重放样本回归;比较退出码、实际验签身份和规则结果,再切换生产验证器。回滚二进制前确认旧版本能读取新生成的 metadata 与 envelope,不能假设包版本回退等于数据格式回退。
迁移到另一验证平台时,以 digest 导出 Layout、Links、Statements、DSSE Envelopes、公钥/证书、信任根、策略和决策日志。目标平台先只读双验,确认摘要连续性、threshold 去重、Predicate 约束和重放策略等价;随后停止旧端签发,保留历史只读验证,最终移除旧 owner key 与 functionary 授权。退出完成的证据不是“卸载了 in-toto”,而是历史证据仍可按保留策略验证,旧身份无法再为新发布取得授权,Inspection 执行凭据和临时密钥均已回收。
机制选型从要证明的问题开始
只需要证明“谁对某个 digest 作出了某类声明”时,Statement + Predicate + DSSE 更直接;需要约束多组织供应链的步骤、授权人员、材料/产物连续性和双人执行时,Layout/Link 才提供相应模型。两者可以组合:Link 或供应链结果可被包装为 Attestation,但不能因此省略各自策略。
DSSE 适合避免序列化规范化和类型混淆,不负责 PKI、撤销、透明日志、发现或保留;这些能力需要 Sigstore、TUF、Registry 或独立信任服务。Inspection 适合必须在消费端重做的检查,却会把不稳定依赖和代码执行带入验证路径。能够用声明式 artifact rules 表达的约束,优先不运行命令;必须运行的检查则放进可重建沙箱并准备超时、降级与证据保留策略。
in-toto 规范与稳定状态入口。in-toto 核心 Layout/Link 规范。in-toto Attestation Framework
