4、签名工具链:OpenSSL命令行工具、Python的cryptography库、HASH校验工具
做固件签名,说白了就是三样东西:算法、密钥、工具。算法我们上一章聊过了,密钥后面会细讲,这一章咱们就聚焦在工具上。
我个人习惯把工具链分成三个梯队:OpenSSL(命令行瑞士军刀)、Python cryptography(自动化脚本首选)、HASH校验工具(日常调试必备)。这三样东西,你只要玩熟了,签名联调里 90% 的问题都能自己搞定。
4.1 OpenSSL 命令行:签名界的“万能扳手”
OpenSSL 这东西,说实话,刚开始用的时候挺劝退的。参数又多又长,报错信息还经常看不懂。但没办法,它是事实标准。我在项目中遇到过好几次,客户给的签名工具包就是基于 OpenSSL 封装的,底层调的全是它的命令行。
4.1.1 生成密钥对
签名第一步,你得有把“锁”和“钥匙”。这里我推荐用 ECDSA P-256,兼顾安全和性能。
# 生成私钥(PEM格式,不加密)
openssl ecparam -genkey -name prime256v1 -out private.pem
# 从私钥导出公钥
openssl ec -in private.pem -pubout -out public.pem
# 查看密钥信息
openssl ec -in private.pem -text -noout
4.1.2 签名与验签
签名其实就一条命令,但参数顺序容易搞错。我每次都要默念一遍:私钥签名,公钥验签。
# 对固件进行签名(输出 DER 格式签名)
openssl dgst -sha256 -sign private.pem -out firmware.sig firmware.bin
# 验证签名
openssl dgst -sha256 -verify public.pem -signature firmware.sig firmware.bin
-binary | base64 转换。我刚开始联调时,就因为格式不匹配,跟 Bootloader 那边对了一天日志。
4.1.3 查看签名内容
有时候签名联调不过,你得看看签名文件里到底写了啥。用这个命令可以解析 DER 格式的签名结构:
# 解析 DER 签名(ECDSA 签名包含 r 和 s 两个值)
openssl asn1parse -in firmware.sig -inform DER
输出大概长这样:
0:d=0 hl=2 l= 69 cons: SEQUENCE
2:d=1 hl=2 l= 33 prim: INTEGER :A1B2C3D4...
37:d=1 hl=2 l= 32 prim: INTEGER :E5F6G7H8...
看到两个 INTEGER 了吗?那就是 ECDSA 签名的 r 和 s 值。如果 Bootloader 那边解析出来跟你这边不一致,八成是字节序或者 ASN.1 编解码的问题。
4.2 Python cryptography 库:自动化签名的“主力军”
命令行适合调试,但到了产线或者 CI/CD 流水线里,你还用手敲命令?那不得累死。这时候 Python 的 cryptography 库就派上用场了。
我为什么推荐这个库而不是 PyCrypto 或 M2Crypto?说白了,cryptography 的 API 设计更现代,文档更全,而且底层用的是 OpenSSL 的 C 库,性能有保障。
4.2.1 安装与导入
pip install cryptography
from cryptography.hazmat.primitives import hashes, asymmetric
from cryptography.hazmat.primitives.asymmetric import ec, utils
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
4.2.2 签名脚本示例
下面这个脚本,是我在实际项目里提炼出来的模板。你拿过去改改路径就能用。
def sign_firmware(private_key_path, firmware_path, signature_path):
# 加载私钥
with open(private_key_path, "rb") as f:
private_key = load_pem_private_key(f.read(), password=None)
# 读取固件
with open(firmware_path, "rb") as f:
firmware_data = f.read()
# 计算签名
signature = private_key.sign(
firmware_data,
ec.ECDSA(hashes.SHA256())
)
# 保存签名(DER格式)
with open(signature_path, "wb") as f:
f.write(signature)
print(f"签名完成,长度: {len(signature)} 字节")
ec.ECDSA(hashes.SHA256()) 必须跟 Bootloader 端完全一致。我见过最坑的一次,两边用的都是 ECDSA P-256 + SHA256,但一方用了 Prehashed 模式,另一方没用,结果验签死活过不去。
4.2.3 验签脚本示例
def verify_signature(public_key_path, firmware_path, signature_path):
# 加载公钥
with open(public_key_path, "rb") as f:
public_key = load_pem_public_key(f.read())
# 读取固件和签名
with open(firmware_path, "rb") as f:
firmware_data = f.read()
with open(signature_path, "rb") as f:
signature = f.read()
# 验证
try:
public_key.verify(
signature,
firmware_data,
ec.ECDSA(hashes.SHA256())
)
print("验签通过!")
return True
except Exception as e:
print(f"验签失败: {e}")
return False
4.3 HASH 校验工具:联调时的“照妖镜”
签名联调出问题时,80% 的情况是数据不一致。这时候 HASH 校验工具就是你的照妖镜,能快速定位到底是哪一段数据出了问题。
4.3.1 常用 HASH 工具对比
| 工具 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
sha256sum |
Linux 命令行 | 系统自带,零依赖 | 只能算文件,不能算内存块 |
openssl dgst -sha256 |
跨平台 | 支持多种算法,输出格式灵活 | 命令稍长 |
Python hashlib |
自动化脚本 | 可编程,支持分块计算 | 需要写代码 |
xxd / hexdump |
查看二进制内容 | 能看到原始字节 | 不能直接算 HASH |
4.3.2 联调时的典型用法
我记得有一次联调,Bootloader 那边一直报签名无效。我让那边把收到的固件和签名都发给我,然后我在本地算了一遍 HASH:
# 计算原始固件的 SHA256
sha256sum firmware.bin
# 计算签名文件的 SHA256
sha256sum firmware.sig
# 对比两边结果
# 如果固件 HASH 一致,说明传输没问题
# 如果签名 HASH 不一致,说明签名文件在传输中被篡改了
结果发现,固件的 HASH 两边一致,但签名的 HASH 对不上。再一查,原来是 Bootloader 那边把签名文件当文本文件传输了,换行符被自动转换,导致二进制数据变了。这种坑,你不拿 HASH 工具照一照,光看日志能看一天。
checksum.txt 文件,里面记录固件和签名的 HASH 值。联调时直接把这个文件发给对方,双方一对比就知道数据有没有被“污染”。
4.4 工具链协作流程
下面这张图,是我自己总结的工具链协作流程。每次做签名方案设计时,我都会拿这张图出来对照一遍,看看哪个环节的工具还没选好。
4.5 避坑指南
最后,分享几个我踩过的坑,希望能帮你省点时间。
- 密钥格式不匹配:OpenSSL 默认生成的是 PKCS#8 格式,但有些 Bootloader 只认 PKCS#1。转换命令:
openssl pkcs8 -topk8 -in private.pem -out private_pkcs1.pem -nocrypt - 签名输出格式:ECDSA 签名有两种编码方式:DER(ASN.1)和原始 r||s。联调前一定要确认 Bootloader 端用哪种。我习惯在签名脚本里加一个参数,支持两种格式切换。
- 字节序问题:有些 MCU 是大端,有些是小端。签名里的 r 和 s 值,如果字节序反了,验签必败。解决办法:在签名脚本里统一转成网络字节序(大端)。
- HASH 计算范围:签名是对整个固件文件算 HASH,还是只算固件头部?这个一定要跟 Bootloader 团队对齐。我曾经因为两边算 HASH 的范围差了 4 个字节,联调了整整两天。