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 数字签名:把哈希和密钥结合起来

数字签名不是加密,也不是哈希,而是哈希 + 非对称加密的组合拳。

流程很简单:

  1. 签名方(服务器/开发机):
    • 对固件做 SHA-256 哈希,得到 digest。
    • 用私钥对 digest 做签名运算,得到 signature。
    • 把 signature 附加到固件后面,或者单独存放。
  2. 验签方(设备 bootloader):
    • 读取固件,同样做 SHA-256 哈希,得到 digest'。
    • 用公钥对 signature 做验签运算,得到 digest''。
    • 比较 digest' 和 digest''。一致则通过,否则拒绝启动。

为什么要先哈希再签名?直接对固件签名不行吗?

行,但没必要。固件可能几兆字节,非对称加密算法处理大块数据非常慢。哈希一下,把数据压缩到 32 字节,再签名,效率高得多。

下面这张图展示了整个流程:

数字签名与验签流程 固件 (Firmware.bin) SHA-256 哈希运算 摘要 (Digest) 32 字节 私钥签名 → 签名值 固件 + 签名值 SHA-256 哈希运算 摘要 (Digest') 公钥验签 → 摘要 (Digest'') 比较 Digest' == Digest'' → 验签通过 ✅

避坑指南: 验签时一定要做“恒定时间比较”,不要用 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 或更高

嗯,这一章的内容就到这里。密码学基础是安全启动的根基,理解清楚哈希、非对称加密和签名的关系,后面写代码时才不会跑偏。