4、镜像签名与验证:镜像格式设计、签名算法选择、哈希链验证流程、多级签名支持
好,咱们接着聊。上一节我们把密钥体系搭起来了,现在该聊聊怎么把这些密钥真正用起来——也就是给固件镜像签名,然后在启动时验证它。
说实话,镜像签名这块儿,我见过太多“看起来安全、实际一捅就破”的设计了。有的直接把签名区放在镜像末尾,连个校验都没有;有的用了RSA-1024,觉得够用了,结果没几年就被破解了。嗯,咱们今天就把这块儿彻底讲透。
4.1 镜像格式设计:头部、载荷、签名区
一个标准的签名镜像,我习惯把它分成三个部分:头部(Header)、载荷(Payload)、签名区(Signature)。说白了,就是“说明、数据、凭证”。
头部里放什么?我个人习惯至少包含这些字段:
- 魔数(Magic Number):用来快速识别是不是合法的镜像文件。比如0xDEADBEEF,或者你们团队自己约定的一个值。
- 版本号(Version):这个太重要了,防回滚全靠它。我建议用递增的整数,别用字符串,字符串比较容易出bug。
- 镜像长度(Image Length):整个镜像的大小,包括头部和签名区。Bootloader拿到后先校验这个长度,防止截断攻击。
- 哈希算法标识(Hash Algorithm ID):比如0x01代表SHA-256,0x02代表SHA-384。这样以后换算法也方便。
- 签名算法标识(Signature Algorithm ID):类似,标明用的是RSA还是ECDSA。
- 载荷哈希值(Payload Hash):对载荷部分算出来的哈希,签名区签的就是这个值。
载荷部分就是真正的固件数据,可能是压缩的,也可能是加密的。我建议载荷本身也加一个CRC校验,双重保险。
签名区就是签名算法输出的结果。如果是RSA-2048,那就是256字节;如果是ECDSA P-256,那就是64字节(R+S各32字节)。
这里有个细节:签名区到底签的是整个镜像还是只签哈希? 我建议只签哈希。为什么呢?因为RSA签名对数据长度有限制,而且签名整个镜像太慢了。你想想看,一个几兆的固件,每次启动都要做一次RSA签名验证,那启动时间就受不了。所以标准做法是:先对载荷算哈希,再对哈希做签名。
镜像结构示意(我常用的布局):
+------------------+
| Magic (4字节) |
| Version (4字节) |
| Image Length (4) |
| Hash Alg ID (1) |
| Sig Alg ID (1) |
| Reserved (2字节) |
| Payload Hash (32)|
+------------------+
| Payload (N字节) |
+------------------+
| Signature (M字节) |
+------------------+
4.2 签名算法选择:RSA vs ECDSA
这个选择题,我当年也纠结过。咱们直接对比一下:
| 特性 | RSA | ECDSA |
|---|---|---|
| 签名长度 | 较大(RSA-2048 = 256字节) | 较小(P-256 = 64字节) |
| 验证速度 | 较快(公钥指数小) | 较慢(需要点乘运算) |
| 签名速度 | 较慢(私钥指数大) | 较快 |
| 密钥生成 | 较慢 | 较快 |
| 安全性 | RSA-2048 ≈ 112位安全强度 | P-256 ≈ 128位安全强度 |
| 硬件支持 | 非常广泛 | 逐渐普及 |
我个人怎么选?如果MCU有硬件加速器,我倾向于ECDSA P-256。签名短、安全性高,而且密钥生成快。但如果你用的芯片比较老,没有硬件加速,那RSA-2048更稳妥——验证速度快,而且库支持非常成熟。
我曾经在一个项目里选了ECDSA,结果芯片的硬件加速器只支持RSA,ECDSA全靠软件算,启动时间多了将近两秒。后来赶紧改方案,换成了RSA。所以啊,选算法之前,一定先看看芯片的手册,看看硬件支持什么。
我的建议: 如果条件允许,优先选ECDSA P-256。签名短意味着镜像小,传输快。而且NIST已经建议2030年后淘汰RSA-2048了,咱们得有点前瞻性。
4.3 哈希链验证流程
好了,镜像格式定了,算法选了,那验证流程怎么走?我画个流程图给你看:
Bootloader启动
↓
读取镜像头部
↓
校验魔数 → 失败则停止启动
↓
检查版本号 → 低于当前版本则回滚拒绝
↓
校验镜像长度 → 防止截断
↓
读取载荷哈希值(从头部)
↓
对载荷计算哈希 → 与头部哈希比对
↓
不一致则镜像损坏,停止
↓
用公钥验证签名(对载荷哈希签名)
↓
验证通过 → 跳转到载荷执行
↓
验证失败 → 进入恢复模式
这里有个关键点:哈希链验证。什么意思呢?就是每一步的验证结果都依赖于上一步。比如,如果魔数不对,那后面的版本检查、长度校验都没必要做了。这样设计的好处是:尽早失败,节省时间。
我见过一个设计,先做签名验证,再做版本检查。结果签名验证通过了,版本检查却失败了——白白浪费了签名验证的时间。所以顺序很重要:先做轻量级的检查(魔数、版本、长度),再做重量级的检查(哈希、签名)。
注意: 版本检查一定要在签名验证之前吗?不一定。但如果你先做签名验证,那签名验证通过后,你还需要再检查版本。我建议把版本信息也放到签名范围内,这样版本号被篡改了也能被签名验证发现。但这样有个问题:版本检查失败时,你仍然需要做一次签名验证才能知道版本被篡改了。嗯,这是个权衡。
4.4 多级签名支持
最后聊聊多级签名。说白了,就是允许多个实体对同一个镜像签名。比如,开发团队签一次,QA团队再签一次,或者不同的产品线有不同的签名密钥。
多级签名怎么实现?我常用的方法有两种:
- 串联式(Chain):每个签名区依次排列,验证时按顺序验证。任何一个签名验证失败,镜像就不合法。
- 并联式(Parallel):多个签名区并列,验证时只要有一个通过就行。这种适合“多选一”的场景,比如不同的OEM用不同的密钥。
我个人更推荐串联式,因为安全强度更高。你想想看,如果并联式,只要攻破一个密钥就能启动镜像,那安全性就取决于最弱的那个密钥了。
多级签名的镜像结构大概长这样:
+------------------+
| 头部 |
+------------------+
| 载荷 |
+------------------+
| 签名区1 (开发) |
| 签名区2 (QA) |
| 签名区3 (发布) |
+------------------+
每个签名区里,除了签名值,还要包含签名者的ID和签名算法标识。这样Bootloader就知道用哪个公钥来验证。
避坑指南: 我曾经在一个项目里支持了最多8级签名,结果Bootloader的代码复杂度翻了一倍,还出了个bug——签名区索引越界。后来我学乖了,多级签名最多支持3级就够了,再多就是过度设计了。
好了,镜像签名这块儿就聊到这儿。下一节咱们聊聊怎么把公钥安全地存到芯片里——也就是信任根的建立。这个要是搞砸了,前面所有的签名验证都是白搭。