4. 签名与验签机制:非对称加密基础(RSA/ECC)、哈希算法(SHA256)、数字签名生成与验证流程
安全启动的核心,说白了就是回答一个问题:“这段代码到底是不是我信任的人写的?”
你不能靠猜,也不能靠“我觉得”。你需要一套数学上可靠的机制。这套机制,就是数字签名。
我在做第一个安全启动项目时,犯过一个低级错误——只校验了哈希,没验签名。结果呢?攻击者直接替换了整个镜像,哈希值也跟着变了,系统照样启动。嗯,从那以后,我彻底理解了“签名”这两个字的分量。
4.1 非对称加密:两把钥匙的故事
非对称加密,也叫公钥密码学。它有两把钥匙:私钥和公钥。
- 私钥:只有你自己知道,绝对不能泄露。它用来签名。
- 公钥:可以公开给任何人。它用来验签。
你想想看,私钥签名,公钥验签。这个逻辑一旦成立,身份认证和完整性校验就同时解决了。
4.1.1 RSA 与 ECC:两种主流算法
目前嵌入式领域最常用的两种非对称算法,就是 RSA 和 ECC。
| 特性 | RSA | ECC(椭圆曲线) |
|---|---|---|
| 安全性基础 | 大整数分解难题 | 椭圆曲线离散对数难题 |
| 密钥长度(等效安全) | 2048 位(安全) | 256 位(安全) |
| 签名速度 | 较慢 | 较快 |
| 验签速度 | 较快 | 较慢 |
| 资源消耗 | 高(大数运算) | 低(适合 MCU) |
| 典型应用 | PC、服务器、网关 | IoT 设备、车规芯片 |
我的建议:如果你的 MCU 算力有限(比如 Cortex-M0+),优先考虑 ECC。我曾经在一个只有 64KB Flash 的芯片上硬跑 RSA-2048,结果签名验签一次要 3 秒多,用户体验极差。换成 ECC-256 后,时间降到了 200ms 以内。
4.2 哈希算法:数据的“指纹”
非对称加密虽然强大,但它有个缺点:慢。你不能拿私钥去加密整个固件镜像,那会慢到无法接受。
所以,我们引入哈希算法。哈希函数能把任意长度的数据,压缩成一个固定长度的摘要(Digest)。这个摘要就像数据的“指纹”——哪怕只改一个 bit,指纹都会完全变样。
4.2.1 SHA256:当前的主流选择
SHA256 是 SHA-2 家族的一员,输出 256 位(32 字节)的摘要。它在安全性和性能之间取得了很好的平衡。
- 抗碰撞性:找到两个不同输入但 SHA256 值相同,在计算上不可行。
- 单向性:从摘要反推原始数据,几乎不可能。
- 雪崩效应:输入微小变化,输出剧烈变化。
注意:SHA1 已经被证明不安全,不要再用了。我曾经在维护一个老项目时,发现它还在用 SHA1 做签名校验,吓得我连夜写了补丁。SHA1 的碰撞攻击在 2017 年就已经被公开实现,攻击者可以伪造出相同 SHA1 值的恶意固件。
4.3 数字签名生成与验证流程
好了,现在我们把非对称加密和哈希算法结合起来,看看完整的签名流程。
4.3.1 签名生成(在安全的生产环境中完成)
- 计算哈希:对原始固件镜像(Firmware.bin)计算 SHA256 摘要,得到 H。
- 签名:使用私钥(Private Key)对摘要 H 进行加密(RSA 或 ECC 签名算法),生成签名值 S。
- 打包:将原始固件镜像和签名值 S 打包在一起,形成最终的发布文件。
# 伪代码示例:签名生成
firmware_data = read_file("firmware.bin")
digest = SHA256(firmware_data)
signature = RSA_Sign(private_key, digest)
output_package = firmware_data + signature
4.3.2 验签流程(在嵌入式设备启动时执行)
- 分离:从接收到的包中,分离出固件数据和签名值 S。
- 计算哈希:对固件数据计算 SHA256 摘要,得到 H'。
- 解密签名:使用公钥(Public Key)对签名值 S 进行解密,得到原始摘要 H。
- 比较:比较 H 和 H'。如果相等,验签通过;否则,拒绝启动。
# 伪代码示例:验签流程
received_package = read_from_flash()
firmware_data = extract_firmware(received_package)
signature = extract_signature(received_package)
digest_computed = SHA256(firmware_data)
digest_original = RSA_Verify(public_key, signature)
if digest_computed == digest_original:
boot_ok()
else:
boot_fail()
核心要点:验签时,公钥必须被安全地存储在设备内部(通常是 OTP 或 eFuse 区域),并且不能被篡改。如果公钥被替换,攻击者就可以用自己的私钥签名恶意固件。
4.4 知识体系流程图
下面这张图,把整个签名验签的流程和涉及的关键组件串了起来。你可以把它当作本章的“地图”。
4.5 避坑指南与实战经验
理论说完了,聊聊我在实际项目中踩过的坑。
- 公钥存储位置:千万不要把公钥放在可读写的 Flash 区域。我见过一个产品,公钥放在外部 SPI Flash 里,结果攻击者直接替换了公钥,然后用自己的私钥签名固件,安全启动形同虚设。正确的做法是:把公钥哈希烧录到 eFuse 或 OTP 中,或者使用硬件信任根(如 TPM、HSM)来保护。
- 签名算法选择:如果你的芯片有硬件加速单元(如 NXP i.MX 系列、STM32MP1 系列),优先使用硬件支持的算法。纯软件实现 RSA-2048 签名,在低端 MCU 上可能会让启动时间增加数秒。
- 哈希算法不要自己实现:SHA256 的实现看起来简单,但优化不好容易出 bug。我建议直接使用芯片厂商提供的 SDK 库,或者经过认证的开源库(如 mbedTLS、OpenSSL)。
- 注意侧信道攻击:在验签过程中,如果比较两个摘要时使用了不安全的比较函数(如 memcmp),攻击者可以通过时序分析推断出摘要内容。一定要使用“恒定时间比较”函数。
一个小技巧:在开发阶段,你可以把公钥硬编码在代码里,方便调试。但发布前,一定要改成从安全存储区域读取。我曾经因为忘记改这个,导致量产版本的公钥可以被轻易提取,后来花了整整一周做安全补丁。
4.6 小结
数字签名不是魔法,它是数学。RSA 和 ECC 提供了非对称加密的基础,SHA256 提供了数据的唯一指纹,两者结合,就构成了安全启动的信任链。
记住:私钥签名,公钥验签,哈希保证完整性,公钥必须安全存储。这四句话,是安全启动的基石。