3、密码学基础(上):哈希函数、数字签名与PKI在安全启动中的应用

各位同学,欢迎来到密码学基础的上半部分。说实话,很多做嵌入式开发的工程师,一听到「密码学」三个字就头大,觉得那是数学博士才搞的东西。但我想告诉你,在安全启动这个领域,你不需要成为密码学专家,你只需要理解几个核心工具怎么用、为什么用、以及用的时候容易踩哪些坑。

我个人习惯把安全启动比作「给固件办身份证」。哈希函数是提取指纹,数字签名是盖公章,PKI就是那个发公章的权威机构。这三样东西配合好了,你的设备才能信任它自己跑的程序。好,我们一个一个来看。

3.1 哈希函数:固件的「指纹」

哈希函数,说白了就是一个单向的压缩算法。你给它任意长度的数据,它给你一个固定长度的摘要。这个摘要就像人的指纹——理论上,世界上没有两片完全相同的雪花,也没有两个不同文件能算出完全相同的哈希值。

核心特性:

  • 单向性:从哈希值反推原始数据,几乎不可能。我在项目中见过有人问「能不能从SHA-256值还原出固件」——嗯,如果能的话,比特币早就崩盘了。
  • 抗碰撞性:找不到两个不同的输入,使得它们的哈希值相同。SHA-256的碰撞概率,比你连续中两次彩票头奖还要低得多。
  • 雪崩效应:输入改一个比特,输出会有一半以上的比特发生变化。这个特性很重要,后面讲固件完整性校验时会用到。

在安全启动中,哈希函数主要干两件事:

  • 完整性校验:计算固件的哈希值,和出厂时记录的哈希值对比。不一样?说明固件被篡改了。
  • 数字签名的预处理:签名算法通常不直接对固件签名,而是对固件的哈希值签名。为什么?因为RSA签名几百字节的数据和签名几兆字节的数据,性能差太多了。

SHA-256 vs SHA-384,怎么选?

算法 输出长度 安全强度 典型应用场景
SHA-256 256 bits (32 bytes) 128 bits 大多数嵌入式设备、IoT固件校验
SHA-384 384 bits (48 bytes) 192 bits 高安全等级设备、车规级、金融终端

我个人建议:如果你的MCU主频在100MHz以下,老老实实用SHA-256。SHA-384的计算量大约多出50%,对于资源受限的设备来说,这个代价不小。我曾经在一个Cortex-M4的项目上试过SHA-384,结果启动时间多了200毫秒——产品经理当场就急了。

避坑指南: 我曾经遇到过一个问题:固件升级后,设备变砖了。查了半天,发现是哈希计算时把固件头部的签名区域也算进去了。签名本身是附加在固件后面的,计算哈希时一定要排除签名区域,否则你永远算不出和出厂一致的值。

3.2 数字签名:谁签的?有没有被改过?

哈希只能保证完整性,但保证不了「这个固件到底是谁写的」。数字签名就是来解决这个问题的。

数字签名的核心思想很简单:

  • 私钥签名,公钥验签
  • 私钥只有固件发布者持有,公钥可以公开
  • 签名验证通过,说明固件确实来自持有私钥的一方,且未被篡改

RSA vs ECDSA,实战中的选择

这两个算法是目前嵌入式领域最主流的签名方案。我直接说结论:

对比项 RSA ECDSA
密钥长度 2048/4096 bits 256/384 bits
签名速度 慢(私钥操作)
验签速度 快(公钥操作) 较快
存储开销 大(公钥、签名都大)
硬件加速 很多MCU内置RSA引擎 较新的MCU才支持

你想想看,在安全启动的场景下,设备端只做验签,不做签名。所以RSA的验签速度快这个优势就很香。但ECDSA的密钥和签名都短很多,对于Flash空间紧张的小设备来说,ECDSA可能是唯一的选择。

我记得有一次给一个智能门锁做安全启动方案,Flash总共就512KB,固件本身占了400KB。如果用RSA-2048,光公钥和签名就要占掉将近1KB。换成ECDSA P-256,公钥只有32字节,签名64字节——省下来的空间够多存好几条开锁记录了。

重要提醒: 千万不要在设备里硬编码私钥!我见过不止一个团队,为了方便测试,把签名私钥直接编译进了固件里。这等于你把自家大门钥匙挂在门把手上,然后跟小偷说「你看,我有锁」。

3.3 PKI:谁来证明这把公钥是真的?

好,现在你有了数字签名,设备里也存了公钥。但问题来了:设备怎么知道它存的这把公钥,真的是固件发布者的公钥?

这就是PKI要解决的问题。PKI说白了就是一个信任链:

  • 根证书(Root CA)是信任的锚点,通常由芯片厂商或设备制造商预置在芯片的一次性可编程存储器(OTP)里
  • 中间证书由根证书签发,用于日常固件签名
  • 固件签名证书由中间证书签发,直接用于验证固件

在安全启动中,PKI的典型流程是这样的:

  1. 芯片上电,BootROM从OTP中读取根证书公钥
  2. 用根公钥验证中间证书的签名
  3. 用中间证书的公钥验证固件签名证书
  4. 用固件签名证书的公钥验证固件本身的签名

为什么要搞这么复杂?直接存一把公钥不行吗?

当然可以,但那样的话,一旦私钥泄露,你就要召回所有设备。有了PKI,你只需要吊销中间证书或固件签名证书,根证书还在,设备还能继续用。我在一个车规级项目上就遇到过这种情况——供应商的签名私钥泄露了,幸好我们用了三层PKI结构,只换了中间证书,避免了数万辆车的召回。

安全启动中的PKI最佳实践:

  • 根证书密钥必须离线保存,最好放在硬件安全模块(HSM)里
  • 中间证书和固件签名证书要有有效期,过期后必须重新签发
  • 设备端要支持证书吊销列表(CRL)或在线证书状态协议(OCSP)
  • 证书链的长度不要超过3层,否则启动时间会变得不可接受

3.4 实战:一个完整的安全启动验证流程

说了这么多理论,我们来走一遍实际流程。假设你有一个基于ARM Cortex-M的IoT设备:

// 伪代码:安全启动验证流程
void secure_boot(void) {
    // 1. 从OTP读取根公钥
    uint8_t root_pubkey[32]; // ECDSA P-256
    read_otp(OTP_ROOT_PUBKEY, root_pubkey, 32);
    
    // 2. 从Flash读取中间证书并验证
    Cert intermediate_cert;
    read_flash(FLASH_INTERMEDIATE_CERT, &intermediate_cert);
    if (!ecdsa_verify(root_pubkey, intermediate_cert.hash, intermediate_cert.signature)) {
        enter_error_mode(); // 中间证书验证失败,死机
    }
    
    // 3. 从Flash读取固件签名证书并验证
    Cert firmware_cert;
    read_flash(FLASH_FIRMWARE_CERT, &firmware_cert);
    if (!ecdsa_verify(intermediate_cert.pubkey, firmware_cert.hash, firmware_cert.signature)) {
        enter_error_mode(); // 固件证书验证失败,死机
    }
    
    // 4. 计算固件哈希并验证签名
    uint8_t firmware_hash[32];
    sha256_calculate(FLASH_FIRMWARE_ADDR, FIRMWARE_SIZE, firmware_hash);
    if (!ecdsa_verify(firmware_cert.pubkey, firmware_hash, firmware_signature)) {
        enter_error_mode(); // 固件签名验证失败,死机
    }
    
    // 5. 全部通过,跳转到固件入口
    jump_to_firmware();
}

这段代码看起来简单,但实际项目中要考虑的细节非常多。比如:

  • OTP的写入次数有限,一般只有一次,所以根公钥必须在芯片出厂前就烧录好
  • Flash中的证书和固件签名,必须放在受保护的区域,防止被恶意擦写
  • 验签过程中如果发生错误,不能只是简单地返回错误码,而应该直接进入死循环或复位——防止攻击者绕过安全检查

我的经验: 在调试阶段,可以在BootROM里加一个调试模式,跳过签名验证。但量产固件里一定要把这个功能去掉。我曾经见过一个团队,量产固件里忘了关调试模式,结果被安全研究员轻松攻破——上了Black Hat大会的PPT,那叫一个尴尬。

3.5 小结

好了,这一章的内容就到这里。我们讲了哈希函数怎么给固件提取指纹,数字签名怎么给固件盖公章,PKI怎么建立信任链。这三个东西组合在一起,就构成了安全启动的密码学基础。

下一章我们会深入讲安全启动的具体实现架构,包括BootROM的设计、信任根的建立、以及如何防止物理攻击。到时候我会分享一些我在车规级芯片上做安全启动的真实案例,保证比这一章更刺激。

记住一句话:密码学工具本身是安全的,但用错地方、用错方式,再强的算法也救不了你。安全启动的成败,往往不在算法强度,而在实现细节。