签名没问题,但那把钥匙我们已经吊销了
一句话总结
签名所保证的只有“这些字节是用那把密钥签名的”,而另外一半——现在是否 仍然信任那把密钥——必须在签名算法之外另行管理。
为什么需要它
“制品已经签名”这句话,说明的东西比想象中要少得多。 因为保证并不产生在加上签名的地方,而是产生在验证失败的地方。 所以引入签名体系时,首先要做的是把所有应当失败的情况全部写下来,然后逐个 真正让它失败。
把清单写出来,通常会得到四行。制品被改动的情况,签名不是我们的密钥的情况, 是我们的密钥但已被撤销或已过期的情况,以及完全不认识的密钥的情况。 前两种,签名算法自己就会拦住。后两种它拦不住。 因为原始密钥对没有过期时间,而“这把密钥是不是我们的”这个问题, 密钥里并没有可以用来回答的资料。
工作原理
签名针对的不是整个文件,而是摘要。先求出哈希,再用私钥对该值 签名,所以制品哪怕只有一个字节不同,哈希也会整个改变,无法再用同一个签名 匹配。因此篡改检测几乎是免费附带的。
问题在于密钥。如果密钥由我们自己持有,那么是谁把密钥放在了哪里、泄露后如何 察觉、更换时需要重新签署什么,就全都成了我们的难题。这正是 sigstore 用另一种方式 解决这道难题的原因。在默认流程基于身份的签名中, 会在内存里生成临时密钥对,证书颁发机构(CA)确认 OpenID Connect 身份令牌之后, 签发一张把该身份绑定到公钥上的短期有效的证书。签名完成后,私钥随即 销毁,证书也会过期。验证方不看已销毁的密钥,而是依据透明日志中留下的 带时间戳的条目来判断 (sigstore 签名概览)。
如果想自己管理密钥,cosign 也允许通过 KMS 来做到。用 awskms://、
gcpkms://、k8s:// 这样的 URI 指向 AWS KMS、GCP KMS、Azure Key Vault、
HashiCorp Vault、Kubernetes Secret 等提供方,再用 cosign public-key --key <URI> 取出公钥
用于验证
(cosign 密钥管理)。
在验证一侧,文档写明 cosign verify 不仅验证签名,还会确认签名证书
是否来自受信任的证书颁发机构
(cosign 验证)。
无论走哪条路,留下来的原则都是一样的。“这个签名对不对”和“现在是否信任这把密钥” 是两个不同的问题,而且两者都必须问。 如果自己管理,就需要一份写明 信任哪把密钥、从什么时候到什么时候的密钥环(keyring);如果使用 sigstore, 则由证书的有效期和透明日志来取代它的位置。
在现场相遇的样子
最昂贵的失败是做了验证,却把失败吞掉了。表现为流水线里不查看验证 命令的退出码,或者只把错误写进日志,然后继续进入下一个步骤。 这种状态比没有签名更糟——因为所有人都以为自己受到了保护。
第二种在更换密钥时暴露出来。迁移到新密钥时,如果不把旧密钥从密钥环中去掉, 用已撤销的密钥签名的制品就会继续通过。反过来,如果直接把旧密钥删掉, 还没有发布出去的旧版本就会全部被拦住。所以,与其删除,不如写下期限, 这种做法才能在生产环境中存活。
第三种是签名对象模糊不清。如果签名文件里没有写明签的是什么, 验证方就只能靠文件名去猜“这个签名属于那个文件”。在有多个发布版本的目录中, 这种猜测很快就会出错。所以在实践中,与其把签名直接加在制品上, 不如对写明对象及其摘要的文档签名,并把这份文档一起 发布出去。下一个模块的来源证明,正是这样的文档。
第四种是让私钥在构建阶段可以被读取。如果构建脚本能够接触到密钥, 那么在该构建中运行的任何代码都能接触到密钥,一旦某个依赖被污染,签名 本身就失去了意义。把密钥放在 KMS 之后,或者干脆使用临时密钥的设计, 正是直接针对这个问题的。
第五种是生成签名之后一次也不做验证。签名命令通常会 悄悄成功,所以即使两次指定了不同的哈希算法,或者用错了密钥签名, 当场也不会发生任何事。问题要到几周之后的发布流水线里才会暴露,而且偏偏 是在紧急的日子里。只要在签名之后立刻在原地加一行验证,这种事故 就会被彻底消除。
下一项实验要做什么
生成密钥对并对制品签名,然后亲手制造验证应当失败的四种情况。 被篡改的制品、用已撤销的旧密钥生成的完好签名、密钥环中没有的密钥,以及正常制品。最后 制作一个读取密钥环并同时检查期限的验证器,一次区分这四种情况。