3. 密码学基础(下):数字签名(RSA/ECDSA)、公钥基础设施(PKI)、证书链
好,咱们接着聊。上一节我们把哈希和对称/非对称加密理清了,这一节要啃的,是安全启动真正的“脊梁骨”——数字签名、PKI 和证书链。
你想想看,安全启动最核心的问题是什么?其实就是一句话:我怎么知道这段代码是“谁”写的,而且没被人动过手脚?哈希只能保证完整性,加密只能保证机密性。但“谁写的”这个问题,得靠数字签名来回答。
3.1 数字签名:不只是“加密的哈希”
很多人刚接触数字签名时,觉得它就是“把哈希值用私钥加密一下”。这个说法在 RSA 场景下勉强能说得通,但到了 ECDSA 就不对了。我建议你从“签名是独立算法”这个角度去理解,而不是把它当成加密的变种。
3.1.1 RSA 签名:先哈希,再私钥操作
RSA 签名的流程,说白了就是两步:
- 对消息做哈希,得到一个固定长度的摘要(比如 SHA-256 的 32 字节)。
- 用私钥对这个摘要做“签名运算”(其实就是 RSA 的私钥指数运算),输出签名值。
验签的时候,接收方用公钥解签名,拿到摘要,再跟自己算的消息哈希比对。一致,就说明消息确实是私钥持有者签的,而且没被篡改。
关键点:RSA 签名 ≠ RSA 加密。虽然数学运算形式上有点像,但填充方案完全不同。签名用的是 PSS(Probabilistic Signature Scheme),加密用的是 OAEP。千万别混用,我在项目里见过有人把加密函数直接拿来签名,结果验签死活过不了——填充格式不对。
3.1.2 ECDSA:椭圆曲线上的签名
ECDSA 跟 RSA 的思路完全不一样。它不直接对哈希做“逆运算”,而是基于椭圆曲线上的点乘运算,生成一对值 (r, s)。
我个人习惯把 ECDSA 理解成“零知识证明的雏形”:签名者证明自己知道私钥,但又不暴露私钥本身。验签者通过公钥和签名值,能验证这个证明是否成立。
经验之谈:ECDSA 的签名过程中,随机数 k 极其重要。如果 k 值重复或可预测,私钥就能被算出来。索尼 PS3 当年就栽在这个坑上——他们用了固定的 k 值,结果黑客直接逆向出了私钥。嗯,这就是所谓的“索尼大法好,签名随便搞”。
3.1.3 RSA vs ECDSA:怎么选?
| 对比维度 | RSA | ECDSA |
|---|---|---|
| 密钥长度 | 2048 / 4096 位 | 256 / 384 位(同等安全强度) |
| 签名速度 | 慢(私钥操作) | 快 |
| 验签速度 | 快(公钥操作) | 较慢 |
| 签名体积 | 与密钥等长(如 256 字节) | 固定 64 字节(P-256) |
| 硬件支持 | 广泛,几乎所有安全芯片都支持 | 较新,但已成为主流 |
在安全启动场景下,我个人的建议是:如果芯片硬件支持 ECDSA 加速,优先用 ECDSA P-256。签名体积小,适合固件镜像的存储和传输。但如果你的安全芯片比较老,只支持 RSA,那 2048 位 RSA 也完全够用。
3.2 公钥基础设施(PKI):信任从哪来?
好,现在你会签名了。但问题来了:我怎么相信你给我的公钥就是你的?
这就是 PKI 要解决的问题。PKI 不是某个算法,而是一整套管理公钥的体系。它包括:
- 证书颁发机构(CA):负责签发证书的“权威机构”。
- 证书:把公钥和身份信息绑定在一起的数字文件。
- 证书撤销列表(CRL):记录哪些证书已经失效。
- 在线证书状态协议(OCSP):实时查询证书状态。
在嵌入式安全启动里,我们通常不会搞那么复杂。但核心思想是一样的:信任锚(Root of Trust)。
注意:PKI 的安全性完全取决于根 CA 私钥的保护。一旦根私钥泄露,整个信任链就崩塌了。我在一个客户项目里见过他们把根私钥放在一个没有密码保护的 U 盘里……我当时差点没背过气去。
3.3 证书链:从根到叶子
证书链,说白了就是“信任的传递”。
你想想看:如果每个设备都只信任一个根证书,那根证书的私钥就得用来签所有固件。这太危险了——万一私钥泄露,整个产品线都得完蛋。
所以,我们搞了个分层结构:
- 根 CA:自签名证书,私钥锁在 HSM 或安全芯片里,几乎不用。
- 中间 CA:由根 CA 签发,用于签发具体的固件或设备证书。
- 最终实体证书:由中间 CA 签发,直接用于签名固件。
验签的时候,设备从固件签名里拿到最终实体证书,然后一路向上验证,直到找到设备里预置的根证书。只要链条上的每一环都验证通过,信任就建立起来了。
核心原则:设备里只存根证书。中间 CA 和最终实体证书都随固件一起下发。这样即使某个中间 CA 的私钥泄露,你只需要吊销那张证书,重新签发一张就行,不用动设备里的根证书。
3.4 证书格式:X.509 长什么样?
在嵌入式世界里,我们最常用的证书格式是 X.509 v3。一个典型的证书包含:
- 版本号:通常是 v3。
- 序列号:CA 分配的唯一编号。
- 签名算法:比如 sha256WithRSAEncryption。
- 颁发者:签发这个证书的 CA 名称。
- 有效期:起止时间。
- 主体:证书持有者的名称。
- 公钥:持有者的公钥。
- 扩展:比如密钥用途(digitalSignature)、基本约束(CA:TRUE/FALSE)。
在安全启动里,我们最关心的是 密钥用途扩展。根证书的密钥用途通常只允许“签发证书”,而固件签名证书的密钥用途是“数字签名”。这样即使固件签名证书被滥用,也无法签发新的证书。
避坑指南:我曾经在一个项目里发现,有人把根证书的密钥用途设成了“数字签名”。结果固件签名证书的私钥泄露后,攻击者直接用固件签名证书的私钥给自己签发了一个中间 CA 证书……嗯,从那以后,我对密钥用途的检查就变得极其严格。
3.5 知识体系总览
下面这张图,把这一节的核心逻辑串起来了。你可以把它当成一个“信任链”的路线图:
3.6 实践中的证书链验证
最后,咱们看看代码里怎么验证证书链。下面是一个简化的验证流程:
// 伪代码:证书链验证
bool verify_cert_chain(cert_t *leaf, cert_t *intermediate, cert_t *root) {
// 1. 验证叶子证书由中间 CA 签发
if (!verify_signature(leaf->tbs, leaf->signature, intermediate->pubkey)) {
return false;
}
// 2. 验证中间 CA 证书由根 CA 签发
if (!verify_signature(intermediate->tbs, intermediate->signature, root->pubkey)) {
return false;
}
// 3. 验证根证书是自签名(信任锚)
if (!verify_signature(root->tbs, root->signature, root->pubkey)) {
return false;
}
// 4. 检查有效期
if (!check_validity_period(leaf) ||
!check_validity_period(intermediate) ||
!check_validity_period(root)) {
return false;
}
// 5. 检查密钥用途
if (!check_key_usage(leaf, DIGITAL_SIGNATURE) ||
!check_key_usage(intermediate, KEY_CERT_SIGN) ||
!check_key_usage(root, KEY_CERT_SIGN)) {
return false;
}
return true;
}
这段代码看起来简单,但实际项目里坑很多。我遇到过最典型的问题是:证书链顺序搞反了。有人把根证书当叶子证书传进来,结果验签当然失败。还有一次,中间 CA 证书的“基本约束”扩展里没设 CA:TRUE,导致验证通不过——嗯,那是个下午,我 debug 了整整三个小时才找到原因。
重要提醒:在嵌入式环境里,证书解析和验证的代码一定要自己写,或者用经过审计的轻量级库(比如 Mbed TLS、WolfSSL)。不要直接移植 OpenSSL,那玩意儿在资源受限的 MCU 上跑不动,而且攻击面太大。