3. 固件签名工具链:OpenSSL命令行工具、Python cryptography库、签名脚本编写
好,咱们进入实操环节。前面讲了签名验证的理论,现在得真刀真枪地干了。这一章,我带你搭建一套完整的固件签名工具链。
说白了,工具链就三样东西:OpenSSL(命令行老炮)、Python cryptography(脚本利器)、还有你自己写的签名脚本。这三者配合好了,签名流程就能自动化跑起来。
核心思路:私钥签名,公钥验签。工具链的任务就是生成密钥对、计算哈希、执行签名、打包输出。每一步都要可追溯、可复现。
3.1 OpenSSL 命令行工具
OpenSSL 是嵌入式安全领域的瑞士军刀。我个人习惯,不管项目用啥语言,都会先拿 OpenSSL 把流程跑通一遍。为什么?因为它稳定、透明,出了问题好排查。
3.1.1 生成 RSA 密钥对
先来生成一对 RSA 2048 位的密钥。2048 位是目前嵌入式领域的底线,别用 1024 了,不安全。
# 生成私钥(PKCS#8 格式)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
-out private_key.pem
# 从私钥导出公钥
openssl rsa -pubout -in private_key.pem -out public_key.pem
# 查看密钥信息
openssl rsa -text -in private_key.pem -noout
嗯,这里要注意:genpkey 比 genrsa 更推荐,因为它支持更多算法。我在一个 IoT 项目里就吃过亏,用 genrsa 生成的密钥在某个老版本 OpenSSL 上解析失败,换成 genpkey 就没事了。
3.1.2 计算哈希并签名
签名不是直接签固件文件,而是签它的哈希值。这是行业标准做法。
# 计算固件哈希(SHA-256)
openssl dgst -sha256 -binary firmware.bin > firmware.hash
# 用私钥签名哈希
openssl pkeyutl -sign -inkey private_key.pem \
-in firmware.hash -out firmware.sig
# 或者一步到位(推荐)
openssl dgst -sha256 -sign private_key.pem \
-out firmware.sig firmware.bin
一步到位的做法更简洁,内部也是先哈希再签名。我建议你用这种方式,少一个中间文件就少一个出错机会。
3.1.3 验证签名
# 用公钥验证签名
openssl dgst -sha256 -verify public_key.pem \
-signature firmware.sig firmware.bin
返回 Verified OK 就说明签名有效。如果返回 Verification Failure,别慌,先检查公钥是否匹配,再检查固件是否被修改过。我曾经调试过一整天,最后发现是公钥文件多了一个换行符……
小技巧:OpenSSL 验证命令返回码是 0(成功)或 1(失败),可以在脚本里用 $? 判断。别傻等命令行输出。
3.2 Python cryptography 库
OpenSSL 命令行适合手动测试和调试,但到了自动化流水线里,还是得用 Python。Python 的 cryptography 库是业界标准,API 设计得比 OpenSSL 的 C 接口友好太多了。
3.2.1 安装与导入
pip install cryptography
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
3.2.2 生成密钥对
# 生成私钥
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# 导出 PEM 格式
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
# 导出公钥
public_key = private_key.public_key()
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
注意 NoEncryption() 表示私钥不加密。生产环境里,私钥必须加密存储,这里为了演示先简化。
3.2.3 签名与验证
def sign_firmware(firmware_data: bytes, private_key) -> bytes:
"""对固件数据进行签名"""
signature = private_key.sign(
firmware_data,
padding.PKCS1v15(),
hashes.SHA256()
)
return signature
def verify_signature(firmware_data: bytes, signature: bytes, public_key) -> bool:
"""验证固件签名"""
try:
public_key.verify(
signature,
firmware_data,
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception:
return False
避坑指南:sign() 方法内部会自动计算哈希,你传原始固件数据就行。千万别自己先哈希一遍再传,那样就成了「签哈希的哈希」,验签会失败。我曾经在代码审查时看到过这种错误,改起来倒不难,但要是流片了才改……
3.3 签名脚本编写
好了,现在把上面的东西串起来,写一个完整的签名脚本。这个脚本我用了好几年,基本结构没变过,只是根据项目需求微调。
3.3.1 脚本结构
#!/usr/bin/env python3
"""
固件签名工具 v2.1
用法: python sign_firmware.py --input firmware.bin --key private_key.pem --output firmware_signed.bin
"""
import argparse
import struct
import hashlib
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
def load_private_key(key_path: str):
"""加载 PEM 格式私钥"""
with open(key_path, 'rb') as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
return private_key
def sign_and_pack(firmware_path: str, key_path: str, output_path: str):
"""签名并打包固件"""
# 1. 读取固件
with open(firmware_path, 'rb') as f:
firmware_data = f.read()
# 2. 加载私钥
private_key = load_private_key(key_path)
# 3. 计算签名
signature = private_key.sign(
firmware_data,
padding.PKCS1v15(),
hashes.SHA256()
)
# 4. 打包:签名长度(4字节) + 签名数据 + 固件数据
sig_len = len(signature)
output_data = struct.pack('<I', sig_len) + signature + firmware_data
# 5. 写入输出文件
with open(output_path, 'wb') as f:
f.write(output_data)
print(f'[OK] 签名完成,输出文件: {output_path}')
print(f' 固件大小: {len(firmware_data)} 字节')
print(f' 签名大小: {sig_len} 字节')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='固件签名工具')
parser.add_argument('--input', required=True, help='输入固件文件')
parser.add_argument('--key', required=True, help='私钥文件(PEM)')
parser.add_argument('--output', required=True, help='输出签名固件')
args = parser.parse_args()
sign_and_pack(args.input, args.key, args.output)
3.3.2 打包格式说明
打包格式决定了 BootROM 怎么解析。我用的格式很简单:签名长度 + 签名 + 固件。BootROM 先读 4 字节得到签名长度,再读签名,最后读固件进行验证。
| 偏移 | 大小 | 内容 | 说明 |
|---|---|---|---|
| 0x00 | 4 字节 | 签名长度 | 小端序 uint32,RSA 2048 固定为 256 |
| 0x04 | 签名长度 | 签名数据 | 原始签名二进制 |
| 0x04 + sig_len | 固件大小 | 固件数据 | 原始固件,不做任何修改 |
经验之谈:签名放在固件前面,BootROM 可以边读边验证,不用先跳到固件末尾找签名。这对资源受限的 MCU 很友好。
3.4 完整工具链流程图
下面这张图展示了整个工具链的工作流程,从原始固件到签名固件,再到 BootROM 验证。你仔细看,每一步都有对应的工具或代码。
3.5 生产环境注意事项
脚本写完了,但离生产环境还有几步路。我列几个关键点,都是我在项目里踩过的坑。
- 私钥保护:私钥绝不能硬编码在脚本里,也不能放在代码仓库中。用硬件安全模块(HSM)或者密钥管理服务(KMS)存储。
- 版本管理:每次签名都要记录固件版本、哈希值、签名时间。方便追溯。
- 多平台支持:签名脚本最好在 Linux 和 Windows 上都能跑。用 Python 的
pathlib处理路径,别用os.path。 - 错误处理:签名失败时,不要生成输出文件,避免误用。脚本里加
try-except捕获异常。
重要提醒:签名脚本的哈希算法和填充方式必须与 BootROM 完全一致。SHA-256 配 PKCS#1 v1.5 是常见组合,但有些芯片用 ECDSA 或者 PSS 填充。拿到芯片手册先确认,别想当然。