3. 固件签名实践:用 OpenSSL 搞定密钥对、签名与验证
这一节我们直接动手。光讲理论没用,你得亲手敲一遍命令,才能真正理解固件签名在干什么。我个人习惯把 OpenSSL 当作瑞士军刀——它几乎能处理所有跟密码学相关的杂活。好,我们开始。
3.1 整体流程:先看一张图
在敲命令之前,我建议你先在脑子里搭一个框架。固件签名说白了就三步:生成密钥对 → 签名固件 → 验证签名。下面这张图把整个流程串起来了,你盯着看一分钟,后面操作就不会迷路。
这张图左边是签名方,右边是验证方。中间那条箭头代表传输通道——你想想看,固件在传输过程中可能被篡改,签名就是为了防止这种情况。
3.2 生成密钥对:私钥是你的命根子
第一步,生成 RSA 密钥对。我一般用 2048 位,平衡安全性和性能。4096 位当然更安全,但嵌入式设备上验签会慢不少。
# 生成私钥(2048位 RSA)
openssl genrsa -out firmware_private.pem 2048
# 从私钥提取公钥
openssl rsa -in firmware_private.pem -pubout -out firmware_public.pem
⚠️ 私钥安全警告
私钥文件一旦泄露,攻击者就能伪造你的固件签名。我曾经见过一个团队把私钥直接放在 Git 仓库里——结果被扫描工具扒了出来,整个产品线都得紧急换密钥。记住:私钥永远不要提交到版本控制,永远不要放在可公开访问的位置。
生成之后,你可以看看这两个文件长什么样:
# 查看私钥信息
openssl rsa -in firmware_private.pem -text -noout
# 查看公钥信息
openssl rsa -pubin -in firmware_public.pem -text -noout
输出里会显示模数(modulus)、公钥指数(publicExponent)这些参数。你不需要记住每个字段的含义,但至少要知道——公钥和私钥在数学上是配对的,只有配对的私钥才能签出能被对应公钥验证的签名。
3.3 签名固件:把指纹盖上去
有了密钥对,下一步就是对固件文件签名。这里有个关键点:我们不是直接对固件文件做 RSA 签名,而是先计算固件的哈希值,再对哈希值签名。为什么?因为 RSA 签名算法对输入长度有限制,而且直接签大文件效率太低。
我个人习惯用 SHA-256 做哈希,配合 RSA 签名。命令如下:
# 对固件进行签名(输出二进制签名文件)
openssl dgst -sha256 -sign firmware_private.pem -out firmware.bin.sig firmware.bin
# 查看签名文件大小
ls -lh firmware.bin.sig
签名文件通常只有 256 字节(2048 位 RSA 签名)。你想想看,一个几兆的固件,签名只有几百字节,这就是哈希的魅力。
💡 小技巧:同时输出签名和固件
在实际项目中,我经常把签名和固件打包在一起,方便分发。可以用 cat 命令合并:
cat firmware.bin firmware.bin.sig > firmware_signed.bin
验证时先拆开再验签。当然,更规范的做法是用专门的固件打包工具(比如 mcuboot 的 imgtool),但手动操作能帮你理解底层原理。
3.4 验证签名:设备端的最后一道防线
签名完成后,验证方(比如 Bootloader)拿到固件和签名,用公钥验签。命令如下:
# 使用公钥验证签名
openssl dgst -sha256 -verify firmware_public.pem -signature firmware.bin.sig firmware.bin
如果输出 Verified OK,说明固件没有被篡改,确实是私钥持有者签发的。如果输出 Verification Failure,那就要警惕了——要么固件被改过,要么签名不匹配。
🔑 验证失败的常见原因
- 固件被修改:哪怕只改了一个字节,哈希值都会变,验签必然失败。
- 签名文件损坏:传输过程中签名文件被截断或篡改。
- 公钥不匹配:用了错误的公钥去验签——这种情况我在项目中遇到过,有人把测试环境的公钥混到了生产环境。
- 哈希算法不一致:签名时用 SHA-256,验证时却用了 SHA-1,结果肯定对不上。
3.5 完整脚本:一条龙搞定
为了方便重复操作,我写了一个小脚本。你把它保存成 sign_firmware.sh,以后每次发布固件跑一遍就行。
#!/bin/bash
# 固件签名脚本
# 用法:./sign_firmware.sh firmware.bin
FIRMWARE=$1
PRIVATE_KEY="firmware_private.pem"
PUBLIC_KEY="firmware_public.pem"
SIGNATURE="${FIRMWARE}.sig"
# 检查参数
if [ -z "$FIRMWARE" ]; then
echo "用法: $0 <firmware.bin>"
exit 1
fi
# 检查密钥是否存在,不存在则生成
if [ ! -f "$PRIVATE_KEY" ]; then
echo "[INFO] 未找到私钥,正在生成..."
openssl genrsa -out "$PRIVATE_KEY" 2048
openssl rsa -in "$PRIVATE_KEY" -pubout -out "$PUBLIC_KEY"
echo "[INFO] 密钥对已生成"
fi
# 签名
echo "[INFO] 正在签名: $FIRMWARE"
openssl dgst -sha256 -sign "$PRIVATE_KEY" -out "$SIGNATURE" "$FIRMWARE"
# 验证
echo "[INFO] 验证签名..."
openssl dgst -sha256 -verify "$PUBLIC_KEY" -signature "$SIGNATURE" "$FIRMWARE"
if [ $? -eq 0 ]; then
echo "[SUCCESS] 签名验证通过"
else
echo "[FAIL] 签名验证失败"
exit 1
fi
这个脚本会自动检查密钥是否存在,不存在就生成。每次签名完自动验签一遍,确保签名文件没问题再分发。我建议你也养成这个习惯——签名后立即验证,别等到设备反馈了才发现问题。
3.6 避坑指南:我踩过的几个坑
做固件签名这几年,我踩过不少坑。挑几个典型的跟你说说:
| 坑 | 现象 | 原因 | 解决方法 |
|---|---|---|---|
| 私钥权限 | 签名脚本被其他用户读取 | 私钥文件权限设为 644 | 设为 600,仅所有者可读写 |
| 换行符差异 | Windows 签的固件在 Linux 验签失败 | 文本模式 vs 二进制模式 | 始终用二进制模式处理固件 |
| 密钥过期 | 旧设备无法验证新固件 | 更换密钥对后未更新设备公钥 | 建立密钥轮换机制,保留旧公钥 |
| 哈希碰撞 | 理论上存在但概率极低 | SHA-256 目前安全 | 关注 NIST 建议,必要时升级到 SHA-384 |
我曾经有一次把私钥权限设成了 644,结果 CI 流水线上的其他用户能读到私钥。虽然没出安全事故,但吓得我连夜改了所有密钥。从那以后,我写了个脚本在每次构建后检查私钥权限——自动化检查比人肉记忆靠谱得多。
3.7 小结:你学会了什么
这一节我们走完了固件签名的完整流程:生成密钥对、签名、验证。你亲手敲了命令,看了输出,也知道了常见的坑在哪里。这些操作看起来简单,但它们是整个固件安全体系的基石。