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
我的习惯:私钥文件一定要设置权限 600,或者直接存到 HSM(硬件安全模块)里。我曾经见过有人把私钥放在 Git 仓库里,那场面……嗯,不提了。

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
注意:OpenSSL 默认的签名输出格式是 DER(二进制),不是 Base64。如果你需要在文本协议里传输签名,记得加一步 -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 工具链协作流程

下面这张图,是我自己总结的工具链协作流程。每次做签名方案设计时,我都会拿这张图出来对照一遍,看看哪个环节的工具还没选好。

签名工具链协作流程图 签名工具链协作流程图 阶段一:密钥生成 OpenSSL ecparam 阶段二:固件准备 sha256sum / Python hashlib 阶段三:签名计算 OpenSSL dgst / Python cryptography 阶段四:签名验证 OpenSSL verify / Python verify 阶段五:联调比对 HASH校验工具 阶段六:问题定位 对比HASH / 检查字节序 反馈修正 说明: 1. 密钥生成只需做一次,私钥必须安全保管 2. 联调比对时,优先用 HASH 工具确认数据一致性,再查签名逻辑

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 个字节,联调了整整两天。
总结一下:工具链本身不复杂,复杂的是两边对齐。OpenSSL 负责“造”,Python 负责“自动化”,HASH 工具负责“查”。三样东西配合好,签名联调就能事半功倍。