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校验。流程是这样的:

  1. 读取固件文件
  2. 逐块计算SHA256
  3. 得到32字节的哈希值
  4. 与固件包中携带的哈希值对比
  5. 一致则通过,不一致则拒绝升级

我的习惯:计算哈希时,建议把固件头部的签名信息排除在外。只对纯固件数据做哈希,这样签名本身的变化不会影响哈希值。

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内核验证应用固件的签名

每一级只信任上一级。只要第一级是安全的,整个链条就是安全的。

我在设计游戏机固件时,把安全启动链分成了四层:

  1. ROM Bootloader:芯片出厂固化的,不可更改。只做一件事——验证SPL(Secondary Program Loader)的签名。
  2. SPL:初始化DDR、时钟等。验证UBoot的签名。
  3. UBoot:加载内核。验证内核镜像的签名。
  4. 内核:挂载根文件系统。验证应用固件的签名。

关键点:每一级的公钥都固化在上一级的代码里。比如ROM里存了SPL的公钥,SPL里存了UBoot的公钥。这样攻击者无法替换公钥。

嗯,这里要注意一个细节:公钥怎么存?

  • 不能明文存,容易被读取
  • 建议用eFuse或OTP(一次性可编程存储器)
  • 或者用芯片的唯一ID做密钥派生

我曾经在一个项目里,把公钥直接编译进了bootloader。结果有人通过读Flash把公钥提取出来了。虽然公钥本身不怕泄露,但攻击者可以利用它做中间人攻击。后来我改用eFuse存储,物理上不可读,安全多了。

4.5 完整的OTA安全流程

把上面这些串起来,一个完整的OTA安全升级流程是这样的:

  1. 厂商生成固件,计算SHA256哈希
  2. 用私钥对哈希签名,得到签名文件
  3. 打包:固件 + 签名 + 公钥证书(可选)
  4. 设备下载固件包
  5. 设备用内置公钥验证签名
  6. 验证通过后,计算固件哈希,与签名中的哈希对比
  7. 全部通过,写入Flash
  8. 重启,安全启动链逐级验证

我的建议:在固件包里加一个版本号字段。签名时把版本号也签进去。这样攻击者无法回滚到旧版本(回滚攻击)。我见过有人没做这个,结果被降级攻击搞垮了整条产品线。

4.6 性能与安全的权衡

最后聊点实际的。安全是有代价的。

  • 签名验证需要时间:RSA-2048约200ms,ECDSA约400ms
  • 哈希计算需要时间:SHA256处理1MB固件约50ms(取决于CPU)
  • 安全启动链每多一级,启动时间就多几百毫秒

对于游戏机来说,用户能接受几秒的启动时间。但如果是IoT设备,可能要求1秒内启动。这时候你就得权衡:

  • 能不能只验证关键部分?比如只验证内核,不验证文件系统
  • 能不能用硬件加速?很多芯片有SHA256和RSA硬件引擎
  • 能不能用更快的算法?比如Ed25519(一种基于Curve25519的签名算法)

我个人习惯是:安全不能妥协,但实现方式可以优化。比如用硬件加速,或者把验证放到后台线程。记住一句话:安全是设计出来的,不是测试出来的。

最后提醒:私钥一定要保管好!我见过有人把私钥放在Git仓库里,结果被泄露。私钥泄露等于整个安全体系崩溃。建议用HSM(硬件安全模块)存储私钥,或者至少用密码保护。

好了,这一章的内容就到这。签名与安全,说白了就是「你是谁」和「你改没改」的问题。搞清楚了这两个问题,你的OTA方案就成功了一半。