Sigstore Cosign 制品签名、证明与验证手册
发布流水线显示镜像构建、扫描和签名全部成功,生产准入也找到了 Cosign 签名。事故发生后,团队才发现部署使用的是可变 Tag,验证命令又把证书身份正则写成了 .*:攻击者用另一个合法 OIDC 身份签了被替换的 digest,形式上“有签名”,却不是获准发布该仓库的构建身份。
另一条离线发布链也曾在演练中通过,真正断开出口后却卡在 TUF 更新。操作人员以为 --offline 会阻止网络访问,实际只带走了镜像和签名,没有带走覆盖签名时段的可信根与历史日志公钥。两个现场指向同一个结论:签名算法通过只是开始,制品摘要、签名者身份、签发者、透明日志、可信时间和消费策略必须同时闭环。
先把“谁签了什么”建成对象模型
Cosign 的验证对象不是 Tag、文件名或流水线运行号,而是内容。OCI 场景的 subject 是 manifest digest,blob 场景的 subject 是本地字节计算出的摘要。签名把该摘要绑定到密钥或短期证书;标准 bundle 再封装签名内容、证书或公钥、透明日志条目与包含证明、时间戳等验证材料。消费端最后用策略回答:哪个 issuer 签发的哪个 identity,可以为哪个仓库或制品类型作出哪类声明。
Keyless 流程会在签名端临时生成密钥,OIDC token 证明工作负载或人员身份,Fulcio 给临时公钥签发短期证书,CT log 记录证书,Rekor 记录签名事件,TSA 可提供 RFC 3161 时间证据。它减少了长期私钥分发,却没有消除密钥与信任风险:OIDC、CA、日志、TSA、TUF 和客户端实现中的任一环节失守,都需要消费策略与事件响应接住。
KMS 流程则以长期受控密钥版本作为 signer。验证者通过公钥或 KMS URI 校验签名,不天然得到 OIDC 身份语义;团队要用 KMS IAM、密钥用途、版本和发布审批定义“谁能签”。两种机制可以并存,但不能把 keyless identity 和 KMS key ID 混成一条模糊白名单。
安装 Cosign 3.1.1 并固定发行物
本文命令以 Cosign v3.1.1 为行为基线。该版本的发布说明明确标准 Sigstore bundle 已成为默认输入输出,并把一批旧验证材料、bundle、服务配置和 OCI 参数标为弃用;这些参数在 3.x 还能运行,但计划在未来 v4 移除。自动化应固定版本与校验值,不能拉取裸 latest。
开发机可从官方安装入口选择包管理器或 release 二进制。CI 更适合下载指定平台资产,按发布页给出的校验材料验证后放入只读工具缓存:
export COSIGN_VERSION=v3.1.1
export COSIGN_BIN=./tools/cosign
"$COSIGN_BIN" version
test "$("$COSIGN_BIN" version --json | jq -r .gitVersion)" = "$COSIGN_VERSION"
"$COSIGN_BIN" initializeinitialize 用内嵌初始 TUF root 更新本地 Sigstore 信任缓存,默认状态位于用户目录下的 .sigstore/root/。自定义镜像可使用 --root、--root-checksum 和 --mirror,但初始根若经网络取得必须校验 checksum;更强的做法是由安全团队通过带外介质交付初始根。CI 缓存可以缓存公开元数据,却不应把不同租户的 registry 凭证、OIDC token 或 KMS 会话一并缓存。
安装验收至少记录二进制摘要、cosign version、目标平台和 TUF 初始化结果。旧版 Cosign 曾存在自定义 trusted root 验证问题,安全公告给出的修复线是 3.0.4;使用 3.1.1 可越过该修复点,但升级仍需核对对应安全公告和组织自己的依赖扫描结果。
配置字段怎样改变签名与验证
SigningConfig 描述签名端去哪里获取 Fulcio 证书、写哪个 Rekor、联系哪个 TSA,以及服务 API 版本、operator 和有效窗口;TrustedRoot 描述验证端信任哪些 Fulcio/CT 证书链、Rekor 公钥、TSA 证书和有效窗口。前者是服务发现,后者是信任材料,任何把 URL 写进 trusted root 或把公钥塞进 signing config 的做法都会破坏轮换职责。
私有实例可用目标版本提供的命令生成配置骨架,再由平台仓库评审和分发:
cosign signing-config create \
--no-default-fulcio --no-default-rekor \
--no-default-oidc --no-default-tsa \
--fulcio="url=https://fulcio.example.internal,api-version=1,start-time=<START_TIME>,operator=example.internal" \
--rekor="url=https://rekor.example.internal,api-version=2,start-time=<START_TIME>,operator=example.internal" \
--rekor-config="EXACT:1" \
--oidc-provider="url=https://oidc.example.internal,api-version=1,start-time=<START_TIME>,operator=example.internal" \
--tsa="url=https://tsa.example.internal,api-version=1,start-time=<START_TIME>,operator=example.internal" \
--tsa-config="EXACT:1" \
--out signing-config.json
cosign trusted-root create \
--no-default-fulcio --no-default-rekor \
--no-default-ctfe --no-default-tsa \
--fulcio="url=https://fulcio.example.internal,certificate-chain=fulcio-chain.pem,start-time=<START_TIME>" \
--rekor="url=https://rekor.example.internal,public-key=rekor.pub,start-time=<START_TIME>" \
--ctfe="url=https://ct.example.internal,public-key=ctfe.pub,start-time=<START_TIME>" \
--tsa="url=https://tsa.example.internal,certificate-chain=tsa-chain.pem,start-time=<START_TIME>" \
--out trusted-root.json
cosign sign --signing-config signing-config.json \
registry.example.com/team/app@sha256:<DIGEST>
cosign verify --trusted-root trusted-root.json \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER" \
registry.example.com/team/app@sha256:<DIGEST>实际字段以 v3.1.1 的 cosign signing-config create --help 和 protobuf schema 为准。URL 改动会切换写入服务,majorApiVersion 决定 Rekor v1/v2 协议,validFor 决定材料在何时可被选择,operator 用于区分运营域。Trusted root 中漏掉历史日志公钥会让旧 bundle 失验;窗口过宽会延长被泄露材料的接受期;窗口重叠不足则在轮换边界制造发布中断。
Keyless 验证必须同时给出精确 identity 与 issuer,v3.1.1 的verify CLI 文档也把两者列为 keyless 流程的约束。正则只在身份确有结构化变化时使用,并应锚定整串、转义仓库名,禁止以 .* 或只匹配组织前缀代替授权策略。
Blob 的 bundle 正反实验
先使用测试 OIDC 身份签一个普通文件。签名阶段可能打开浏览器或从 runner 的工作负载身份取 token;不要在个人终端对敏感制品使用公共日志。标准 bundle 是必须归档的交付物,不要再拆成证书、SCT、签名三个零散文件。
set -euo pipefail
printf 'family-44\n' > artifact.bin
cosign sign-blob artifact.bin \
--bundle artifact.bin.sigstore.json \
--yes
cosign verify-blob artifact.bin \
--bundle artifact.bin.sigstore.json \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER"正向结果应为退出码 0。证据不是屏幕上的 Verified OK 一句话,还要保存 bundle 摘要、制品摘要、命中的证书 identity/issuer、透明日志和时间验证结果。官方blob 验证命令给出了 bundle 与身份参数的目标版本接口。
随后复制原始制品与 bundle,逐步制造内容和身份错误:
cp artifact.bin artifact.tampered.bin
printf 'tampered\n' >> artifact.tampered.bin
if cosign verify-blob artifact.tampered.bin \
--bundle artifact.bin.sigstore.json \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER"; then
echo 'ERROR: tampered artifact was accepted' >&2
exit 1
fi
if cosign verify-blob artifact.bin \
--bundle artifact.bin.sigstore.json \
--certificate-identity 'urn:example:identity:that-does-not-exist' \
--certificate-oidc-issuer "$EXPECTED_ISSUER"; then
echo 'ERROR: wrong identity was accepted' >&2
exit 1
fi第一条应在内容摘要或签名校验处非零退出,第二条应在证书身份策略处失败。把制品 A 的 bundle 配给制品 B 也必须在内容绑定处失败。若失败原因只是网络超时,实验没有证明摘要绑定生效,应先恢复网络或切换到完整离线材料再重跑。
OCI 必须沿 digest 签名和部署
Registry 中的 Tag 只负责发现候选对象。推送完成后先解析服务端 digest,再对 repo@sha256:... 签名;部署清单和晋级记录也传递同一个 digest。Cosign 3 默认把 OCI 签名写成 OCI 1.1 referring artifact,Registry 的 referrer、复制和 GC 能力会直接影响证据可用性。
export IMAGE=registry.example.com/team/app
export TAG=release-candidate
docker push "$IMAGE:$TAG"
DIGEST=$(docker buildx imagetools inspect "$IMAGE:$TAG" \
--format '{{json .Manifest.Digest}}' | tr -d '"')
SUBJECT="$IMAGE@$DIGEST"
cosign sign --yes "$SUBJECT"
cosign verify "$SUBJECT" \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER" \
--output json > cosign.verify.json
jq -e --arg d "$DIGEST" \
'any(.[]; .critical.image["docker-manifest-digest"] == $d)' \
cosign.verify.json正向证据包含 Registry 返回的 subject digest、签名 referrer、验证输出和命中策略。验证输出格式属于接口,升级时要用样本测试保护 jq 路径,不要让字段变化变成静默放行。
# 推送不同内容并让同一 Tag 指向新 digest
docker tag local/app:changed "$IMAGE:$TAG"
docker push "$IMAGE:$TAG"
NEW_DIGEST=$(docker buildx imagetools inspect "$IMAGE:$TAG" \
--format '{{json .Manifest.Digest}}' | tr -d '"')
test "$NEW_DIGEST" != "$DIGEST"
if cosign verify "$IMAGE@$NEW_DIGEST" \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER"; then
echo 'ERROR: unsigned replacement digest was accepted' >&2
exit 1
fi新 digest 应非零退出,旧 digest 仍可验证。这个反例证明“Tag 曾经签过”没有任何继承关系。若部署系统只接受 Tag,应在晋级环节解析并冻结 digest,准入侧比较实际拉取 digest,而不是重复解析可能已改变的 Tag。
KMS 与 Keyless 怎样选
自动化构建具备稳定 OIDC issuer、不可变 workflow identity 和细粒度 audience 时,keyless 能减少私钥落盘、轮换和 runner 分发负担,适合高频 CI 签名。消费策略要绑定完整 workflow URI、仓库、ref 或受保护环境,账户和组织重命名都应被视为身份迁移。
受监管发布、离线签名、跨平台兼容或现有 PKI/KMS 审批成熟时,KMS 更容易建立双人审批与密钥版本审计。签名命令只拿 kms:// URI,runner 获得限定 Sign 权限,不需要导出私钥:
export KMS_KEY='awskms:///arn:aws:kms:<REGION>:<ACCOUNT>:key/<KEY_ID>'
cosign sign --key "$KMS_KEY" --yes "$SUBJECT"
cosign verify --key "$KMS_KEY" "$SUBJECT"不同 provider 的 URI 和 IAM 动作以目标版本帮助与云官方文档为准。KMS 验证若通过,只说明该密钥产生了签名;仓库授权仍需把 key ID/version、subject 仓库、digest、环境和审批记录关联起来。关键发布可采用不同信任域的多签或阈值策略,但不要用同一个 KMS key 冒充独立审批人。
本地加密私钥适合开发实验和应急兼容,生产中会引入 COSIGN_PASSWORD、私钥备份、分发与吊销成本。硬件安全密钥适合低频人工发布,却不适合无人值守横向扩容。选择标准不是哪种命令最短,而是谁持有授权、私钥能否导出、验证者如何轮换和事故时能否界定 compromise time。
Attestation 不是签名注释的放大版
Attestation 把 subject digest 与结构化 predicate 绑定,常见载荷包括 provenance、SBOM 或测试声明。验证签名后仍要验证 predicateType、subject 数量与 digest、builder identity、材料、构建参数和策略字段;“存在一个 attestation”不代表声明内容满足发布要求。
cosign attest --yes \
--predicate provenance.json \
--type slsaprovenance \
"$SUBJECT"
cosign verify-attestation "$SUBJECT" \
--type slsaprovenance \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER" \
> attestations.json
jq -e --arg d "${DIGEST#sha256:}" '
any(.[];
(.payload | @base64d | fromjson) as $s |
any($s.subject[]; .digest.sha256 == $d) and
($s.predicateType | contains("slsa"))
)' attestations.json反向实验可以把 provenance 的 subject 改成另一个 digest 后重新签名。Cosign 会正确证明“某身份签了这份错误声明”,而消费策略应因 subject 或 builder 不匹配而拒绝。这正是密码学验证与业务授权的分界。
Rekor v2 对大型 DSSE attestation 可以记录其 PAE 哈希,v3.1.1 发布说明明确了这一路径。它避免把大型 SBOM 全文塞入日志,不表示 Rekor 替你保存完整 predicate;原始 attestation 仍需在 Registry、bundle 或证据仓中按保留策略归档。
流水线接入要保存策略证据
流水线可以拆成 build、resolve-digest、attest、sign、verify、promote 六步。签名 job 仅接收不可变 digest,并使用受保护环境的 OIDC 或 KMS 权限;验证 job 从目标 Registry 重新读取 subject 与 referrer,不能复用签名进程内的成功对象。晋级输出至少记录 subject digest、bundle/referrer digest、identity、issuer、策略版本、验证工具摘要和结果。
OIDC token 应由 runner 原生 provider 按需签发,audience 限定为 Sigstore 所需值,生命周期只覆盖签名步骤。禁止把 token 作为命令参数回显,禁止 set -x,禁止写入缓存、artifact 和失败日志。Registry 凭证只给目标仓库 push/referrer 权限;验证 job 通常只需 pull。KMS signer 只给指定 key version 的 Sign,不给 Decrypt、CreateKey 或策略管理。
验证失败默认阻断。网络错误、TUF 过期、Rekor 不可达、证据缺失与身份不匹配要分开计数,不能统一重试后放行。紧急例外应生成有时限、有审批、有精确 digest 的策略对象,并在事后补签或重建;禁止用 --insecure-ignore-tlog、--insecure-ignore-sct、--allow-insecure-registry 修复生产故障。
真正离线验证靠材料封装和网络隔离
--offline 在 Cosign 3 新路径中已弃用,它不是可靠的无网络保证。真正的隔离流程要在联网区预取完整对象与标准 bundle,取得带外校验的 TrustedRoot,冻结 identity/issuer 策略,再将清单和材料转移到阻断 DNS 与 egress 的容器或 VM。TUF 元数据需要受控刷新,不能在过期时自动降级为跳过透明日志、SCT 或 TSA。
联网准备区执行:
set -euo pipefail
cosign initialize
cosign save "$SUBJECT" --dir ./transfer/oci-layout
install -m 0444 "$HOME/.sigstore/root/"*/targets/trusted_root.json \
./transfer/trusted_root.json
printf '%s\n' "$EXPECTED_IDENTITY" > ./transfer/identity.txt
printf '%s\n' "$EXPECTED_ISSUER" > ./transfer/issuer.txt
(cd transfer && sha256sum trusted_root.json identity.txt issuer.txt > SHA256SUMS)转移清单还应包含本地 OCI 布局的完整文件哈希、来源审批、可信根版本与到期信息。带外通道校验 SHA256SUMS 后,在禁网验证区执行:
set -euo pipefail
cd transfer
sha256sum -c SHA256SUMS
cosign verify \
--local-image \
--trusted-root ./trusted_root.json \
--certificate-identity "$(cat identity.txt)" \
--certificate-oidc-issuer "$(cat issuer.txt)" \
./oci-layout > verify.offline.json
test -s verify.offline.json正向验收必须同时满足退出码 0、抓包或防火墙日志无外呼、输出命中正确 digest 与身份。命令参数与本地布局的具体组合要用 3.1.1 在目标 Registry 格式上演练,因为 cosign save/load 与 OCI 1.1 referrer 兼容性会受 Registry 和历史签名格式影响。
mv trusted_root.json trusted_root.missing.json
if cosign verify \
--local-image \
--trusted-root ./trusted_root.json \
--certificate-identity "$(cat identity.txt)" \
--certificate-oidc-issuer "$(cat issuer.txt)" \
./oci-layout; then
echo 'ERROR: verification succeeded without the trusted root' >&2
exit 1
fi
mv trusted_root.missing.json trusted_root.json反向还应分别删除 bundle/referrer、换入不含历史 Rekor key 的 root、改错 issuer,并让防火墙拒绝所有外呼。每种情况都应稳定非零退出且错误类型可区分。可信根过期时应提示更新包,而不是从网络临时下载或转入不校验日志的模式。
排障从失败层级而不是重试开始
先判断 subject 层:打印部署实际 digest,与签名输出和 attestation subject 比较。Tag 漂移、平台 manifest 与架构 manifest 混淆、Registry 代理重写,是“找不到签名”最常见的上游原因。再用 cosign tree 或 oras discover 查看 referrer;cosign triangulate 已弃用,不应进入新脚本。
第二层看身份与证书。保存验证 JSON,检查 identity、issuer、证书有效期、SCT 与签名时间。certificate identity mismatch 不是网络故障,通常是 workflow URI、ref、环境或 issuer 变化。不要为排障把正则放宽成 .*;应同时采集当前证书与预期策略,做精确差异。
第三层看 trust 与 transparency。TUF 到期、trusted root 缺失历史材料、Rekor v1/v2 API 不匹配、TSA 证书链缺失都会表现为密码学材料不完整。记录 SigningConfig/TrustedRoot 摘要、有效窗口和 operator,再判断是客户端过旧、配置未发布还是服务端轮换错误。只有 Registry TLS 问题才处理 CA、mTLS 和代理,禁止用 insecure flags 绕过。
最后看容量与并发。cosign verify 有 --max-workers,提高它会同时增加 Registry、TUF 和验证 CPU 压力;批量验证应以延迟分位、429/5xx、缓存命中和 CPU 基线调参。重试只覆盖幂等网络错误并加抖动,身份失败、摘要不匹配和证据缺失不得重试成成功。
清理、回滚与证据保留
测试 blob 可删除本地文件与 bundle,测试 OCI 对象则先按 Registry 能力删除 referrer,再删除 subject。两者都不会抹除公共 Rekor/CT 中已经公开的身份与事件;公开日志是 append-only 证据系统,不是可按工单删行的业务数据库。使用公共实例前必须审查邮箱、workflow URI、仓库、ref、发布频率和 artifact hash 的可见性。
流水线回滚应回滚策略和工具版本,不回滚不可变制品本身。保留旧验证工具样本、历史 bundle、TrustedRoot、策略版本和镜像 digest,先证明旧制品仍可验证,再切回上一条发布链。新旧 bundle 迁移可评估 cosign bundle upgrade,但必须逐条比较 subject、identity、issuer、日志和时间证据,不能转换后立即删除原件。
退出 Cosign 时,先停止产生新格式,保持验证端双读;再复制 OCI referrer、bundle、attestation 与信任材料到替代系统,做正反样本互验;最后移除签名权限和 OIDC audience。Registry GC 前建立“subject 仍被引用时 referrer 不得清理”的规则,离线归档需能在无网络环境重放。
容量、HA、升级与迁移决策
Cosign CLI 本身无服务端 HA,瓶颈落在 Registry、OIDC、Fulcio、Rekor、TSA、TUF、KMS 与 CI runner。容量估算应从每次发布的签名数、attestation 数、subject/referrer 大小、验证次数和保留期出发。大型 SBOM 不应复制进透明日志;Registry 需计算 referrer 存储、跨区域复制和 GC 扫描成本,KMS 需计算签名请求费用与限流,验证端需测量证书链、日志证明和策略解析的 CPU。
升级到 3.1.1 时先在影子流水线验证历史 keyless、KMS、blob、OCI、attestation 和离线样本。标准 bundle 已是默认路径,--new-bundle-format、独立证书/签名/SCT 文件参数、服务 URL 参数、--offline、--tlog-upload、--private-infrastructure 以及一批旧 OCI 命令都不应再进入新脚本。attach、copy、dockerfile、generate、manifest、upload 等被标记未来移除的子命令,要分别迁移到标准 bundle、ORAS/Crane 或专用生成器。
发布策略至少维护三组回归制品:当前格式成功样本、身份/摘要错误样本、历史格式和历史信任材料样本。连续多轮升级后,成功样本仍命中相同 digest/identity/issuer,不合格样本持续 fail closed,Registry referrer 数量与存储增长回到容量预算,才算迁移稳定。
机制选型的最终判断很朴素:能稳定提供 workload identity 且接受公共可观察性的团队可优先 keyless;已有强 KMS 治理或隔离签名需求的团队可优先密钥签名;两者都必须产出可归档 bundle、固定 digest、明确消费策略,并保留退出时可独立验证的材料。
