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 验证。你仔细看,每一步都有对应的工具或代码。

固件签名工具链流程图 固件签名工具链流程图 原始固件 firmware.bin SHA-256 哈希计算 RSA 私钥签名 私钥 打包:长度+签名+固件 签名固件 firmware_signed.bin 工具链对应关系 1. OpenSSL 命令行 → 手动测试/调试 2. Python cryptography → 自动化脚本 3. 签名脚本 → 生产环境集成 BootROM 验证流程 1. 读取签名长度 2. 读取签名数据 3. 用公钥验签

3.5 生产环境注意事项

脚本写完了,但离生产环境还有几步路。我列几个关键点,都是我在项目里踩过的坑。

  • 私钥保护:私钥绝不能硬编码在脚本里,也不能放在代码仓库中。用硬件安全模块(HSM)或者密钥管理服务(KMS)存储。
  • 版本管理:每次签名都要记录固件版本、哈希值、签名时间。方便追溯。
  • 多平台支持:签名脚本最好在 Linux 和 Windows 上都能跑。用 Python 的 pathlib 处理路径,别用 os.path。
  • 错误处理:签名失败时,不要生成输出文件,避免误用。脚本里加 try-except 捕获异常。

重要提醒:签名脚本的哈希算法和填充方式必须与 BootROM 完全一致。SHA-256 配 PKCS#1 v1.5 是常见组合,但有些芯片用 ECDSA 或者 PSS 填充。拿到芯片手册先确认,别想当然。