4、固件签名实战:使用OpenSSL对固件进行签名、验证签名流程、签名算法选择(RSA/ECDSA)

好,咱们进入实战环节。前面聊了那么多证书和PKI的理论,现在该动手了。这一章我会带着你,用OpenSSL这个工具,把固件签名的完整流程走一遍。从生成密钥对,到签名,再到验证,每一步我都会讲清楚。

我个人习惯在生产环境中使用OpenSSL 1.1.1以上版本,因为对椭圆曲线(ECDSA)的支持更完善。你如果还在用老版本,建议先升级一下。

4.1 签名算法选型:RSA还是ECDSA?

选算法是第一步。很多工程师上来就问「哪个更安全?」。其实到了256位密钥长度这个级别,两者安全性都够用。关键看你的硬件平台和业务场景。

对比维度 RSA (2048位) ECDSA (P-256)
签名长度 256字节 64字节
签名速度 较慢 快
验证速度 快 较快
密钥生成速度 慢 极快
硬件加速支持 广泛 逐渐普及
固件体积影响 签名占用空间大 签名占用空间小

我的建议:

  • 如果MCU主频低、Flash紧张,优先选ECDSA P-256。签名短,省空间。
  • 如果芯片内置了RSA硬件加速引擎,或者你的Bootloader只支持RSA验签,那就用RSA 2048。
  • 千万别用RSA 1024,已经不安全了。也别用ECDSA P-192,强度不够。

我在一个IoT网关项目里遇到过这种情况:芯片的Bootloader只硬编码了RSA验签逻辑,但OTA升级包太大,256字节的签名占了不少带宽。后来我们跟芯片原厂沟通,在新版Bootloader里加入了ECDSA支持,升级包体积一下就降下来了。

4.2 生成密钥对

选好算法,咱们就开干。先分别演示RSA和ECDSA的密钥生成命令。

4.2.1 生成RSA私钥

# 生成2048位RSA私钥,使用AES-256加密保护
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
    -aes-256-cbc -out private_rsa.pem

# 从私钥导出公钥
openssl pkey -in private_rsa.pem -pubout -out public_rsa.pem

这里有个细节:-aes-256-cbc参数会让OpenSSL提示你输入密码。这个密码用来加密私钥文件。嗯,这里要注意,密码一定要妥善保管。我见过有人把密码写在代码注释里,然后整个仓库被拖库……

4.2.2 生成ECDSA私钥

# 生成P-256(prime256v1)椭圆曲线私钥
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 \
    -aes-256-cbc -out private_ecdsa.pem

# 导出公钥
openssl pkey -in private_ecdsa.pem -pubout -out public_ecdsa.pem

小技巧:你可以用 openssl ecparam -list_curves 查看所有支持的椭圆曲线。我个人只推荐 prime256v1(即secp256r1)和 secp384r1。别的曲线要么兼容性差,要么安全性存疑。

4.3 固件签名流程

签名不是简单地对整个固件文件做加密。标准做法是:先计算固件的哈希值,再对这个哈希值做签名。这样做的好处是,签名长度固定,而且验签时不需要把整个固件加载到内存。

下面这张图展示了完整的签名和验证流程:

固件签名与验证流程 签名方(开发环境) 固件镜像 私钥 ↓ 计算哈希 → 签名哈希值 固件 + 签名 + 证书 分发 验证方(Bootloader) 固件 + 签名 公钥 ↓ 计算哈希 → 验签 验证通过 / 失败 关键步骤说明 1. 签名方:SHA-256(固件) → 私钥签名 → 得到签名值 2. 打包:固件 + 签名值 + 签名证书(含公钥) 3. 验证方:SHA-256(固件) → 公钥验签 → 比对结果 4. 验签通过才允许Bootloader加载固件 5. 验签失败则拒绝启动或回滚

4.3.1 使用RSA签名

# 第一步:计算固件哈希
openssl dgst -sha256 -binary firmware.bin > firmware.hash

# 第二步:对哈希值做RSA签名
openssl pkeyutl -sign -inkey private_rsa.pem \
    -in firmware.hash -out firmware.sig

# 或者一步到位(推荐):
openssl dgst -sha256 -sign private_rsa.pem \
    -out firmware.sig firmware.bin

第二种方式更简洁,OpenSSL内部会先算哈希再签名。我建议你用这种,少一步中间文件操作,也少一个出错的可能。

4.3.2 使用ECDSA签名

# ECDSA签名(一步到位)
openssl dgst -sha256 -sign private_ecdsa.pem \
    -out firmware.sig firmware.bin

命令和RSA一模一样,只是私钥文件不同。OpenSSL会根据私钥类型自动选择签名算法。这一点做得挺方便的。

注意:ECDSA签名是随机化的。同样的固件、同样的私钥,每次签名得到的签名值都不一样。这是正常现象,不是bug。RSA签名是确定性的,同样的输入永远得到同样的输出。

4.4 验证签名流程

验证签名通常在Bootloader或者安全启动代码中完成。但开发阶段,我们先用OpenSSL模拟验证,确保签名过程没问题。

4.4.1 RSA验签

# 使用公钥验证签名
openssl dgst -sha256 -verify public_rsa.pem \
    -signature firmware.sig firmware.bin

如果输出 Verified OK,说明验签通过。如果输出 Verification Failure,那就得排查了。

4.4.2 ECDSA验签

# 使用公钥验证ECDSA签名
openssl dgst -sha256 -verify public_ecdsa.pem \
    -signature firmware.sig firmware.bin

命令完全一样。这就是OpenSSL设计好的地方——上层接口统一,底层算法隔离。

4.5 避坑指南

我在几个项目里踩过一些坑,分享给你,希望你不用再走一遍。

  • 哈希算法不一致:签名时用SHA-256,验签时用SHA-1,结果肯定失败。我曾经在脚本里写死了SHA-1,后来升级固件签名改成了SHA-256,但验签脚本忘了改……排查了半天。
  • 公钥搞混:开发环境有多套密钥对,签名用了A套的私钥,验签却用了B套的公钥。这种错误很隐蔽,因为密钥长得都差不多。
  • 签名格式问题:OpenSSL默认输出DER格式的签名。有些Bootloader要求原始格式(r||s)。你需要用 -hex 或者自己解析DER结构。
  • 文件换行符:在Windows上开发,Linux上构建,固件文件可能因为换行符不同导致哈希值不一致。建议统一在Linux环境下做签名。

我的习惯:在CI/CD流水线中,每次构建都会自动执行一次「签名→验签」的自检。如果自检失败,构建直接报红。这样能第一时间发现密钥配置错误或者脚本问题。

4.6 完整脚本示例

下面是一个完整的RSA签名和验证脚本,你可以直接拿去用。我把它写成了函数形式,方便集成到你的构建系统里。

#!/bin/bash
# sign_firmware.sh - 固件签名脚本

set -e

SIGN_KEY="private_rsa.pem"
VERIFY_KEY="public_rsa.pem"
FIRMWARE="firmware.bin"
SIGNATURE="firmware.sig"

# 签名
echo "=== 签名固件 ==="
openssl dgst -sha256 -sign "$SIGN_KEY" -out "$SIGNATURE" "$FIRMWARE"
echo "签名完成: $SIGNATURE"

# 自检
echo "=== 自检验证 ==="
openssl dgst -sha256 -verify "$VERIFY_KEY" -signature "$SIGNATURE" "$FIRMWARE"

if [ $? -eq 0 ]; then
    echo "✓ 自检通过"
else
    echo "✗ 自检失败!请检查密钥配置"
    exit 1
fi

这个脚本我用了好几年,基本没出过问题。你只需要把密钥文件和固件路径换成自己的就行。