4. 镜像签名与验签机制:非对称加密在固件签名中的应用,RSA/ECDSA算法选择,哈希校验流程
各位同学,今天我们聊一个硬核话题——固件签名与验签。说白了,就是怎么让你的Boot ROM相信,它加载的恢复镜像是你亲手做的,而不是别人塞进来的恶意代码。
我在做第一代IoT安全网关时,就吃过没签名的亏。当时为了赶工期,跳过了签名验证,结果产品上市三个月就被爆出固件替换漏洞。嗯,从那以后,我再也不敢在签名环节偷懒了。
4.1 为什么需要签名?
你想想看,Boot ROM是芯片上电后执行的第一段代码。它要加载恢复镜像,然后跳过去执行。如果这个镜像被人篡改过,那整个设备就沦陷了。
签名机制解决三个核心问题:
- 完整性:镜像有没有被改过?
- 真实性:这个镜像是不是官方发布的?
- 抗抵赖:发布者不能事后否认自己签过这个镜像。
这三个需求,非对称加密正好都能满足。
4.2 非对称加密基础
非对称加密,也叫公钥密码体制。它有一对密钥:公钥和私钥。私钥自己藏着,公钥公开出去。
在固件签名场景里:
- 私钥:放在安全的签名服务器上,只有发布者能用。
- 公钥:烧死在Boot ROM里,或者用证书链验证后加载到内存。
签名过程是这样的:
- 用私钥对镜像的哈希值加密,生成签名。
- 把签名附加到镜像后面,一起发布。
- Boot ROM用公钥解密签名,得到哈希值。
- 再自己算一遍镜像的哈希值,对比一下。
如果两个哈希值一样,说明镜像没被改过,而且确实是私钥持有者签的。
核心要点:签名不是加密整个镜像,而是加密镜像的哈希值。这样既保证了安全,又不会拖慢加载速度。
4.3 RSA vs ECDSA:怎么选?
现在主流的签名算法就两个:RSA和ECDSA。我两个都用过,各有各的脾气。
| 对比维度 | RSA | ECDSA |
|---|---|---|
| 密钥长度 | 2048/4096位 | 256/384位 |
| 签名速度 | 慢(私钥操作) | 快 |
| 验签速度 | 快(公钥操作) | 较快 |
| 签名大小 | 与密钥长度相同(256/512字节) | 固定64/96字节 |
| 硬件支持 | 几乎所有芯片都支持 | 较新芯片才支持 |
| 安全性 | 成熟,但量子计算威胁大 | 同等安全强度下密钥更短 |
我个人习惯这样选:
- 如果芯片有硬件RSA加速器,用RSA-2048。稳,而且验签快。
- 如果芯片资源紧张,比如只有几十KB的Boot ROM,用ECDSA P-256。签名小,省空间。
- 如果芯片支持硬件ECDSA,那果断上ECDSA。性能更好。
经验之谈:我曾经在一个Cortex-M0的芯片上做Boot ROM,Flash只有32KB。RSA-2048的签名占256字节,加上公钥和代码,差点塞不下。后来换成ECDSA P-256,签名只有64字节,空间问题一下就解决了。
4.4 哈希校验流程
哈希校验是整个签名验签流程的核心。我画了一张流程图,帮你理清思路。
流程其实不复杂,但有几个细节要注意:
4.5 哈希算法选择
哈希算法我推荐用SHA-256。为什么?
- SHA-1已经被证明有碰撞攻击,别用了。
- SHA-256目前还是安全的,而且几乎所有硬件都支持。
- SHA-512虽然更安全,但计算量大,在Boot ROM里跑起来有点慢。
注意:千万不要用MD5做固件签名哈希。MD5的碰撞攻击在2004年就被公开了。我曾经见过一个产品用MD5做签名,结果被攻击者伪造了合法镜像。嗯,那个项目后来回炉重造了。
4.6 签名流程的完整步骤
我整理了一个标准的签名流程,你可以直接拿来用:
- 准备镜像:编译生成固件二进制文件,比如 firmware.bin。
- 计算哈希:用SHA-256算镜像的哈希值。
- 签名:用私钥对哈希值做签名,生成签名数据。
- 打包:把签名附加到镜像后面,或者单独放一个签名文件。
- 发布:把打包后的镜像分发出去。
验签流程更简单:
- 接收:Boot ROM拿到镜像和签名。
- 算哈希:对镜像算SHA-256。
- 解密签名:用公钥解密签名,得到原始哈希。
- 对比:两个哈希值一样就通过,不一样就拒绝启动。
4.7 代码示例:签名与验签
下面是一个用OpenSSL做RSA签名的例子。我在项目中经常这么用:
# 生成RSA私钥
openssl genrsa -out private.pem 2048
# 提取公钥
openssl rsa -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
ECDSA的用法类似,只是生成密钥时指定椭圆曲线:
# 生成ECDSA私钥(P-256曲线)
openssl ecparam -genkey -name prime256v1 -out private_ec.pem
# 提取公钥
openssl ec -in private_ec.pem -pubout -out public_ec.pem
# 签名
openssl dgst -sha256 -sign private_ec.pem -out firmware_ec.sig firmware.bin
# 验签
openssl dgst -sha256 -verify public_ec.pem -signature firmware_ec.sig firmware.bin
小技巧:在Boot ROM里验签时,公钥最好用硬件PUF(物理不可克隆函数)保护,或者至少用OTP(一次性可编程)存储器烧死。我曾经见过一个产品把公钥放在Flash里,结果攻击者直接改了公钥,用自己的私钥签名了恶意镜像。嗯,这个坑我踩过,你们别踩。
4.8 避坑指南
最后,分享几个我踩过的坑:
- 签名大小要固定:RSA-2048签名永远是256字节,ECDSA P-256永远是64字节。不要用变长签名,Boot ROM不好处理。
- 哈希算法要硬编码:不要在镜像里指定用哪种哈希算法,攻击者可以改成弱哈希。直接在Boot ROM里写死SHA-256。
- 公钥要防篡改:公钥必须放在只读区域,或者用硬件安全机制保护。
- 验签失败要处理:不要只是打印个日志就继续启动。验签失败应该直接死循环或者进入恢复模式。
我曾经在一个项目里,验签失败后只是打了个警告日志,然后继续启动了。结果测试人员用损坏的镜像刷机,设备居然正常跑起来了。嗯,那个bug被列为P0紧急缺陷,我连夜改代码。