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
公钥呢?它要嵌入到设备的 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
这里有个细节: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,说明签名有效
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
3.5 完整流程的 SVG 示意图
下面这张图把整个签名-验证流程串起来了。你对照着看,会更清楚。
3.6 避坑指南:我踩过的几个坑
最后,分享几个实战中容易翻车的地方:
| 坑 | 后果 | 解决办法 |
|---|---|---|
| 私钥无密码保护 | 泄露后攻击者可签发任意固件 | 用 AES-256-CBC 加密私钥,密码存密码管理器 |
| 签名算法不匹配 | 设备验签永远失败 | 签名和验签用同一套参数,写死在脚本里 |
| 公钥未防篡改 | 攻击者可替换公钥,绕过验签 | 公钥烧录在一次性写入的 OTP 区域或安全元件中 |
| 签名文件与固件分离 | 传输过程中容易丢失或错配 | 打包在一起,设备端按固定偏移量解析 |
嗯,固件签名这块,说白了就是「用私钥盖章,用公钥验章」。流程不复杂,但每一步都不能马虎。你把这些脚本和流程搭好,后面做 OTA 更新时就能省很多心。