2、密码学基础回顾:哈希算法(SHA256)、非对称加密(RSA/ECC)、数字签名原理
做安全启动,说白了就是解决两个问题:“这代码是谁写的?” 和 “代码被人改过没?”。
密码学就是回答这两个问题的工具。我刚开始接触嵌入式安全时,总觉得这些数学概念离底层开发很远。直到有一次,我在一个 IoT 项目里发现 bootloader 根本没校验签名,攻击者直接替换了固件……嗯,从那以后我再也不敢跳过密码学基础了。
这一章,我们只聊三个核心概念:哈希、非对称加密、数字签名。不推公式,只讲工程里怎么用。
2.1 哈希算法:数据的“指纹”
哈希算法,你可以把它想象成给一段数据生成一个唯一的“身份证号”。不管输入是 1 个字节还是 1 个 GB,输出长度固定。而且,哪怕只改了一个 bit,输出的哈希值也会完全不一样。
在安全启动里,我们最常用的是 SHA-256。它输出 256 位,也就是 32 个字节。
SHA-256 的核心特性:
- 单向性:从哈希值无法反推出原始数据。你想想看,如果能反推,那密码存储就全废了。
- 抗碰撞性:几乎不可能找到两个不同的输入,产生相同的哈希值。虽然理论上存在,但计算量大到不现实。
- 雪崩效应:输入微小变化,输出天翻地覆。
我在项目中遇到过一个问题:有人把固件哈希值存在了外部 Flash 里,结果攻击者同时篡改了固件和哈希值。这等于没校验。所以,哈希值本身必须被保护——要么存在芯片内部 OTP,要么用签名来保护。
下面是一个简单的 SHA-256 计算示例,用 OpenSSL 命令行就能跑:
# 计算固件文件的 SHA-256 哈希值
openssl dgst -sha256 firmware.bin
# 输出示例:
# SHA256(firmware.bin)= a3f5b8c9d1e2f4a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9
工程小技巧: 在嵌入式环境里,MCU 通常有硬件 SHA 加速器。别自己用软件实现,又慢又容易踩坑。我习惯直接调用硬件 HAL 库接口,一次计算几毫秒就搞定。
2.2 非对称加密:公钥与私钥
对称加密就像一把钥匙开一把锁,加密解密用同一个密钥。问题来了——这个密钥怎么安全地分发给所有设备?
非对称加密解决了这个问题。它用一对密钥:私钥自己藏好,公钥公开出去。私钥签名,公钥验签;或者公钥加密,私钥解密。
在安全启动场景里,我们只用它的签名/验签功能,不用加密功能。为什么?因为验签只需要公钥,而公钥可以明文存到芯片里,不怕被人看到。
目前主流方案有两种:
| 算法 | 密钥长度 | 签名长度 | 计算速度 | 典型应用 |
|---|---|---|---|---|
| RSA | 2048 / 4096 位 | 与密钥长度相同 | 验签较快,签名较慢 | 传统 MCU、Linux 启动 |
| ECC | 256 / 384 位 | 约 64 字节(256位曲线) | 整体更快,密钥更短 | 资源受限的 IoT 设备 |
我个人习惯:如果 MCU 有硬件 RSA/ECC 加速器,优先用 ECC。密钥短、签名短、省 Flash 空间。如果芯片比较老,没有硬件加速,RSA 的验签速度其实也够用——毕竟 bootloader 只验一次签。
注意: 千万别用 RSA-1024 或更短的密钥。NIST 已经明确要求 2048 位以上。我曾经见过一个产品用了 1024 位 RSA,被安全审计直接打回。省那点计算时间,不值得。
2.3 数字签名:把哈希和密钥结合起来
数字签名不是加密,也不是哈希,而是哈希 + 非对称加密的组合拳。
流程很简单:
- 签名方(服务器/开发机):
- 对固件做 SHA-256 哈希,得到 digest。
- 用私钥对 digest 做签名运算,得到 signature。
- 把 signature 附加到固件后面,或者单独存放。
- 验签方(设备 bootloader):
- 读取固件,同样做 SHA-256 哈希,得到 digest'。
- 用公钥对 signature 做验签运算,得到 digest''。
- 比较 digest' 和 digest''。一致则通过,否则拒绝启动。
为什么要先哈希再签名?直接对固件签名不行吗?
行,但没必要。固件可能几兆字节,非对称加密算法处理大块数据非常慢。哈希一下,把数据压缩到 32 字节,再签名,效率高得多。
下面这张图展示了整个流程:
避坑指南: 验签时一定要做“恒定时间比较”,不要用 memcmp。为什么?因为 memcmp 一旦发现第一个字节不同就立即返回,攻击者可以通过测量返回时间来猜测签名内容。我曾经在代码审查里抓到过这个问题,改成了安全比较函数。
2.4 密钥管理:安全启动的命门
算法选得再好,密钥管不好,一切白搭。
在 bootloader 场景里,公钥必须存储在芯片内部,而且最好是一次性烧写、不可更改的。常见的做法有:
- OTP (One-Time Programmable) 存储器:芯片出厂时烧录公钥哈希,或者直接烧录公钥本身。
- eFuse:通过熔断丝的方式写入,一旦写入无法修改。
- 安全元件 (SE) 或 TEE:公钥存储在隔离的安全环境中,bootloader 通过安全通道读取。
私钥呢?私钥必须保存在绝对安全的离线环境中。我见过最离谱的事:有人把私钥放在 Git 仓库里,还忘了加 .gitignore。嗯,那项目后来整个密钥体系都废了。
关键原则: 私钥永远不应该出现在生产设备上。设备只需要公钥。如果有人告诉你“设备里也要存私钥”,请立刻拒绝——那说明设计有问题。
2.5 工程中的常见误区
做安全启动这几年,我踩过不少坑,也帮别人填过不少坑。总结几个最常见的:
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 只用哈希校验,不做签名 | 攻击者可同时篡改固件和哈希值 | 哈希值必须被签名保护,或存储在不可篡改的区域 |
| 公钥存储在可写 Flash 中 | 攻击者可替换公钥,用自己的私钥签名恶意固件 | 公钥必须存储在 OTP 或 eFuse 中 |
| 验签失败后继续启动 | 安全启动形同虚设 | 验签失败必须进入错误处理流程,不能启动 |
| 使用弱哈希算法(如 MD5、SHA-1) | 可被碰撞攻击,伪造合法固件 | 至少使用 SHA-256,推荐 SHA-384 或更高 |
嗯,这一章的内容就到这里。密码学基础是安全启动的根基,理解清楚哈希、非对称加密和签名的关系,后面写代码时才不会跑偏。