造出四种必须验证失败的情形
目标
亲手为发布制品签名,并手工制造验证应当失败的情况。 制作一个能拒绝篡改、其他密钥、已撤销的密钥、密钥环中没有的密钥这四种情况的验证器。
为什么重要
“加上了签名”这句话什么也不保证。保证产生于验证失败的地方。 所以引入签名体系时,首先要做的不是签名,而是把应当失败的情况 全部写下来,并逐个真正让它失败。尤其是最后两种经常被漏掉—— 签名本身在数学上完好无损,但我们已经不再信任那把密钥。原始密钥没有 过期时间,所以信任到什么时候,必须在外部另行记录。这就是 sigstore 使用短期有效的 证书和透明日志的原因——为的是让验证方不必依赖签名者的密钥管理 习惯。
步骤
- 把制品放在
/root/sign/dist中,并把指纹写入/root/sign/01-digest.txt。 - 在
/root/sign/release.key和/root/sign/release.pub中生成 P-256 密钥对。 - 对制品签名,生成
/root/sign/dist/paygate-1.4.2.js.sig。 - 把验证输出留在
/root/sign/04-verify.txt中。 - 制作被篡改的副本,把失败输出留在
/root/sign/05-tamper.txt中。 - 用已撤销的旧密钥签名,把失败输出留在
/root/sign/06-oldkey.txt中。 - 把两把密钥连同期限和指纹登记到
/root/sign/keyring.json中。 - 创建
/root/sign/verify-release.sh,并把四种情况的退出码写入/root/sign/08-checks.json。
参考
- 这个镜像里没有 cosign。无密钥签名以 Fulcio 和 Rekor 这两项外部服务为 前提,而 Pod 只开放了 DNS。所以用 openssl 搭出同样的骨架。
- 签名文件是二进制的。不要用
cat查看,请用-out写入文件。 - 验证失败时,openssl 也会通过标准错误输出。记录时请用
2>&1一并接收。 - 基准日固定为
2026-09-11。如果读取date来使用,明天就会得到不同的答案。 - 常见错误——签名时和验证时指定了不同的哈希,以及制作了篡改副本之后 又覆盖到原件上,导致两个文件变得相同。
- 实验 Pod 没有卷。这里生成的密钥在会话结束后会消失。
把要签名的东西放在一处,并计算指纹
把 /opt/fixtures/sbom/release/paygate-1.4.2.js 复制到 /root/sign/dist/paygate-1.4.2.js,并把该文件的 SHA-256 以小写十六进制 64 个字符的一行保存到 /root/sign/01-digest.txt。
签名针对的不是整个文件,而是摘要。先把这个值握在手里再开始,之后就能用眼睛确认哪里发生了变化。
生成发布密钥对
使用 prime256v1(P-256)曲线生成密钥对,把私钥保存到 /root/sign/release.key,公钥保存到 /root/sign/release.pub。不要使用设置了口令的私钥。
用 openssl ecparam 指定曲线来生成私钥,再给 openssl ec 加上 -pubout,就能从同一把密钥得到公钥。
对摘要签名
用 /root/sign/release.key 对 /root/sign/dist/paygate-1.4.2.js 签名,并保存到 /root/sign/dist/paygate-1.4.2.js.sig。哈希是 SHA-256。
openssl dgst 加上 -sign 就是签名,加上 -verify 就是验证。输出是二进制的,所以请用 -out 写入文件。
记录验证通过的情况
用 /root/sign/release.pub 验证 /root/sign/dist/paygate-1.4.2.js 和 /root/sign/dist/paygate-1.4.2.js.sig,把 openssl 给出的输出保存到 /root/sign/04-verify.txt。
通过时 openssl 只会输出一行。这一行就是这个步骤的证据。
看看把制品改一个字符会怎样
把 /root/sign/dist/paygate-1.4.2.js 复制到 /root/sign/tampered/paygate-1.4.2.js 后修改其内容,用原来的签名 /root/sign/dist/paygate-1.4.2.js.sig 验证,并把失败的输出保存到 /root/sign/05-tamper.txt。
签名是加在摘要上的。哪怕只有一个字节不同,摘要也会整个改变,所以无法再用同一个签名匹配。
制作一个由去年用过又撤销的密钥签名的东西
再生成一对 P-256 密钥,保存到 /root/sign/old-release.key 和 /root/sign/old-release.pub,并用这把密钥对 /root/sign/dist/paygate-1.4.2.js 签名,生成 /root/sign/dist/paygate-1.4.2.js.oldsig。然后用 /root/sign/release.pub 验证这个签名,把失败的输出保存到 /root/sign/06-oldkey.txt。
这个签名不是伪造的——它在数学上是完好的签名,用那把密钥的公钥可以通过验证。问题在于“现在是否仍然信任那把密钥”,而这是签名算法回答不了的。
写下信任哪把密钥、信任到什么时候
在 /root/sign/keyring.json 中以 keys 数组登记两把密钥。每个条目包含 id、file、fingerprint、not_before、not_after、owner。release.pub 的 id 为 release-2026,期限从 2026-01-01 到 2026-12-31;old-release.pub 的 id 为 release-2025,期限从 2025-01-01 到 2025-12-31,owner 都是 payments-platform。file 只写 /root/sign 中的文件名,fingerprint 是把该公钥导出为 DER 后求得的 SHA-256 小写十六进制。
原始密钥没有过期时间。所以“信任到什么时候”必须在外部另行记录,这也是 sigstore 使用短期有效的证书的原因。
制作会查看密钥环的验证器,并判定四种情况
创建 /root/sign/verify-release.sh。以 bash /root/sign/verify-release.sh <산출물> <서명> <키id>(占位符依次为制品、签名、密钥 id)调用时,它会从 /root/sign/keyring.json 中查找该密钥,查看在基准日 2026-09-11 期限是否仍然有效,如果有效,则用该公钥验证签名。通过则以 0 结束;密钥环中没有、已过期或验证失败则以 1 结束。然后运行这四种情况,把退出码以 good、tampered、old_key、unknown_key 保存到 /root/sign/08-checks.json。good 是正常制品和签名加上 release-2026,tampered 是篡改副本加上相同的签名和 release-2026,old_key 是正常制品加上 .oldsig 和 release-2025,unknown_key 是密钥环中没有的 release-2024。
验证器必须回答两个问题——签名是否与这些字节相符,以及现在是否可以信任那把密钥。如果只看其中一个,用已撤销的密钥签名的东西就会原样通过。