3、密码学基础(下):公钥基础设施(PKI)、证书链、根证书与中间证书

上一讲我们聊了哈希、对称加密和非对称加密。那些是工具,是砖块。今天要讲的 PKI,就是把这些砖块砌成一座可信大厦的蓝图。说白了,PKI 解决的是一个很朴素的问题:你怎么确定你拿到的公钥,真的是属于对方的?

我在做第一个固件签名项目时,就踩过这个坑。当时我直接从服务器下载了一个公钥文件,拿来验签,结果怎么都验不过。折腾了两天,最后发现那个公钥被人篡改过。嗯,从那以后我才真正理解了 PKI 存在的意义。

3.1 什么是 PKI?

PKI,全称 Public Key Infrastructure,公钥基础设施。它不是某个算法,而是一整套体系。包括证书颁发机构(CA)、注册机构(RA)、证书吊销列表(CRL)、数字证书本身,以及相关的管理策略。

你可以把 PKI 想象成一个数字世界的身份证系统。CA 就是公安局,它负责发证、验证身份。数字证书就是你的身份证,上面写着「我是谁,我的公钥是什么,谁给我担保的」。

核心要点:PKI 的本质是建立信任链。你不需要认识所有人,你只需要信任一个根,然后让这个根去担保其他人。

3.2 数字证书长什么样?

数字证书最常用的标准是 X.509。我习惯把它拆成三部分来看:

  • 主体信息:证书持有者的身份,比如域名、组织名、公钥
  • 颁发者信息:谁签发了这张证书
  • 签名:颁发者用自己的私钥对上述内容做的数字签名

一个典型的 X.509 证书结构如下:

证书:
  版本号: V3
  序列号: 0x1A2B3C4D
  签名算法: SHA256-RSA
  颁发者: CN=MyRootCA, O=MyCompany
  有效期:
    开始: 2024-01-01
    结束: 2029-01-01
  主体: CN=firmware-sign.mycompany.com
  主体公钥: (RSA 2048位公钥)
  扩展:
    密钥用途: digitalSignature
    基本约束: CA=FALSE
  签名值: (256字节的RSA签名)

这里有个细节要注意。证书里的「基本约束」扩展,标记了这张证书能不能继续签发下级证书。CA 证书的 CA=TRUE,终端证书的 CA=FALSE。这个标记非常重要,我后面会讲为什么。

3.3 证书链:信任是如何传递的?

你想想看,如果每个设备都要内置所有可信公钥,那根本不现实。所以就有了证书链的概念。

证书链是一条从根证书到终端证书的路径。每一级证书都由上一级签发,最终追溯到根证书。根证书是自签名的,也就是自己给自己担保。

我画了一张图,帮你理解这个链条:

证书链信任模型 根证书 (Root CA) 自签名 · 内置在设备中 中间证书 (Intermediate CA) 由根CA签发 固件签名证书 由中间CA签发 固件镜像 信任锚点 设备出厂时预置根证书 这是信任的起点 验证路径 从固件证书 → 中间CA → 根CA,逐级验证签名 最终信任 根证书可信 → 整条链可信 固件签名有效

验证过程是这样的:设备拿到固件和签名后,先看签名证书是谁签发的。发现是中间 CA,就去查中间 CA 的证书。中间 CA 又是根 CA 签发的。根 CA 的证书已经内置在设备里了,是信任的起点。只要每一级的签名都验证通过,整条链就是可信的。

我的经验:在固件升级场景中,我建议把根证书和中间证书都预置在设备里。这样即使网络不通,设备也能完成证书链的本地验证。但根证书一定要用安全存储,最好是 OTP 或者安全元件,防止被篡改。

3.4 根证书与中间证书:为什么要分层?

你可能会有疑问:为什么不能直接用根证书签固件?非要中间插一层?

原因很简单:安全隔离。根证书是整个信任体系的命根子。如果根证书私钥泄露,整个 PKI 体系就崩塌了。所以根证书的私钥通常放在离线环境里,甚至放在硬件加密机里,一年可能就用几次。

中间证书则相对灵活。它可以放在在线服务器上,用于日常签发。万一中间证书的私钥泄露了,我们只需要吊销它,再用根证书签发一个新的中间证书就行。根证书本身不受影响。

我整理了一个对比表,帮你理解两者的区别:

特性 根证书 中间证书
签名方式 自签名 由根证书签发
私钥存储 离线、硬件加密机 在线服务器
使用频率 极低(年/次) 高(日/次)
泄露影响 整个体系崩塌 可吊销、可替换
有效期 长(10-20年) 短(2-5年)
基本约束 CA=TRUE, pathLen=1+ CA=TRUE, pathLen=0

这里有个关键参数:pathLenConstraint。它限制了证书链的深度。比如根证书设置 pathLen=1,那它下面只能有一级中间证书,中间证书不能再签发下级证书。这个约束能防止证书链无限延伸。

3.5 固件签名中的证书链实践

在实际的固件签名流程中,证书链是怎么用的?我拿一个典型的嵌入式设备升级场景来说:

  1. 生产阶段:设备出厂时,烧录根证书和中间证书到安全存储区
  2. 签名阶段:固件开发完成后,用签名证书的私钥对固件哈希做签名
  3. 打包阶段:将固件、签名值、签名证书、中间证书打包成一个升级包
  4. 验证阶段:设备收到升级包后,先验证证书链,再用签名证书的公钥验签

验证阶段的代码逻辑大致是这样的:

// 伪代码:固件签名验证流程
bool verify_firmware(const uint8_t* firmware, size_t fw_len,
                     const cert_t* sign_cert,
                     const cert_t* intermediate_cert,
                     const cert_t* root_cert) {
    // 1. 验证中间证书由根证书签发
    if (!verify_cert_signature(intermediate_cert, root_cert)) {
        return false;  // 中间证书不可信
    }

    // 2. 验证签名证书由中间证书签发
    if (!verify_cert_signature(sign_cert, intermediate_cert)) {
        return false;  // 签名证书不可信
    }

    // 3. 检查证书有效期
    if (!check_cert_validity(sign_cert)) {
        return false;  // 证书已过期
    }

    // 4. 检查证书吊销状态(可选)
    if (!check_cert_revocation(sign_cert)) {
        return false;  // 证书已被吊销
    }

    // 5. 用签名证书的公钥验证固件签名
    if (!verify_signature(firmware, fw_len, sign_cert->public_key)) {
        return false;  // 固件签名无效
    }

    return true;  // 全部通过
}

注意:我曾经在一个项目里发现,有人只验证了证书链,却忘了检查证书有效期。结果设备升级了一个已经过期两年的证书签名的固件。虽然签名本身没问题,但证书过期意味着安全策略已经失效。所以验证证书链时,有效期检查绝对不能少。

3.6 证书吊销:信任的撤回

证书不是发了就永远有效。如果私钥泄露了,或者员工离职了,证书需要被吊销。常用的吊销机制有两种:

  • CRL(证书吊销列表):CA 定期发布一个列表,列出所有被吊销的证书序列号。设备需要下载这个列表,在验证时检查证书是否在列表中。
  • OCSP(在线证书状态协议):设备实时向 OCSP 服务器查询某张证书的状态。响应更快,但需要网络连接。

在固件场景中,我建议用 CRL。因为很多嵌入式设备没有实时联网能力。你可以把 CRL 打包在固件升级包里一起下发,设备在升级时顺便更新吊销列表。

不过要注意,CRL 本身也需要签名验证。你不能随便拿个列表就信了。CRL 是由 CA 签名的,验证方式和验证证书一样。

3.7 固件签名中的常见坑

最后分享几个我在项目中踩过的坑:

  • 证书链不完整:升级包里只带了签名证书,没带中间证书。设备只有根证书,无法构建完整的证书链。解决办法:打包时一定要包含完整的证书链,从签名证书到根证书(根证书可以省略,因为设备已有)。
  • 时间戳问题:设备没有 RTC 或者 RTC 电池没电了,导致系统时间回到 1970 年。这时候检查证书有效期,所有证书都显示「未生效」。我建议在固件验证时,如果系统时间不可信,可以跳过有效期检查,但要在日志里记录警告。
  • 根证书硬编码:有人把根证书直接硬编码在代码里,用 const 数组存着。这本身没问题,但要注意根证书过期后怎么更新。我习惯在固件升级包里预留一个「根证书更新」的专用区域,允许通过安全通道更新根证书。

好了,这一讲的内容就到这里。PKI 体系看起来复杂,但核心逻辑其实很简单:信任一个根,然后让这个根去担保其他东西。你只要把根证书管好,整个链条就安全了。