4. 签名与安全:固件签名算法(RSA/ECDSA)、哈希校验(SHA256)、安全启动链(Secure Boot)原理
好,咱们进入整个OTA方案里最核心、也最容易翻车的一环——安全。
说实话,我见过太多团队把OTA做通了,结果因为没做签名校验,被黑客塞了个恶意固件进去,整批设备变砖。这种事,一次就够你喝一壶的。所以这一章,咱们把签名、哈希、安全启动链这三个东西彻底讲透。
4.1 为什么需要签名?
你想想看,你的游戏机通过网络下载固件。这个固件在传输过程中,可能被篡改。哪怕只有1个字节变了,设备刷进去就可能死机。更可怕的是,如果有人故意植入恶意代码,你的设备就成了别人的肉鸡。
所以我们需要两件事:
- 完整性校验:固件有没有被改过?
- 来源认证:这个固件是不是我(厂商)发布的?
哈希校验解决第一个问题,数字签名解决第二个问题。两者缺一不可。
核心原则:永远不要信任你下载的东西,除非你能验证它的签名。
4.2 哈希校验:SHA256
哈希算法,说白了就是给固件算一个「指纹」。这个指纹是固定长度的,比如SHA256就是256位(32字节)。哪怕固件里改了一个bit,算出来的哈希值就完全不一样。
我在项目中遇到过一件事:有个同事用MD5做校验,我说不行,MD5已经被破解了,碰撞攻击很容易。他还不信。后来我们做安全审计,发现MD5确实可以被伪造。从那以后,我们团队统一用SHA256。
代码示例,生成固件的SHA256哈希:
# 命令行计算SHA256
sha256sum firmware.bin
# 输出示例:
# a3f5b8c1d2e4f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0 firmware.bin
在嵌入式设备里,你需要在bootloader中实现SHA256校验。流程是这样的:
- 读取固件文件
- 逐块计算SHA256
- 得到32字节的哈希值
- 与固件包中携带的哈希值对比
- 一致则通过,不一致则拒绝升级
我的习惯:计算哈希时,建议把固件头部的签名信息排除在外。只对纯固件数据做哈希,这样签名本身的变化不会影响哈希值。
4.3 数字签名:RSA vs ECDSA
哈希只能防篡改,不能防伪造。因为攻击者也可以自己算一个哈希,替换掉原来的。所以我们需要数字签名。
数字签名的原理,我简单说:
- 厂商用私钥对固件的哈希值进行签名
- 设备用公钥验证签名
- 私钥只有厂商有,公钥可以公开
目前主流的两大算法:RSA和ECDSA。
| 特性 | RSA | ECDSA |
|---|---|---|
| 密钥长度 | 2048位起 | 256位起 |
| 签名速度 | 较慢 | 较快 |
| 验证速度 | 较快 | 较慢 |
| 签名大小 | 256字节 | 64字节 |
| 安全性 | 成熟可靠 | 同等安全下密钥更短 |
我个人更倾向于ECDSA。为什么?因为嵌入式设备的存储空间和带宽都有限。ECDSA的签名只有64字节,RSA要256字节。你想想看,如果设备有10万台,每次升级省下192字节的传输量,累积起来很可观。
但要注意,ECDSA的验证计算比RSA慢。如果你的设备CPU很弱,比如只有几十MHz的MCU,那RSA可能更合适。我在一个项目里用过RSA-2048,验证一次大概要200ms,而ECDSA要400ms。这个差距在某些场景下是致命的。
避坑指南:我曾经在一个项目里用了ECDSA,但没注意随机数生成器的质量。结果签名时用了弱随机数,导致私钥被恢复。嗯,从那以后我每次都会检查RNG(随机数生成器)的熵源。
生成签名的流程:
# 使用OpenSSL生成ECDSA密钥对
openssl ecparam -genkey -name prime256v1 -out private.pem
openssl ec -in private.pem -pubout -out public.pem
# 对固件签名
openssl dgst -sha256 -sign private.pem -out firmware.sig firmware.bin
# 验证签名
openssl dgst -sha256 -verify public.pem -signature firmware.sig firmware.bin
4.4 安全启动链(Secure Boot)
签名校验做完了,是不是就安全了?不一定。因为攻击者可以绕过你的OTA流程,直接通过物理接口(比如JTAG、UART)刷入固件。或者,他可以在bootloader阶段就注入恶意代码。
这就是安全启动链要解决的问题。
安全启动链的原理,说白了就是「信任链」:
- 芯片内部有一个不可更改的ROM bootloader
- ROM bootloader验证下一级bootloader的签名
- 下一级bootloader验证OS内核的签名
- OS内核验证应用固件的签名
每一级只信任上一级。只要第一级是安全的,整个链条就是安全的。
我在设计游戏机固件时,把安全启动链分成了四层:
- ROM Bootloader:芯片出厂固化的,不可更改。只做一件事——验证SPL(Secondary Program Loader)的签名。
- SPL:初始化DDR、时钟等。验证UBoot的签名。
- UBoot:加载内核。验证内核镜像的签名。
- 内核:挂载根文件系统。验证应用固件的签名。
关键点:每一级的公钥都固化在上一级的代码里。比如ROM里存了SPL的公钥,SPL里存了UBoot的公钥。这样攻击者无法替换公钥。
嗯,这里要注意一个细节:公钥怎么存?
- 不能明文存,容易被读取
- 建议用eFuse或OTP(一次性可编程存储器)
- 或者用芯片的唯一ID做密钥派生
我曾经在一个项目里,把公钥直接编译进了bootloader。结果有人通过读Flash把公钥提取出来了。虽然公钥本身不怕泄露,但攻击者可以利用它做中间人攻击。后来我改用eFuse存储,物理上不可读,安全多了。
4.5 完整的OTA安全流程
把上面这些串起来,一个完整的OTA安全升级流程是这样的:
- 厂商生成固件,计算SHA256哈希
- 用私钥对哈希签名,得到签名文件
- 打包:固件 + 签名 + 公钥证书(可选)
- 设备下载固件包
- 设备用内置公钥验证签名
- 验证通过后,计算固件哈希,与签名中的哈希对比
- 全部通过,写入Flash
- 重启,安全启动链逐级验证
我的建议:在固件包里加一个版本号字段。签名时把版本号也签进去。这样攻击者无法回滚到旧版本(回滚攻击)。我见过有人没做这个,结果被降级攻击搞垮了整条产品线。
4.6 性能与安全的权衡
最后聊点实际的。安全是有代价的。
- 签名验证需要时间:RSA-2048约200ms,ECDSA约400ms
- 哈希计算需要时间:SHA256处理1MB固件约50ms(取决于CPU)
- 安全启动链每多一级,启动时间就多几百毫秒
对于游戏机来说,用户能接受几秒的启动时间。但如果是IoT设备,可能要求1秒内启动。这时候你就得权衡:
- 能不能只验证关键部分?比如只验证内核,不验证文件系统
- 能不能用硬件加速?很多芯片有SHA256和RSA硬件引擎
- 能不能用更快的算法?比如Ed25519(一种基于Curve25519的签名算法)
我个人习惯是:安全不能妥协,但实现方式可以优化。比如用硬件加速,或者把验证放到后台线程。记住一句话:安全是设计出来的,不是测试出来的。
最后提醒:私钥一定要保管好!我见过有人把私钥放在Git仓库里,结果被泄露。私钥泄露等于整个安全体系崩溃。建议用HSM(硬件安全模块)存储私钥,或者至少用密码保护。
好了,这一章的内容就到这。签名与安全,说白了就是「你是谁」和「你改没改」的问题。搞清楚了这两个问题,你的OTA方案就成功了一半。