3. 固件镜像的签名验证:非对称加密(RSA、ECDSA)在固件签名中的应用

说到固件安全,签名验证是绕不开的一环。你想想看,如果设备随便拿个镜像就往里刷,那跟把家门钥匙挂在门外有什么区别?我这些年做过的项目里,因为签名没做好导致设备被篡改的案例,一只手都数不过来。

非对称加密,说白了就是一对钥匙——私钥签名,公钥验签。私钥你死死攥在手里,公钥可以公开给全世界。这个机制天然适合固件升级场景:开发者在服务器上用私钥给固件签名,设备端用预置的公钥去验证。只要私钥不泄露,谁也别想伪造你的固件。

3.1 为什么非对称加密更适合固件签名?

对称加密也能做校验,比如 HMAC。但有个致命问题——密钥必须共享。你想想,如果每台设备里都藏着同一个密钥,一旦某台设备被破解,整个产品线的密钥就全废了。我在一个 IoT 项目里就吃过这个亏,当时为了省成本用了 HMAC,结果被逆向出来,所有设备都得返厂升级。

非对称加密就不一样了:

  • 私钥:只保存在安全的签名服务器上,永远不离开
  • 公钥:烧录在每台设备的只读存储区,公开也没关系
  • 签名:用私钥对固件哈希加密,任何人都能用公钥验证

这个模型的好处是,即使设备被物理破解,攻击者拿到的也只是公钥,无法伪造签名。最多只能验证签名,但造不出新的合法固件。

核心原则:签名 ≠ 加密。签名是为了证明「谁写的」,加密是为了「不让别人看」。固件升级里我们只关心前者——确保镜像来自可信源。

3.2 RSA vs ECDSA:怎么选?

目前主流方案就两个:RSA 和 ECDSA。我两个都用过,各有各的脾气。

对比项 RSA ECDSA
密钥长度 2048 / 4096 位 256 / 384 位
签名大小 256 / 512 字节 64 / 96 字节
验签速度 较慢(指数运算) 较快(椭圆曲线点乘)
硬件支持 几乎所有 MCU 都有硬件加速 较新芯片才支持
安全性 成熟,量子计算前够用 同等安全强度下密钥更短

我个人习惯:如果芯片有硬件 ECC 引擎,优先用 ECDSA。签名小、速度快,对资源受限的嵌入式设备很友好。但如果芯片比较老,或者你不想依赖硬件加速,RSA 2048 仍然是稳妥的选择。

经验之谈:别为了省空间用 RSA 1024。我在 2019 年做过一个项目,客户坚持用 1024 位,结果第二年就被学术界攻破了 768 位的 RSA。虽然 1024 还没完全沦陷,但安全余量已经不够了。现在底线是 2048 位。

3.3 签名验证的完整流程

嗯,这里要注意,签名不是对整个固件做非对称加密——那样太慢了。实际做法是:

  1. 计算哈希:对固件镜像做 SHA-256,得到一个 32 字节的摘要
  2. 签名:用私钥对这个摘要做签名运算,生成签名数据
  3. 打包:把签名数据附加到固件镜像后面,或者单独放在一个签名文件中
  4. 验签:设备端先计算本地固件的哈希,再用公钥验证签名是否匹配

下面这张图把流程画清楚了:

固件签名与验证流程 签名端(服务器) ① 固件镜像 → SHA-256 → 摘要 H ② 私钥 SK + 摘要 H → 签名 S ③ 打包:镜像 || 签名 S || 元数据 验证端(设备) ④ 解析固件包,提取签名 S ⑤ 计算本地镜像 SHA-256 → H' ⑥ 公钥 PK + H' + S → 验证结果 固件包传输 验证结果 匹配 → 固件合法,允许升级 | 不匹配 → 拒绝升级 输出

3.4 代码示例:用 OpenSSL 做 ECDSA 签名与验证

光说不练假把式。我贴一段实际项目里用过的脚本,帮你理解签名和验签到底怎么操作。这里用 ECDSA 曲线 P-256:

# 1. 生成 ECDSA 密钥对(P-256 曲线)
openssl ecparam -genkey -name prime256v1 -out private.pem

# 2. 导出公钥
openssl ec -in private.pem -pubout -out public.pem

# 3. 计算固件哈希并签名
openssl dgst -sha256 -sign private.pem -out firmware.sig firmware.bin

# 4. 验证签名(设备端操作)
openssl dgst -sha256 -verify public.pem -signature firmware.sig firmware.bin

# 如果输出 "Verified OK",说明固件合法

实际嵌入式代码里,你不可能跑 OpenSSL 命令行。一般是用 mbedTLS 或 OpenSSL 库的 API。核心逻辑就三步:

  • mbedtls_pk_parse_public_key():加载公钥
  • mbedtls_pk_verify():传入哈希、签名、公钥,返回 0 表示验证通过
  • mbedtls_sha256_ret():计算固件哈希

曾经踩过的坑:有一次我把签名数据放在固件镜像末尾,验签时把整个镜像包括签名一起算了哈希。结果永远验不过。记住——签名不能参与哈希计算。正确的做法是:先算固件本身的哈希,再验证签名。签名数据是附加的,不是固件的一部分。

3.5 公钥的存储与防篡改

公钥虽然可以公开,但你不能让它被随便替换。如果攻击者把设备里的公钥换成自己的,那他就能用自己的私钥签名恶意固件,设备照样认。

我建议的做法:

  • 烧录在一次性可编程(OTP)区域:比如 MCU 的 eFuse,写进去就改不了
  • 使用安全元件(SE):公钥存在独立的安全芯片里,主控只能调用验证接口,拿不到原始数据
  • 代码中硬编码 + 校验:如果硬件不支持 OTP,至少把公钥的哈希也存一份,启动时自检

一个小技巧:公钥可以拆成多段分散存储,启动时再拼接。虽然不能完全防住专业攻击,但能增加逆向成本。我在一个消费类产品上用过这招,至少拖慢了破解者两周时间。

3.6 签名验证的常见陷阱

做了这么多年固件安全,我总结了几条高频翻车点:

  • 哈希算法不匹配:签名时用 SHA-256,验签时用了 SHA-1,结果永远对不上。统一用 SHA-256 就好。
  • 字节序问题:大端小端搞反了。尤其是 ECDSA 签名,R 和 S 值的顺序、字节序都必须严格一致。
  • 填充方式:RSA 签名有 PKCS#1 v1.5 和 PSS 两种填充。两边必须用同一种。我建议用 PSS,安全性更高。
  • 忽略签名格式:OpenSSL 默认输出 DER 格式,有些库要求原始 R||S 格式。转换一下就好。

一句话总结:签名验证不是玄学,是工程。把流程理清楚,把边界条件测全,你的固件升级就安全了一大半。