3、固件签名实践:使用OpenSSL对固件进行签名的完整流程,签名验证的自动化脚本编写

好,咱们进入实操环节。

前面讲了签名是什么、为什么需要它。现在,我带你亲手走一遍完整的固件签名流程。工具就用 OpenSSL,这东西在 Linux 和 macOS 上基本是标配,Windows 下也能用。

我个人习惯把签名流程分成三步:生成密钥对 → 计算摘要并签名 → 打包分发。验证端则是反过来:解包 → 验签 → 比对摘要。

你想想看,如果连签名这一步都做不扎实,那信任体系的第一块砖就是歪的。我在项目里见过太多团队,密钥管理一团糟,签名脚本写错了都没人发现……嗯,咱们今天就把这事彻底理清楚。

3.1 密钥对生成:别偷懒,用 RSA 4096

第一步,生成私钥和公钥。我建议直接用 RSA 4096 位,别用 2048 了。虽然 2048 目前还安全,但物联网设备生命周期长,三五年后呢?一步到位省心。

# 生成私钥,带 AES-256 加密保护
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 \
  -aes-256-cbc -out firmware_private.pem

# 从私钥导出公钥
openssl rsa -pubout -in firmware_private.pem -out firmware_public.pem
⚠️ 注意:私钥必须设置密码保护。我曾经见过有人直接把无密码私钥提交到 Git 仓库……那场面,简直是给攻击者送大礼。私钥文件权限记得设为 600,只有 root 或专门的服务账户能读。

公钥呢?它要嵌入到设备的 Bootloader 或安全固件里。设备启动时,用这把公钥去验签新固件。公钥泄露没关系,它本来就是公开的。

3.2 签名流程:先摘要,后加密

签名不是直接对固件文件做 RSA 加密——那太慢了。正确做法是:先对固件算一个哈希摘要,再用私钥加密这个摘要。

我习惯用 SHA-256,安全性和性能平衡得不错。具体命令如下:

# 假设固件文件叫 firmware.bin
# 1. 计算 SHA-256 摘要
openssl dgst -sha256 -binary firmware.bin > firmware.hash

# 2. 用私钥签名摘要,输出 DER 格式签名文件
openssl pkeyutl -sign -inkey firmware_private.pem \
  -in firmware.hash -out firmware.sig

# 3. 打包:固件 + 签名 + 元信息
# 我习惯用简单拼接,方便嵌入式端解析
cat firmware.bin firmware.sig > firmware_signed.bin
💡 小技巧:签名文件用 DER 格式,比 PEM 省空间。嵌入式设备的存储寸土寸金,能省则省。另外,我建议在打包时加入固件版本号和硬件平台 ID,防止刷错固件。

这里有个细节:pkeyutl -sign 默认用的是 PKCS#1 v1.5 填充。如果你需要更安全的 PSS 填充,加个 -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss 就行。不过要注意,Bootloader 端的验签库得支持 PSS。

3.3 验证流程:公钥解签,比对哈希

设备收到固件后,先拆包,再验签。流程如下:

# 1. 拆包:从 signed 文件中提取固件和签名
# 假设签名在末尾 512 字节(RSA 4096 签名固定长度)
dd if=firmware_signed.bin of=firmware_extracted.bin bs=1 \
  count=$(($(stat -c%s firmware_signed.bin) - 512))
dd if=firmware_signed.bin of=firmware_extracted.sig bs=1 \
  skip=$(($(stat -c%s firmware_signed.bin) - 512))

# 2. 计算提取出的固件的哈希
openssl dgst -sha256 -binary firmware_extracted.bin > firmware_extracted.hash

# 3. 用公钥解密签名,得到原始哈希
openssl pkeyutl -verify -pubin -inkey firmware_public.pem \
  -in firmware_extracted.hash -sigfile firmware_extracted.sig

# 如果返回 Verified OK,说明签名有效
✅ 关键点:验签通过 ≠ 固件绝对安全。它只证明固件是由持有私钥的人签发的,没被篡改。但私钥本身是否泄露、签名者是否可信,那是另一层信任问题。咱们这门课后面会讲证书链和 PKI,就是解决这个的。

3.4 自动化脚本:别手动敲命令了

手动敲命令容易出错。我在项目中都是写脚本,集成到 CI/CD 流水线里。下面给一个完整的签名脚本示例,你拿去改改就能用。

3.4.1 签名脚本:sign_firmware.sh

#!/bin/bash
# 用法:./sign_firmware.sh firmware.bin

set -euo pipefail

FIRMWARE="$1"
PRIVATE_KEY="firmware_private.pem"
SIGNATURE="${FIRMWARE}.sig"
OUTPUT="${FIRMWARE%.*}_signed.bin"

# 检查文件是否存在
if [ ! -f "$FIRMWARE" ]; then
    echo "❌ 固件文件不存在: $FIRMWARE"
    exit 1
fi

if [ ! -f "$PRIVATE_KEY" ]; then
    echo "❌ 私钥文件不存在: $PRIVATE_KEY"
    exit 1
fi

echo "🔑 正在对 $FIRMWARE 进行签名..."

# 计算哈希并签名
openssl dgst -sha256 -binary "$FIRMWARE" | \
  openssl pkeyutl -sign -inkey "$PRIVATE_KEY" -out "$SIGNATURE"

# 打包
cat "$FIRMWARE" "$SIGNATURE" > "$OUTPUT"

echo "✅ 签名完成!输出文件: $OUTPUT"
echo "   签名文件: $SIGNATURE"

3.4.2 验证脚本:verify_firmware.sh

#!/bin/bash
# 用法:./verify_firmware.sh firmware_signed.bin

set -euo pipefail

SIGNED_FILE="$1"
PUBLIC_KEY="firmware_public.pem"
SIG_SIZE=512  # RSA 4096 签名固定 512 字节

# 获取文件总大小
TOTAL_SIZE=$(stat -c%s "$SIGNED_FILE")
FIRMWARE_SIZE=$((TOTAL_SIZE - SIG_SIZE))

# 提取固件和签名
FIRMWARE_EXTRACTED="${SIGNED_FILE%.*}_extracted.bin"
SIGNATURE_EXTRACTED="${SIGNED_FILE%.*}_extracted.sig"

dd if="$SIGNED_FILE" of="$FIRMWARE_EXTRACTED" bs=1 count=$FIRMWARE_SIZE 2>/dev/null
dd if="$SIGNED_FILE" of="$SIGNATURE_EXTRACTED" bs=1 skip=$FIRMWARE_SIZE 2>/dev/null

# 验签
if openssl pkeyutl -verify -pubin -inkey "$PUBLIC_KEY" \
  -in <(openssl dgst -sha256 -binary "$FIRMWARE_EXTRACTED") \
  -sigfile "$SIGNATURE_EXTRACTED" 2>/dev/null; then
    echo "✅ 签名验证通过!固件未被篡改。"
    rm -f "$FIRMWARE_EXTRACTED" "$SIGNATURE_EXTRACTED"
    exit 0
else
    echo "❌ 签名验证失败!固件可能被篡改或签名不匹配。"
    rm -f "$FIRMWARE_EXTRACTED" "$SIGNATURE_EXTRACTED"
    exit 1
fi
💡 集成建议:把签名脚本放到 Jenkins/GitLab CI 的构建步骤里。每次构建固件后自动签名,签名后的文件才允许发布。公钥要硬编码在 Bootloader 中,或者通过安全元件(SE)存储。我见过有人把公钥放在文件系统里……那跟没锁门有什么区别?

3.5 完整流程的 SVG 示意图

下面这张图把整个签名-验证流程串起来了。你对照着看,会更清楚。

固件签名与验证流程 固件签名与验证完整流程 签名端(开发/构建服务器) 固件 firmware.bin SHA-256 摘要计算 私钥签名(RSA 4096) 打包:固件 + 签名 网络 传输 验证端(物联网设备) 接收 signed 固件 拆包:分离固件与签名 计算固件哈希 公钥解密签名 比对两个哈希值 ✅ 一致 → 固件可信,允许更新

3.6 避坑指南:我踩过的几个坑

最后,分享几个实战中容易翻车的地方:

坑 后果 解决办法
私钥无密码保护 泄露后攻击者可签发任意固件 用 AES-256-CBC 加密私钥,密码存密码管理器
签名算法不匹配 设备验签永远失败 签名和验签用同一套参数,写死在脚本里
公钥未防篡改 攻击者可替换公钥,绕过验签 公钥烧录在一次性写入的 OTP 区域或安全元件中
签名文件与固件分离 传输过程中容易丢失或错配 打包在一起,设备端按固定偏移量解析
⚠️ 我曾经踩过一个大坑:有一次,我把签名脚本里的哈希算法从 SHA-256 改成了 SHA-384,但忘了更新 Bootloader 里的验签逻辑。结果所有新固件刷进去都报验签失败,产线停了两小时……从那以后,我每次改签名参数都会在验证脚本里加一个自检步骤:先用已知正确的固件跑一遍验签,通过再上线。

嗯,固件签名这块,说白了就是「用私钥盖章,用公钥验章」。流程不复杂,但每一步都不能马虎。你把这些脚本和流程搭好,后面做 OTA 更新时就能省很多心。