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的典型流程是这样的:
- 芯片上电,BootROM从OTP中读取根证书公钥
- 用根公钥验证中间证书的签名
- 用中间证书的公钥验证固件签名证书
- 用固件签名证书的公钥验证固件本身的签名
为什么要搞这么复杂?直接存一把公钥不行吗?
当然可以,但那样的话,一旦私钥泄露,你就要召回所有设备。有了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的设计、信任根的建立、以及如何防止物理攻击。到时候我会分享一些我在车规级芯片上做安全启动的真实案例,保证比这一章更刺激。
记住一句话:密码学工具本身是安全的,但用错地方、用错方式,再强的算法也救不了你。安全启动的成败,往往不在算法强度,而在实现细节。