4. 镜像签名与验签机制:非对称加密在固件签名中的应用,RSA/ECDSA算法选择,哈希校验流程

各位同学,今天我们聊一个硬核话题——固件签名与验签。说白了,就是怎么让你的Boot ROM相信,它加载的恢复镜像是你亲手做的,而不是别人塞进来的恶意代码。

我在做第一代IoT安全网关时,就吃过没签名的亏。当时为了赶工期,跳过了签名验证,结果产品上市三个月就被爆出固件替换漏洞。嗯,从那以后,我再也不敢在签名环节偷懒了。

4.1 为什么需要签名?

你想想看,Boot ROM是芯片上电后执行的第一段代码。它要加载恢复镜像,然后跳过去执行。如果这个镜像被人篡改过,那整个设备就沦陷了。

签名机制解决三个核心问题:

  • 完整性:镜像有没有被改过?
  • 真实性:这个镜像是不是官方发布的?
  • 抗抵赖:发布者不能事后否认自己签过这个镜像。

这三个需求,非对称加密正好都能满足。

4.2 非对称加密基础

非对称加密,也叫公钥密码体制。它有一对密钥:公钥和私钥。私钥自己藏着,公钥公开出去。

在固件签名场景里:

  • 私钥:放在安全的签名服务器上,只有发布者能用。
  • 公钥:烧死在Boot ROM里,或者用证书链验证后加载到内存。

签名过程是这样的:

  1. 用私钥对镜像的哈希值加密,生成签名。
  2. 把签名附加到镜像后面,一起发布。
  3. Boot ROM用公钥解密签名,得到哈希值。
  4. 再自己算一遍镜像的哈希值,对比一下。

如果两个哈希值一样,说明镜像没被改过,而且确实是私钥持有者签的。

核心要点:签名不是加密整个镜像,而是加密镜像的哈希值。这样既保证了安全,又不会拖慢加载速度。

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 哈希校验流程

哈希校验是整个签名验签流程的核心。我画了一张流程图,帮你理清思路。

固件签名与验签流程 固件签名与验签流程 签名端(发布者) 原始固件镜像 SHA-256哈希计算 哈希值(32字节) 私钥签名(RSA/ECDSA) 签名(64/256字节) 验签端(Boot ROM) 镜像 + 签名 SHA-256哈希计算 计算出的哈希值 公钥解密签名 解密出的哈希值 哈希值对比 一致 → 验签通过 不一致 → 验签失败

流程其实不复杂,但有几个细节要注意:

4.5 哈希算法选择

哈希算法我推荐用SHA-256。为什么?

  • SHA-1已经被证明有碰撞攻击,别用了。
  • SHA-256目前还是安全的,而且几乎所有硬件都支持。
  • SHA-512虽然更安全,但计算量大,在Boot ROM里跑起来有点慢。

注意:千万不要用MD5做固件签名哈希。MD5的碰撞攻击在2004年就被公开了。我曾经见过一个产品用MD5做签名,结果被攻击者伪造了合法镜像。嗯,那个项目后来回炉重造了。

4.6 签名流程的完整步骤

我整理了一个标准的签名流程,你可以直接拿来用:

  1. 准备镜像:编译生成固件二进制文件,比如 firmware.bin。
  2. 计算哈希:用SHA-256算镜像的哈希值。
  3. 签名:用私钥对哈希值做签名,生成签名数据。
  4. 打包:把签名附加到镜像后面,或者单独放一个签名文件。
  5. 发布:把打包后的镜像分发出去。

验签流程更简单:

  1. 接收:Boot ROM拿到镜像和签名。
  2. 算哈希:对镜像算SHA-256。
  3. 解密签名:用公钥解密签名,得到原始哈希。
  4. 对比:两个哈希值一样就通过,不一样就拒绝启动。

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紧急缺陷,我连夜改代码。