3. 密码学基础(下):公钥基础设施(PKI)、证书链、根信任锚

上一节我们聊了对称与非对称加密、哈希和签名这些基础工具。但有个问题一直悬着:你拿到了一个公钥,怎么确定它真的是设备A的,而不是中间人伪造的?

这就引出了我们今天的主角——公钥基础设施(PKI)。说白了,PKI就是一套管理公钥的规则和系统。它解决的核心问题就一个:信任传递。

我在做物联网固件安全方案时,最头疼的往往不是签名算法本身,而是怎么安全地把公钥分发到每一台设备上。PKI就是用来干这个的。

3.1 什么是PKI?

PKI不是某个单一技术,而是一套完整的体系。它包括:

  • 证书颁发机构(CA):负责签发和管理数字证书的权威机构
  • 注册机构(RA):负责验证申请者身份的辅助机构
  • 证书库:存储和发布证书及证书撤销列表(CRL)的地方
  • 终端实体:持有证书和私钥的设备或个人

你想想看,如果没有PKI,每个设备都得预置所有信任的公钥。设备一多,这根本管不过来。PKI让我们只需要信任一个根CA,其他信任关系都可以通过证书链推导出来。

核心概念:PKI的本质是建立一个可扩展的信任模型。它把“信任谁”这个问题,从O(n)的复杂度降到了O(1)。你只需要信任一个根,剩下的交给证书链。

3.2 数字证书——公钥的身份证

数字证书就是公钥的身份证。它把公钥和持有者的身份信息绑定在一起,再由CA对这个绑定关系进行数字签名。

最常见的证书格式是X.509 v3。我刚开始接触时,觉得证书字段太多了。后来在项目中调试固件签名验证失败的问题,才发现每个字段都有它的用处。

一个典型的X.509证书包含以下关键字段:

字段 说明 我的经验
版本号 证书格式版本,目前主流是v3 v3引入了扩展字段,非常实用
序列号 CA分配给证书的唯一编号 撤销证书时靠它来定位
签名算法 CA签名使用的算法,如SHA256-RSA 务必确认固件验证器支持该算法
颁发者 签发该证书的CA名称 链式验证时逐级比对
有效期 证书生效和失效时间 设备时钟不准会导致验证失败,踩过坑
主体 证书持有者的身份信息 固件签名证书通常用设备ID或厂商名
公钥 持有者的公钥信息 包含算法标识和公钥值
扩展 v3新增的附加信息 密钥用途、基本约束等,验证时经常用到
签名值 CA对上述字段的签名 验证完整性的关键

小技巧:在嵌入式设备中,证书解析代码要特别注意内存管理。我曾经见过一个设备因为解析证书扩展字段时缓冲区溢出,导致整个签名验证流程被绕过。嗯,解析代码一定要做严格的长度检查。

3.3 证书链——信任的传递

证书链是PKI最精妙的设计之一。它解决了“如何让成千上万的设备都信任一个根”的问题。

举个例子。根CA的证书是自签名的,也就是它用自己的私钥签自己的公钥。根CA给中间CA签发证书,中间CA再给终端设备签发证书。这样就形成了一条链:

根CA证书 → 中间CA证书 → 设备证书

验证的时候,从设备证书开始,逐级向上验证,直到找到我们信任的根证书。每一级都用上一级的公钥来验证下一级证书的签名。

为什么会这样设计?因为根CA的私钥太重要了,不能天天拿出来用。中间CA的私钥即使泄露,也只需要撤销中间CA证书,根CA证书不受影响。

下面这张图展示了证书链的信任传递过程:

证书链信任传递示意图 证书链信任传递流程 根CA证书 自签名 · 信任锚 签发 中间CA证书 由根CA签发 签发 设备证书 由中间CA签发 验证方向 验证时从设备证书开始,逐级向上验证签名,直到找到预置的根证书

注意:证书链的长度不是越长越好。每增加一级,验证时间就增加一次签名验证的开销。在嵌入式设备上,我建议证书链不超过3级。我曾经在一个项目里用了5级链,结果设备启动时固件验证花了将近2秒,用户直接投诉开机太慢。

3.4 根信任锚——信任的起点

根信任锚,就是那个我们无条件信任的根证书。在固件签名验证中,根证书通常被烧录在设备的只读存储器(ROM)或一次性可编程(OTP)区域。

为什么叫“锚”?因为它就是信任的起点,像船锚一样固定住整个信任体系。如果根证书被篡改,整个PKI体系就崩塌了。

我个人习惯把根证书的哈希值也存储在设备中。验证时先计算根证书的哈希,跟存储的哈希比对,确保根证书本身没有被替换。这叫“防根证书替换攻击”。

根信任锚的存储方式通常有几种:

  • 硬件OTP:一次性可编程存储器,写入后不可更改。最安全,但灵活性差。
  • 安全元件(SE):独立的加密芯片,专门存储密钥和证书。安全性高,成本也高。
  • 软件保护区:利用MPU或MMU保护的存储区域。成本低,但安全性依赖系统完整性。

我在一个智能门锁项目里,客户要求根证书必须能远程更新。最后我们采用了“双根”方案:一个硬件OTP存储的不可变根,一个安全存储区的可更新根。更新时需要两个根同时签名才能生效。这样既保证了安全性,又保留了灵活性。

3.5 证书撤销——信任的终结

证书不是永远有效的。私钥泄露、证书信息变更、员工离职……这些情况都需要撤销证书。

证书撤销有两种主要机制:

机制 原理 适用场景
CRL(证书撤销列表) CA定期发布已撤销证书的序列号列表 离线设备、网络条件差的场景
OCSP(在线证书状态协议) 实时查询单个证书的状态 联网设备、需要即时响应的场景

在固件签名验证中,我建议优先使用CRL。因为OCSP需要设备联网,而很多嵌入式设备在固件升级时可能处于离线状态。CRL可以随固件包一起下发,设备在验证签名时顺便检查证书是否被撤销。

避坑指南:我曾经遇到一个设备,因为CRL文件太大(包含了几万个已撤销证书),设备内存不够解析,导致固件验证失败。后来我们改用了“增量CRL”,只包含最近撤销的证书,问题就解决了。如果你的设备内存有限,一定要考虑CRL的大小。

3.6 固件签名中的PKI实践

好了,理论讲完了。我们来看看在固件签名验证中,PKI具体怎么用。

一个典型的固件签名验证流程是这样的:

  1. 固件打包阶段:厂商用设备证书对应的私钥对固件哈希进行签名,生成签名值。将固件、签名值、设备证书打包在一起。
  2. 设备验证阶段:设备收到固件包后,先验证设备证书的合法性(证书链验证),再用证书中的公钥验证固件签名。
  3. 信任锚检查:证书链验证的终点是设备中预置的根证书。如果根证书匹配,整个信任链成立。

这里有个关键点:设备证书的私钥必须安全存储。如果私钥泄露,攻击者就可以用这个私钥签名恶意固件,而设备仍然会信任它。

我建议在硬件层面使用密钥存储机制,比如ARM的TrustZone或独立的SE芯片。软件层面的密钥保护,说实话,在经验丰富的攻击者面前基本等于没有。

下面是一个简化的固件签名验证代码示例:

// 固件签名验证流程(伪代码)
bool verify_firmware(const uint8_t* firmware, size_t fw_size,
                     const uint8_t* signature, size_t sig_size,
                     const cert_t* device_cert) {
    // 1. 验证证书链
    cert_t* current = device_cert;
    while (current->issuer != current->subject) {  // 不是自签名证书
        cert_t* issuer_cert = find_cert(current->issuer);
        if (!verify_cert_signature(current, issuer_cert)) {
            return false;  // 证书签名验证失败
        }
        current = issuer_cert;
    }
    
    // 2. 验证根证书是否在信任锚列表中
    if (!is_trusted_root(current)) {
        return false;  // 根证书不受信任
    }
    
    // 3. 用设备证书的公钥验证固件签名
    uint8_t hash[SHA256_SIZE];
    sha256(firmware, fw_size, hash);
    
    return rsa_verify(hash, SHA256_SIZE, 
                      signature, sig_size,
                      &device_cert->public_key);
}

这段代码看起来简单,但实际项目中要考虑的细节很多。比如证书查找的效率、内存分配、错误处理、时间戳验证等等。每个环节都可能成为攻击面。

核心要点:PKI不是银弹。它解决的是“公钥分发”的问题,但解决不了“私钥保护”的问题。再完善的PKI体系,如果私钥保护做得不好,一切都是空谈。我在项目中见过太多因为私钥泄露导致整个安全体系崩溃的案例了。

嗯,PKI的内容就讲到这里。记住一句话:信任是传递的,但安全是环环相扣的。每个环节都要做到位,才能构建真正可信的固件签名验证体系。