3. 固件签名工具链:OpenSSL命令行签名、Python脚本签名、HASH计算与填充
好,咱们进入第三章。这一章讲的是签名工具链,说白了就是你怎么把固件“签上名”。
我在带项目的时候发现,很多工程师对签名流程的理解停留在“跑个脚本就行”。但一旦签名格式不对、填充位置错了、或者哈希算法没对齐,安全启动就会在某个环节卡死。你想想看,板子变砖了,连个 log 都看不到,那才叫头疼。
所以这一章,我会把 OpenSSL 命令行、Python 脚本、以及 HASH 计算与填充这三个核心工具链讲透。嗯,咱们一个一个来。
3.1 OpenSSL 命令行签名:最直接的方式
OpenSSL 是嵌入式签名绕不开的工具。我个人习惯在开发初期先用命令行把流程跑通,再封装到脚本里。
3.1.1 生成密钥对
签名需要一对密钥:私钥自己藏好,公钥放到固件里或者烧进芯片的 OTP 区域。
# 生成 RSA 私钥,2048 位
openssl genrsa -out private.pem 2048
# 从私钥导出公钥
openssl rsa -in private.pem -pubout -out public.pem
这里有个坑:私钥一定要保护好。我曾经见过一个项目,私钥直接明文放在 Git 仓库里,结果被外部审计抓了个正着。嗯,从那以后我都在构建服务器上用 HSM 或者加密存储来管理私钥。
3.1.2 计算哈希并签名
签名不是直接对固件签名,而是先算哈希,再对哈希值签名。这是标准做法。
# 计算固件的 SHA-256 哈希
openssl dgst -sha256 -binary firmware.bin > firmware.hash
# 用私钥对哈希签名,输出 DER 格式
openssl pkeyutl -sign -in firmware.hash -inkey private.pem -out firmware.sig
注意这里的 -binary 参数。如果不加,OpenSSL 会输出十六进制文本,签名时就会出错。我刚开始用的时候也踩过这个坑,折腾了半天才发现是格式问题。
3.1.3 验证签名
验证是签名的逆过程,用公钥解密签名,然后比对哈希。
# 用公钥验证签名
openssl pkeyutl -verify -in firmware.hash -sigfile firmware.sig -pubin -inkey public.pem
如果输出 Signature Verified Successfully,说明签名有效。否则……嗯,你得回头检查密钥和哈希是否匹配。
3.2 Python 脚本签名:自动化与灵活性
命令行适合调试,但到了量产阶段,你肯定要用脚本。Python 的 cryptography 库是我最常用的工具。
3.2.1 安装依赖
pip install cryptography
3.2.2 签名脚本核心逻辑
下面是一个完整的签名脚本骨架。我建议你把它作为模板,根据项目需求修改。
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.backends import default_backend
def sign_firmware(firmware_path, private_key_path, output_sig_path):
# 1. 加载私钥
with open(private_key_path, "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None, # 如果有密码,在这里传入
backend=default_backend()
)
# 2. 读取固件
with open(firmware_path, "rb") as f:
firmware_data = f.read()
# 3. 计算哈希并签名
signature = private_key.sign(
firmware_data,
padding.PKCS1v15(),
hashes.SHA256()
)
# 4. 保存签名
with open(output_sig_path, "wb") as f:
f.write(signature)
print(f"签名完成,签名文件:{output_sig_path}")
这个脚本看起来简单,但有几个关键点:
- 填充方式:PKCS#1 v1.5 是嵌入式最常用的,兼容性好。PSS 更安全,但有些老芯片不支持。
- 哈希算法:SHA-256 是目前的主流。SHA-1 已经被证明不安全,别用了。
- 私钥保护:脚本里不要硬编码密码,建议从环境变量或密钥管理服务读取。
3.2.3 批量签名与固件打包
量产时,你可能需要对多个固件版本签名,或者把签名附加到固件尾部。我习惯写一个更完整的脚本:
def sign_and_pack(firmware_path, private_key_path, output_path):
# 签名
sig = sign_firmware(firmware_path, private_key_path, "temp.sig")
# 读取签名数据
with open("temp.sig", "rb") as f:
sig_data = f.read()
# 读取固件
with open(firmware_path, "rb") as f:
fw_data = f.read()
# 打包:固件 + 签名长度(4字节) + 签名数据
sig_len = len(sig_data).to_bytes(4, 'little')
packed = fw_data + sig_len + sig_data
with open(output_path, "wb") as f:
f.write(packed)
print(f"打包完成:{output_path}")
这里我把签名长度放在签名数据前面,这样 bootloader 解析时就知道从哪里读取签名。格式可以自定义,但一定要和 bootloader 约定好。
3.3 HASH 计算与填充:细节决定成败
哈希计算看起来简单,但填充方式、数据对齐、以及哈希在签名中的位置,都是容易出错的地方。
3.3.1 哈希算法的选择
| 算法 | 输出长度 | 安全性 | 推荐场景 |
|---|---|---|---|
| SHA-1 | 160 bits | 已破解,不推荐 | 仅兼容旧设备 |
| SHA-256 | 256 bits | 安全 | 通用场景,首选 |
| SHA-384 | 384 bits | 更安全 | 高安全需求 |
| SHA-512 | 512 bits | 最安全 | 性能充裕的场景 |
我个人建议:没有特殊理由,就用 SHA-256。它平衡了安全性和性能,几乎所有芯片的硬件加速单元都支持。
3.3.2 哈希计算的边界问题
计算哈希时,你要明确“对什么数据算哈希”。常见的有两种:
- 对整个固件文件:最简单,但如果有签名附加在固件尾部,需要排除签名部分。
- 对固件的特定区域:比如只对代码段和只读数据段算哈希,跳过可写区域。
我在一个项目中遇到过这样的问题:固件升级时,bootloader 把整个 flash 都算了哈希,结果把升级标志位也算进去了。每次升级后哈希都不一致,导致签名验证失败。嗯,从那以后我都在固件头部定义一个哈希范围字段,明确告诉 bootloader 该算哪一段。
3.3.3 填充方式:PKCS#1 v1.5 vs PSS
| 填充方式 | 特点 | 适用场景 |
|---|---|---|
| PKCS#1 v1.5 | 简单、兼容性好、广泛支持 | 大多数嵌入式芯片 |
| PSS | 更安全、有随机性、需要更多计算 | 高安全需求、性能充裕 |
PKCS#1 v1.5 虽然有一些理论上的攻击,但在实际嵌入式场景中,只要你的私钥没泄露,基本没问题。PSS 更安全,但有些老芯片的硬件加速单元不支持,只能用软件实现,速度会慢不少。
3.3.4 哈希在签名中的位置
签名数据里,哈希值通常放在固定偏移处。以 RSA 2048 为例,签名输出是 256 字节,其中哈希值经过填充后占据特定位置。你不需要手动操作这些,OpenSSL 和 Python 库都帮你处理好了。
但有一点要注意:签名前的哈希值必须和 bootloader 验证时计算的哈希值一致。如果 bootloader 对固件做了预处理(比如解压缩、地址重定位),那么签名时也要对同样的数据算哈希。
3.4 工具链流程图
下面这张图展示了从固件到签名包的完整流程。我建议你把它打印出来贴在工位上,调试时对照着看。
3.5 常见问题与避坑指南
最后,我总结几个我在项目中踩过的坑,希望能帮你少走弯路。
- 哈希不匹配:签名时和验证时算的不是同一段数据。检查 bootloader 的哈希范围定义。
- 填充方式不一致:签名用 PKCS#1 v1.5,但 bootloader 用 PSS 验证,或者反过来。确保两端一致。
- 密钥格式问题:OpenSSL 生成的 PEM 格式,有些芯片只接受 DER 格式。用
openssl rsa -in private.pem -outform DER -out private.der转换。 - 签名附加位置:签名是放在固件头部、尾部、还是单独文件?必须和 bootloader 约定好。我习惯放在尾部,因为头部通常有中断向量表,不能乱改。
- 字节序:签名数据中的长度字段、哈希值,都要注意大端小端。ARM 和 RISC-V 通常小端,但网络传输常用大端。