4、固件签名流程:开发阶段签名、构建系统集成、签名工具链
签名这件事,说起来就是「用私钥算个值贴上去」。但真放到固件开发流程里,你会发现坑特别多。我在几个项目里都见过同一个问题:开发阶段随便签个名,到了量产发现工具链对不上,整个构建系统得重改。
这一章,我就把签名流程拆成三个阶段来讲。你对照着自己的项目看看,哪一步还能优化。
4.1 开发阶段签名:先跑起来,再谈安全
开发阶段的核心目标是「让固件能跑在目标硬件上」。这时候如果搞一套完整的安全签名流程,开发效率会非常低。我个人习惯的做法是:开发阶段用测试密钥,但签名流程必须和生产环境一致。
核心原则:开发阶段可以放宽密钥管理,但不能放宽签名流程。否则到了量产,你会发现签名工具链根本跑不通。
4.1.1 测试密钥 vs 生产密钥
| 对比项 | 测试密钥 | 生产密钥 |
|---|---|---|
| 密钥长度 | 可适当降低(如 RSA-1024) | 按安全标准(如 RSA-4096) |
| 存储位置 | 代码仓库或本地文件 | HSM / 密钥管理服务 |
| 签名速度 | 快,不要求硬件加速 | 慢,但必须硬件保护 |
| 证书链 | 自签名即可 | 完整 CA 链 |
嗯,这里要注意:测试密钥的私钥绝对不能用于生产。我曾经见过一个团队,直接把测试密钥的 .pem 文件拷到量产服务器上——结果固件被逆向,私钥被提取,整个产品线都得召回。
4.1.2 开发阶段的签名脚本
我一般会在项目根目录放一个 sign_dev.sh,内容大概是这样:
#!/bin/bash
# 开发阶段签名脚本 - 仅用于本地测试
KEY_DIR="./dev_keys"
FW_BIN=$1
OUTPUT=$2
# 使用测试私钥签名
openssl dgst -sha256 -sign "$KEY_DIR/dev_private.pem" \
-out "$OUTPUT.sig" "$FW_BIN"
# 将签名附加到固件尾部
cat "$FW_BIN" "$OUTPUT.sig" > "$OUTPUT"
echo "[DEV] 签名完成: $OUTPUT"
这个脚本很简单,但有个关键点:签名算法和格式必须和生产环境一致。我见过有人开发阶段用 MD5,生产用 SHA256,结果签名验证代码要写两套——纯属给自己挖坑。
我的建议:开发阶段就固定好签名算法(比如 SHA256 + RSA-PSS),后面不要改。算法一旦定了,Bootloader 的验签代码、密钥存储格式、签名工具链都得跟着变,改一次成本极高。
4.2 构建系统集成:让签名成为流水线的一部分
构建系统集成,说白了就是让签名这件事自动化。不能靠人手动敲命令,也不能让开发者在本地签完再上传。
我参与过一个项目,初期是开发者在本地签名,然后通过 FTP 上传固件。结果有一次,有人上传了没签名的版本,产线刷进去直接变砖——3000 台设备,一台一台拆机重刷。从那以后,我坚持签名必须集成到 CI/CD 流水线里。
4.2.1 集成方式对比
| 集成方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Makefile 集成 | 简单直接,依赖少 | 密钥管理困难 | 小团队、原型验证 |
| CI/CD Pipeline | 自动化、可审计 | 需要配置密钥服务 | 正规产品开发 |
| 独立签名服务器 | 安全隔离、集中管控 | 网络延迟、单点故障 | 高安全要求场景 |
4.2.2 CI/CD 中的签名流程
以 Jenkins 为例,我习惯的流水线是这样的:
- 编译阶段:生成未签名的固件二进制文件
- 哈希计算:对固件计算 SHA256 摘要
- 签名请求:将哈希发送到签名服务(或 HSM)
- 签名返回:获取签名值,附加到固件尾部
- 完整性校验:用公钥验证签名是否正确
- 产物归档:将已签名的固件上传到制品库
注意:签名服务绝对不能直接暴露在公网上。我见过一个案例,签名 API 没有加认证,结果被外部攻击者调用了 10 万次签名请求——虽然密钥没泄露,但签名次数超限,证书被 CA 吊销了。
4.2.3 构建系统里的密钥管理
密钥怎么喂给构建系统?这是个头疼的问题。我推荐的做法是:
- 测试密钥:加密后存储在代码仓库的
secrets/目录,用项目密码解密 - 生产密钥:存储在 HSM 或云 KMS 中,构建系统通过 API 调用签名
说白了,生产密钥永远不要以文件形式出现在构建服务器上。你想想看,如果构建服务器被攻破,私钥文件直接被人拷走,那整个信任链就崩了。
4.3 签名工具链:选对工具,事半功倍
签名工具链的选择,直接影响开发效率和安全性。我这些年用过不少工具,踩过不少坑,下面说说我的经验。
4.3.1 常用签名工具对比
| 工具 | 支持的算法 | 适用场景 | 我的评价 |
|---|---|---|---|
| OpenSSL | RSA、ECDSA、Ed25519 | 通用签名、测试 | 功能强大,但命令行参数复杂 |
| Python Cryptography | RSA、ECDSA、Ed25519 | 集成到构建脚本 | API 清晰,推荐用于自动化 |
| mbedTLS | RSA、ECDSA | 嵌入式端验签 | 轻量,适合 Bootloader |
| HSM 厂商 SDK | 取决于硬件 | 生产环境签名 | 安全最高,但学习成本高 |
4.3.2 一个完整的签名脚本示例
下面是我在项目中常用的 Python 签名脚本片段。它支持 RSA 和 ECDSA 两种算法:
#!/usr/bin/env python3
"""
固件签名工具 - 支持 RSA 和 ECDSA
用法: python sign_fw.py --key private.pem --fw firmware.bin --algo rsa
"""
import argparse
import hashlib
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding
def sign_firmware(key_path, fw_path, algo):
# 读取私钥
with open(key_path, "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(), password=None
)
# 计算固件哈希
with open(fw_path, "rb") as f:
fw_data = f.read()
fw_hash = hashlib.sha256(fw_data).digest()
# 签名
if algo == "rsa":
signature = private_key.sign(
fw_hash,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
elif algo == "ecdsa":
signature = private_key.sign(
fw_hash,
ec.ECDSA(hashes.SHA256())
)
# 输出签名后的固件
output_path = fw_path + ".signed"
with open(output_path, "wb") as f:
f.write(fw_data)
f.write(signature)
print(f"[OK] 签名完成: {output_path}")
return output_path
避坑指南:签名时一定要明确指定填充方式。RSA 签名建议用 PSS 填充,不要用 PKCS#1 v1.5——后者有已知的 Bleichenbacher 攻击风险。我曾经在一个遗留项目里发现还在用 v1.5,赶紧让团队升级了。
4.3.3 工具链的版本管理
签名工具链的版本必须和 Bootloader 验签代码保持一致。我遇到过最坑的事:开发环境用 OpenSSL 1.1.1,生产环境用 OpenSSL 3.0,结果签名格式变了,验签一直失败。
我的做法是:把签名工具链容器化。在 Docker 里固定好 OpenSSL、Python 库的版本,构建系统直接调用容器签名。这样不管部署到哪台机器,签名结果都是一致的。
4.4 签名流程全景图
下面这张图展示了从开发到量产的完整签名流程。你可以对照着看看自己的项目处在哪个阶段:
4.5 常见问题与避坑指南
最后,我总结几个实际项目中经常遇到的问题:
- 签名格式不统一:开发用 DER 格式,生产用 PEM 格式,验签代码解析失败。我的建议是统一用 DER 格式,解析效率更高。
- 签名位置不一致:有人把签名放在固件头部,有人放在尾部。Bootloader 读取位置写死之后,改起来很麻烦。建议统一放在尾部,兼容性更好。
- 多签名支持:有些场景需要多个签名(比如多个厂商联合签名)。签名工具链要支持签名链的拼接和验证。
重要提醒:签名工具链的日志不要打印私钥内容。我曾经见过一个脚本,调试时把私钥 hex dump 打出来了,日志文件被提交到 Git 仓库——虽然很快删除了,但 Git 历史里永远留下了那个 commit。
签名流程看起来简单,但每个细节都关系到整个信任链的安全。从开发阶段的测试签名,到构建系统的自动化集成,再到工具链的选型,每一步都要想清楚。你想想看,如果签名这关没把好,后面 Bootloader 验签做得再好也没用——因为固件本身可能已经被篡改了。