3、固件签名工具链:OpenSSL命令行签名、Python脚本签名、HASH计算与校验
说到固件签名,很多刚入行的朋友第一反应就是「拿个工具跑一下」。但实际项目里,签名工具链的选型和配置,往往决定了整个可信启动流程的成败。我个人习惯把工具链分成三层:底层密码库、命令行工具、自动化脚本。今天我们就从这三层入手,把 OpenSSL 和 Python 这两把刀磨快。
3.1 核心概念:签名到底在做什么?
先理清一个基本问题:固件签名 ≠ 加密。签名是「用私钥对固件的 HASH 值做非对称运算」,验证方用公钥还原 HASH 并比对。说白了,签名不隐藏固件内容,只保证固件没被篡改。
整个流程可以拆成三步:
- 计算固件的 HASH(摘要)
- 用私钥对 HASH 签名,生成签名文件
- 将签名附加到固件尾部,或单独存放
验证时反过来:提取签名中的 HASH,重新计算固件 HASH,两者一致则通过。
关键点:签名算法通常使用 RSA 或 ECDSA。RSA 兼容性好,但密钥长度大(2048/4096位);ECDSA 签名短、性能高,适合资源受限的嵌入式环境。我建议新项目优先考虑 ECDSA P-256。
3.2 OpenSSL 命令行签名实战
OpenSSL 是嵌入式签名绕不开的工具。虽然它参数多、文档晦涩,但掌握几个核心命令就够了。
3.2.1 生成密钥对
先准备一对 RSA 2048 密钥:
# 生成私钥
openssl genrsa -out private.pem 2048
# 从私钥提取公钥
openssl rsa -in private.pem -pubout -out public.pem
如果是 ECDSA P-256:
# 生成 ECDSA 私钥
openssl ecparam -genkey -name prime256v1 -out ecdsa_private.pem
# 提取公钥
openssl ec -in ecdsa_private.pem -pubout -out ecdsa_public.pem
我的习惯:私钥文件权限一定要设为 600,且不要放在代码仓库里。我曾经见过同事把私钥传到 Git 上,结果整个项目组都得换密钥——那场面,嗯,很尴尬。
3.2.2 计算 HASH 并签名
假设固件文件是 firmware.bin,我们先用 SHA-256 算 HASH,再用私钥签名:
# 计算 HASH(输出十六进制)
openssl dgst -sha256 firmware.bin
# 计算 HASH 并签名,输出 DER 格式签名文件
openssl dgst -sha256 -sign private.pem -out firmware.sig firmware.bin
这里有个细节:dgst -sign 会自动先算 HASH 再签名,一步到位。但有些 bootloader 要求单独传入 HASH 值,那就需要分两步:
# 第一步:计算 HASH 并保存
openssl dgst -sha256 -binary firmware.bin > firmware.hash
# 第二步:对 HASH 文件签名
openssl pkeyutl -sign -inkey private.pem -in firmware.hash -out firmware.sig
3.2.3 验证签名
验证时用公钥:
openssl dgst -sha256 -verify public.pem -signature firmware.sig firmware.bin
输出 Verified OK 表示通过。如果失败,检查密钥是否匹配、固件是否被修改。
避坑指南:我曾经遇到一个 case,签名验证总失败,查了两天才发现是 OpenSSL 版本差异导致的签名格式问题。1.0.x 和 1.1.x 的默认摘要算法不同。建议统一使用 -sha256 显式指定,不要依赖默认值。
3.3 Python 脚本签名:自动化与集成
命令行适合调试,但生产环境需要自动化。Python 的 cryptography 库是首选,它比 pycrypto 更安全、更现代。
3.3.1 安装依赖
pip install cryptography
3.3.2 完整的签名脚本
下面是一个生产级别的签名脚本,支持 RSA 和 ECDSA:
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa, ec
from cryptography.hazmat.backends import default_backend
import sys
def sign_firmware(firmware_path, private_key_path, sig_path, algorithm='rsa'):
# 1. 读取固件
with open(firmware_path, 'rb') as f:
firmware_data = f.read()
# 2. 加载私钥
with open(private_key_path, 'rb') as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
# 3. 计算 HASH 并签名
if algorithm == 'rsa':
signature = private_key.sign(
firmware_data,
padding.PKCS1v15(),
hashes.SHA256()
)
elif algorithm == 'ecdsa':
signature = private_key.sign(
firmware_data,
ec.ECDSA(hashes.SHA256())
)
else:
raise ValueError('Unsupported algorithm')
# 4. 保存签名
with open(sig_path, 'wb') as f:
f.write(signature)
print(f'Signature saved to {sig_path}')
print(f'Signature length: {len(signature)} bytes')
if __name__ == '__main__':
sign_firmware(sys.argv[1], sys.argv[2], sys.argv[3])
3.3.3 HASH 计算与校验函数
单独提取 HASH 计算逻辑,方便集成到 CI/CD 流程:
import hashlib
def compute_firmware_hash(file_path):
"""计算固件 SHA-256 HASH"""
sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
# 分块读取,避免大固件占用过多内存
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return sha256.hexdigest()
def verify_firmware_hash(file_path, expected_hash):
"""校验固件 HASH"""
actual_hash = compute_firmware_hash(file_path)
if actual_hash == expected_hash:
print('HASH verification PASSED')
return True
else:
print(f'HASH mismatch: expected {expected_hash}, got {actual_hash}')
return False
个人经验:分块读取这个细节很重要。有一次客户固件有 64MB,一次性读入内存直接 OOM 了。后来改成 4KB 分块,问题解决。嵌入式固件虽然通常不大,但养成好习惯总没错。
3.4 签名格式与固件打包
签名完成后,需要把签名附加到固件中。常见格式有两种:
| 格式 | 结构 | 适用场景 |
|---|---|---|
| 尾部附加 | [固件数据] + [签名长度(4字节)] + [签名数据] |
Bootloader 从固定偏移读取签名 |
| 头部嵌入 | [签名头] + [固件数据] |
U-Boot、TF-A 等标准格式 |
尾部附加的 Python 实现:
def append_signature(firmware_path, sig_path, output_path):
with open(firmware_path, 'rb') as f:
fw_data = f.read()
with open(sig_path, 'rb') as f:
sig_data = f.read()
# 签名长度用小端序 4 字节
sig_len = len(sig_data).to_bytes(4, 'little')
with open(output_path, 'wb') as f:
f.write(fw_data)
f.write(sig_len)
f.write(sig_data)
print(f'Output firmware size: {len(fw_data) + 4 + len(sig_data)} bytes')
3.5 工具链对比与选型建议
| 工具 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| OpenSSL 命令行 | 通用、调试方便 | 参数复杂、不易集成 | 原型验证、手动测试 |
| Python + cryptography | 可编程、易集成 CI | 依赖库版本 | 生产环境、自动化流水线 |
| 硬件 HSM | 私钥不出设备、合规 | 成本高、配置复杂 | 高安全等级产品 |
我的建议:开发阶段用 OpenSSL 命令行快速验证,CI/CD 环境用 Python 脚本自动化。如果产品需要通过 FIPS 或国密认证,必须上硬件安全模块(HSM)。
3.6 本章知识体系
下面这张图总结了固件签名工具链的核心流程和工具关系:
3.7 常见问题与避坑
- 签名长度不一致:RSA 2048 签名固定 256 字节,ECDSA P-256 签名 64-72 字节(DER 编码)。解析时不要硬编码长度。
- 字节序问题:ARM 平台通常用小端序,x86 也是小端。但有些 bootloader 要求大端,签名打包前确认目标平台。
- HASH 算法不匹配:签名时用 SHA-256,验证时也必须用 SHA-256。我见过有人签名用 SHA-1,验证用 SHA-256,折腾半天才发现。
- 密钥管理:私钥绝对不能出现在固件或代码中。生产环境建议用 HSM 或密钥管理服务(KMS)。
重要提醒:签名工具链的测试不能只在开发环境跑。一定要在目标硬件上做完整的签名-烧录-启动验证。我曾经在 QEMU 上测试一切正常,换到真机就翻车——原因是真机的 bootloader 对签名格式有额外要求。