3. 密码学基础(下):数字签名(RSA/ECDSA)、公钥基础设施(PKI)、证书链

好,咱们接着聊。上一节我们把哈希和对称/非对称加密理清了,这一节要啃的,是安全启动真正的“脊梁骨”——数字签名、PKI 和证书链。

你想想看,安全启动最核心的问题是什么?其实就是一句话:我怎么知道这段代码是“谁”写的,而且没被人动过手脚?哈希只能保证完整性,加密只能保证机密性。但“谁写的”这个问题,得靠数字签名来回答。

3.1 数字签名:不只是“加密的哈希”

很多人刚接触数字签名时,觉得它就是“把哈希值用私钥加密一下”。这个说法在 RSA 场景下勉强能说得通,但到了 ECDSA 就不对了。我建议你从“签名是独立算法”这个角度去理解,而不是把它当成加密的变种。

3.1.1 RSA 签名:先哈希,再私钥操作

RSA 签名的流程,说白了就是两步:

  1. 对消息做哈希,得到一个固定长度的摘要(比如 SHA-256 的 32 字节)。
  2. 用私钥对这个摘要做“签名运算”(其实就是 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 证书链:从根到叶子

证书链,说白了就是“信任的传递”。

你想想看:如果每个设备都只信任一个根证书,那根证书的私钥就得用来签所有固件。这太危险了——万一私钥泄露,整个产品线都得完蛋。

所以,我们搞了个分层结构:

  1. 根 CA:自签名证书,私钥锁在 HSM 或安全芯片里,几乎不用。
  2. 中间 CA:由根 CA 签发,用于签发具体的固件或设备证书。
  3. 最终实体证书:由中间 CA 签发,直接用于签名固件。

验签的时候,设备从固件签名里拿到最终实体证书,然后一路向上验证,直到找到设备里预置的根证书。只要链条上的每一环都验证通过,信任就建立起来了。

核心原则:设备里只存根证书。中间 CA 和最终实体证书都随固件一起下发。这样即使某个中间 CA 的私钥泄露,你只需要吊销那张证书,重新签发一张就行,不用动设备里的根证书。

3.4 证书格式:X.509 长什么样?

在嵌入式世界里,我们最常用的证书格式是 X.509 v3。一个典型的证书包含:

  • 版本号:通常是 v3。
  • 序列号:CA 分配的唯一编号。
  • 签名算法:比如 sha256WithRSAEncryption。
  • 颁发者:签发这个证书的 CA 名称。
  • 有效期:起止时间。
  • 主体:证书持有者的名称。
  • 公钥:持有者的公钥。
  • 扩展:比如密钥用途(digitalSignature)、基本约束(CA:TRUE/FALSE)。

在安全启动里,我们最关心的是 密钥用途扩展。根证书的密钥用途通常只允许“签发证书”,而固件签名证书的密钥用途是“数字签名”。这样即使固件签名证书被滥用,也无法签发新的证书。

避坑指南:我曾经在一个项目里发现,有人把根证书的密钥用途设成了“数字签名”。结果固件签名证书的私钥泄露后,攻击者直接用固件签名证书的私钥给自己签发了一个中间 CA 证书……嗯,从那以后,我对密钥用途的检查就变得极其严格。

3.5 知识体系总览

下面这张图,把这一节的核心逻辑串起来了。你可以把它当成一个“信任链”的路线图:

数字签名与证书链知识体系 数字签名与证书链知识体系 数字签名 RSA 签名 ECDSA 公钥基础设施 (PKI) CA 机构 X.509 证书 证书链 根 CA 中间 CA 实体 核心逻辑:数字签名提供“谁签的”,PKI 提供“怎么信任公钥”,证书链提供“信任怎么传递” 三者结合,构成安全启动的信任基础

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 上跑不动,而且攻击面太大。