sign-tool 本地测试流程

项目 内容
适用 sign 仓,本地(Mac)测试,不依赖持续集成、不上板
sign 提交 main @ 13843ad3
gateway 提交 main @ 1b5226bf(bootloader 主机测试,见第四节)
格式 v3:共用包,包头 312 字节、格式 3,格式 2 直接拒绝;签名范围包头前 248 字节 + 固件明文;身份字段:型号、固件名、版本、构建日期、构建时间,日期/时间 ASCII YYYYMMDD/HHMMSS,UTC;版本块仍是格式 2(偏移 0x1000,228 字节),与包头格式不混淆
包模型 一次打包出共用包(所有设备共用);包密钥每版本随机新生成,包密钥包每设备单独包(80 字节:临时公钥 32 + 包装后包密钥 48),设备用私钥解出包密钥再解固件、验签
命令行 pack --bin --sign-key(只签不分发设备密钥);wrap-key --package-key --encrypt-key --out(出 80 字节包密钥包,--out 必填);verify/unpack/info --verify-key --decrypt-key --key-package(三者齐全);unpack 默认输出为包头生成名加 -out.bin,verify 纯验证、不写文件
参数名 --sign-key(-s)签名私钥种子,pack 签名用;--encrypt-key(-e)接收方设备加密公钥,只给 wrap-key 用,pack 传了直接拒绝;--package-key 包密钥(32 字节),wrap-key 用;--key-package 包密钥包文件(80 字节),verify/unpack/info 用;--decrypt-key(-d)接收方解密私钥,verify/unpack/info 用;--verify-key(-v)签名公钥,verify/unpack/info 用
gen-keys 标签 sign key (seed, private key)、verify key (public key)、decrypt key (private key)、encrypt key (public key),四行值对齐到同一列
pack 包名 {固件名}-{型号}-release-v{版本}-{构建日期}-{构建时间}.pkg(文件名里的型号是完整订货型号,如 stm32h743vit6,由块里家族前缀 stm32h743vi 映射;日期与时间取块内构建日期/时间;包落在输入 bin 旁边,不取输入文件名;块里的型号字段为家族前缀 stm32h743vi,设备比对以块为准)
最新实测 2026-11-02:工具库单测 27 项+服务端单测 26 项+命令行单测 3 项全过;网关 bootloader 主机测试 25 项全过;本地端到端 21 项全过;CI 全绿后已改回私有
测试密钥 密钥由测试人手动生成(gen-keys),本文档不提供现成的密钥值;量产种子必须在用的那台机器上本地生成,绝不经聊天发送

每一步都有明确的通过标准,任一步失败即停,先定位再往下走。测试一律从源码编:无 release,没有可下载的包。

测试原则:全程按生产环境走——密钥由你手动生成(gen-keys),本文档不提供现成的密钥值;固件用随机字节;预期结果是前后一致性(推导对得上、验证后逐字节一致、错密钥拒收),不是固定值。用固定参考向量还是自己生成的密钥,由你定。

相关仓库:sign https://github.com/ffffxxxjjj/sign ;gateway https://github.com/ffffxxxjjj/gateway ;firmware-tool https://github.com/ffffxxxjjj/firmware-tool 。包格式权威定义见 sign 仓 docs/package-format.md;服务端端点见 sign 仓 docs/sign-server-api.md;固件 provisioning 见 gateway 仓 docs/provisioning.md。

准备:编出工具

逐行跑(行内代码点一下就复制):

  1. git clone https://github.com/ffffxxxjjj/sign
  2. cd sign
  3. cargo build --release --workspace

注意:仓已改回私有,克隆需有权限;不要在 /tmp 里编——/tmp 会被自动清理,编好的二进制可能丢,验证工作目录放持久盘。

产物都在仓根 target/release/:sign-tool(命令行)、sign-server(服务)、libsign_tool.dylib(Mac 原生库)。虚拟根下裸编 cargo build --release 也会编出两个成员,--workspace 写法最稳。开发环境(工具与版本)见 docs/development-environment.md。

一、sign-tool

先在仓库根目录编出工具(见“准备”),再跑下面命令。密钥由你手动生成(第 0 步),本文档不提供现成值。

# 第 0 步:手动生成本轮密钥(你自己跑 gen-keys,生成一次)
target/release/sign-tool gen-keys
# 记下:云端种子、云端公钥、设备 A 解密私钥、设备 A 加密公钥
# 再跑一次 gen-keys,只取其中的设备对作设备 B(另一台设备)
target/release/sign-tool gen-keys
# 记下:设备 B 私钥、设备 B 公钥

# 版本号只在这里填一次,造 bin 时填进版本块;pack 从 bin 块读,不用再传;
# 预期输出里的 1.2.3 就是 VERSION 的值
VERSION=1.2.3

# 造带格式 2 版本块的测试固件(4324 字节,版本块在 0x1000,型号 stm32h743vi、固件名 gateway、版本取 $VERSION、构建日期与构建时间取当前 UTC 时间)
python3 - "$VERSION" <<'EOF'
import os, struct, sys
from datetime import datetime, timezone
major, minor, patch = (int(x) for x in sys.argv[1].split('.'))
now = datetime.now(timezone.utc)
body = bytearray(os.urandom(0x1000 + 228))
blk = bytearray(228)
blk[0:4] = b'FWVR'
blk[4] = 2
blk[5:5+11] = b'stm32h743vi'
blk[105:105+7] = b'gateway'
struct.pack_into('<HHH', blk, 205, major, minor, patch)
blk[211:219] = now.strftime('%Y%m%d').encode()
blk[219:225] = now.strftime('%H%M%S').encode()
body[0x1000:0x1000+228] = blk
open('test-fw.bin', 'wb').write(body)
EOF

下面用占位符表示本轮生成的值:<签名私钥种子>、<签名公钥>、<解密私钥A>、<设备加密公钥A>、<解密私钥B>、<包密钥>(pack 输出的包密钥行)、<包密钥包A>(wrap-key 输出的 80 字节文件)。

T1-1 好包:打包、包密钥、解包

# 打包:签名 + 加密一次,出共用包;pack 只传 --sign-key,不传设备密钥(传 --encrypt-key 直接拒绝,见 T1-5)
# 包名永远从块信息生成(版本从 bin 版本块读,块里就是上面的 $VERSION;包落在输入 bin 旁边)
target/release/sign-tool pack --bin test-fw.bin \
  --sign-key <签名私钥种子>
# 生成的包名记下来,后面各步都用它(日期与时间是造 bin 的当时 UTC 日期/时间)
PKG=$(ls gateway-stm32h743vit6-release-v*.pkg)
# pack 输出三行:packed 4324 bytes of firmware (v1.2.3, model stm32h743vi, firmware gateway) into 4652 bytes: <包名>
#   header 312 bytes + body 4340 bytes (ciphertext + 16-byte tag),即包头 312 +固件 4324 +标签 16
#   package key (keep secret; wrap it per device with wrap-key): <64 位十六进制包密钥>
# 注意括号里打印的型号是块里的家族前缀 stm32h743vi,文件名里拼的是完整订货型号 stm32h743vit6,两者是同一颗芯片

# 给设备 A 包包密钥:包密钥包 80 字节(临时公钥 32 +包装后包密钥 48),--out 必填(包密钥包没有可推导名)
target/release/sign-tool wrap-key --package-key <包密钥> \
  --encrypt-key <设备加密公钥A> --out keypkg-a.bin
# 应打印 wrapped package key (80 bytes) written to keypkg-a.bin

# 解包:先解包密钥、再解密、再验签,全过才写出明文(默认输出为包头生成名加 -out.bin,即 ${PKG%.pkg}-out.bin,在包旁边)
target/release/sign-tool unpack --pkg "$PKG" \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 应打印 unpacked OK: firmware v1.2.3 (4324 bytes) written to gateway-stm32h743vit6-release-v1.2.3-<造 bin 的当时 UTC 日期>-<当时 UTC 时间>-out.bin
# sign-tool cmp:逐字节比对原固件与解包明文是否一致;一致打印
# PASS: identical(字节数)并退出 0,不一致报第一个不同字节并退出 1。内建命令,
# 不依赖系统 cmp,Windows 上也一样跑。
target/release/sign-tool cmp test-fw.bin "${PKG%.pkg}-out.bin"
# 应打印 PASS: identical (4324 bytes)

只验证不写文件用 verify(见 T1-5)。

T1-2 改一字节:验证与解包必须失败且不写明文

分两种位置,报错不同,都必须退出码 1 且 unpack 不写文件。verify/unpack 都带 --key-package keypkg-a.bin(T1-1 的包密钥包):

# 包头区(第 200 字节落在固件名字段):身份校验先挂
python3 -c "
d = bytearray(open('$PKG','rb').read()); d[200] ^= 0xFF; open('test-fw-bad.pkg','wb').write(d)"
target/release/sign-tool verify --pkg test-fw-bad.pkg \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 先清掉 T1-1 的解包输出,避免旧文件干扰“不生成”的判断
rm -f "${PKG%.pkg}-out.bin"
target/release/sign-tool unpack --pkg test-fw-bad.pkg \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 预期:退出码均为 1,报错 error: firmware name has non-NUL bytes after the terminator,且不生成 ${PKG%.pkg}-out.bin
# 包体区(第 1000 字节在包体):解密校验挂
python3 -c "
d = bytearray(open('$PKG','rb').read()); d[1000] ^= 0xFF; open('test-fw-bad2.pkg','wb').write(d)"
target/release/sign-tool verify --pkg test-fw-bad2.pkg \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
target/release/sign-tool unpack --pkg test-fw-bad2.pkg \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 预期:退出码均为 1,报错 error: body decryption failed: body tampered, or wrong package key,且不生成 ${PKG%.pkg}-out.bin

T1-3 错解密私钥(用另一台设备的解密私钥):验证与解包必须失败且不写明文

生产关键:设备 A 的包密钥包,设备 B 的私钥解不开。

  1. target/release/sign-tool verify --pkg "$PKG" --verify-key <签名公钥> --decrypt-key <解密私钥B> --key-package keypkg-a.bin
  2. target/release/sign-tool unpack --pkg "$PKG" --verify-key <签名公钥> --decrypt-key <解密私钥B> --key-package keypkg-a.bin

预期:两条命令退出码均为 1,报错 error: failed to unwrap package key: wrong decrypt key, or key package tampered,且不生成 ${PKG%.pkg}-out.bin。

注意:不要用“改末字节”当错密钥测试——X25519 钳制位会让末字节改动无效,测不出失败;要用另一台设备的解密私钥,或改中间字节。

T1-4 公钥推导一致性

  1. target/release/sign-tool derive-pub --kind ed25519 --input <签名私钥种子>
  2. target/release/sign-tool derive-pub --kind x25519 --input <解密私钥A>

预期:两次输出的 hex 值分别与 gen-keys 给出的签名公钥、设备 A 加密公钥逐字一致;注意输出带标签前缀(verify key: <hex>、encrypt key: <hex>),比对时比 hex 值,不要拿整行比;命令不回显私钥。

T1-5 pack 包名规则与 verify 语义

包名永远从块信息生成,格式 {固件名}-{型号}-release-v{版本}-{构建日期}-{构建时间}.pkg(文件名里的型号是完整订货型号,如 stm32h743vit6,由块里家族前缀映射;日期与时间取块内构建日期/时间,不是打包时刻;包落在输入 bin 旁边,不取输入文件名)。

# 用一份拷贝测包名生成,避免覆盖原固件;注意 t15.bin 与 test-fw.bin 是同一块(同一构建日期/时间),
# 所以包名与 T1-1 的相同、会覆盖同名包文件(每包包密钥现生,包字节不同,但解包明文相同,属正常现象)
cp test-fw.bin t15.bin
target/release/sign-tool pack --bin t15.bin \
  --sign-key <签名私钥种子>
# 应生成 gateway-stm32h743vit6-release-v1.2.3-<造 bin 的当时 UTC 日期>-<当时 UTC 时间>.pkg(在 t15.bin 旁边),不是 t15.pkg
# 默认输出为包头生成名加 -out.bin(在包旁边),即 ${PKG15%.pkg}-out.bin
PKG15=$(ls gateway-stm32h743vit6-release-v*.pkg)
target/release/sign-tool unpack --pkg "$PKG15" \
  --verify-key <签名公钥> --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 应生成 ${PKG15%.pkg}-out.bin,且与 test-fw.bin 逐字节一致
target/release/sign-tool cmp test-fw.bin "${PKG15%.pkg}-out.bin"
# verify 只给结论,不写文件
target/release/sign-tool verify --pkg "$PKG" \
  --verify-key <签名公钥> --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 应打印 verified OK: firmware v1.2.3 (4324 bytes),不生成任何文件
# pack 传设备密钥直接拒绝(不是静默忽略)
target/release/sign-tool pack --bin test-fw.bin \
  --sign-key <签名私钥种子> --encrypt-key <设备加密公钥A>
# 应退出码 1,报错 pack takes no --encrypt-key: pack produces a shared package for all devices; wrap its package key per device with wrap-key
# pack 传 --out 同样拒绝(包名永远从块生成)
target/release/sign-tool pack --bin test-fw.bin \
  --sign-key <签名私钥种子> --out custom.pkg
# 应退出码 1,报错 pack takes no --out: the package filename always comes from the firmware version block
# verify 传 --out 同样拒绝(verify 不写文件)
target/release/sign-tool verify --pkg "$PKG" \
  --verify-key <签名公钥> --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin --out custom.bin
# 应退出码 1,报错 verify writes no file; use unpack to write the decrypted firmware
# unpack 传 --out 与 --expect 同样拒绝(输出名永远从包头生成;比对用 cmp)
target/release/sign-tool unpack --pkg "$PKG" \
  --verify-key <签名公钥> --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin --out custom.bin
# 应退出码 1,报错 unpack takes no --out: the output filename always comes from the package header

T1-6 unpack:解包后用 cmp 比对原文件

  1. target/release/sign-tool unpack --pkg "$PKG" --verify-key <签名公钥> --decrypt-key <解密私钥A> --key-package keypkg-a.bin:应通过,写出 ${PKG%.pkg}-out.bin。
  2. target/release/sign-tool cmp "${PKG%.pkg}-out.bin" test-fw.bin:应打印 PASS: identical (4324 bytes)。

T1-7 info:显示 bin 与 pkg 信息

# bin 信息:型号、固件名、版本、构建日期、构建时间来自版本块(日期与时间是造 bin 时写进块里的 UTC 日期/时间,YYYYMMDD/HHMMSS)
target/release/sign-tool info --bin test-fw.bin
# 应打印:size: 4324 bytes、model: stm32h743vi、firmware name: gateway、version: v1.2.3、
# version block: offset 0x1000 (228 bytes)、build date: 造 bin 的当时 UTC 日期、build time: 造 bin 的当时 UTC 时间
# pkg 信息(不用密钥):型号、固件名、版本、构建日期、构建时间、长度、标志都来自 v3 包头,不用解密
target/release/sign-tool info --pkg "$PKG"
# 应打印:format: GWPK v3、model: stm32h743vi、firmware name: gateway、firmware version: v1.2.3、
# build date / build time(造 bin 的当时 UTC 日期/时间)、firmware length: 4324 bytes、
# flags: 0x3 (encrypted, signed)、header: 312 bytes、body: 4340 bytes (ciphertext + 16-byte tag)、total: 4652 bytes
# pkg 信息(给齐三个密钥参数):解密后读内嵌版本块,型号、固件名、版本应与包头一致
target/release/sign-tool info --pkg "$PKG" \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 应在上面各项后追加 embedded firmware version block 一节:model / firmware name / version 三项带 (matches header),
# build date / build time 与包头相同(不带 matches header 字样)

T1-8 包头解析全拒绝路径

包头解析按固定顺序校验:长度 → 魔数 → 格式版本 → 标志 → 型号 → 固件名 → 构建日期/时间。以下全部用正确的云端公钥与设备 A 解密私钥和 T1-1 的包密钥包,只改包、不改密钥,这样报错只可能来自篡改本身。每条从 $PKG 出发写出同一个 test-fw-t8.pkg,逐行覆盖重跑即可;每条跑完都跑一次 verify;预期均为退出码 1;unpack 同文件同报错且不写文件。8a/8b 直接跑 shell 命令,不套模板;8c 起把表里的改动填进模板。

# 8a 文件过短(100 字节,够读魔数与格式、但不够一个包头):跑完跑一次 verify(下面模板块里那条),预期 package too short: 100 bytes, v3 header alone is 312 bytes
head -c 100 "$PKG" > test-fw-t8.pkg
# 8b 空文件(不够 8 字节,连格式都读不出):同上,预期 package too short: 0 bytes, header alone is 312 bytes
: > test-fw-t8.pkg
# 篡改模板:<改动> 由下表给出
python3 -c "import struct; d = bytearray(open('$PKG','rb').read()); <改动>; open('test-fw-t8.pkg','wb').write(d)"
target/release/sign-tool verify --pkg test-fw-t8.pkg \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
用例 改动 预期报错
T1-8c 坏魔数 d[0:4] = b'XXXX' bad magic: package does not start with GWPK
T1-8d 格式版本 1 struct.pack_into('<I', d, 4, 1) unsupported format version: 1 (expected 3)
T1-8d2 格式版本 2 struct.pack_into('<I', d, 4, 2) unsupported format version: 2 (expected 3)(格式 2 直接拒绝,没有兼容路径)
T1-8e 标志缺加密 struct.pack_into('<I', d, 232, 2) unsupported flags: 0x00000002 (packages require encrypted + signed, no other bits)
T1-8f 标志缺签名 struct.pack_into('<I', d, 232, 1) unsupported flags: 0x00000001 (packages require encrypted + signed, no other bits)
T1-8g 标志多余位 struct.pack_into('<I', d, 232, 7) unsupported flags: 0x00000007 (packages require encrypted + signed, no other bits)
T1-8h 型号填充坏 d[50] ^= 0xFF(型号字段 [8..108] 的填充区) model has non-NUL bytes after the terminator
T1-8i 型号全零 d[8:108] = b'\x00'*100 model is empty (field is all zeros)
T1-8j 型号非 ASCII d[8] = 0xFF model is not ASCII
T1-8k 固件名全零 d[108:208] = b'\x00'*100 firmware name is empty (field is all zeros)
T1-8l 固件名非 ASCII d[108] = 0xFF firmware name is not ASCII
T1-8m 日期非数字 d[214] = ord('A') build date/time must be ASCII digits
T1-8n 月份越界 d[218:220] = b'13' build date month out of range: 13
T1-8o 日期越界 d[220:222] = b'32' build date day out of range: 32
T1-8p 小时越界 d[222:224] = b'24' build time hour out of range: 24
T1-8q 分钟越界 d[224:226] = b'60' build time minute out of range: 60
T1-8r 秒越界 d[226:228] = b'60' build time second out of range: 60

固件名填充坏已在 T1-2(第 200 字节)覆盖,不重复。注意校验顺序:同一包改两处时,先列出的检查先报;本节每行只改一处,报错唯一。

T1-9 体长与密钥签名链

verify 的链路顺序:包头解析 → 体长 → 解包密钥 → 解密包体 → 验签名。前面挂了就看不到后面的错;T1-9 每行只改一处,报错唯一。篡改模板与 T1-8 相同,输出 test-fw-t9.pkg,密钥用正确的云端公钥与设备 A 解密私钥和 T1-1 的包密钥包。9a~9i 把表里的改动填进模板;9j/9k/9l 不套模板,见表后三条;每条跑完都跑一次 verify;预期均为退出码 1。

# 篡改模板:<改动> 由下表给出
python3 -c "import struct; d = bytearray(open('$PKG','rb').read()); <改动>; open('test-fw-t9.pkg','wb').write(d)"
target/release/sign-tool verify --pkg test-fw-t9.pkg \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
用例 改动 预期报错
T1-9a 长度字段改大 struct.pack_into('<I', d, 228, 4325) body length mismatch: body is 4340 bytes, expected plaintext 4325 + tag 16
T1-9b 包密钥包临时公钥改 改 keypkg-a.bin 第 10 字节(临时公钥 [0..32]) failed to unwrap package key: wrong decrypt key, or key package tampered
T1-9c 包密钥包包装密钥改 改 keypkg-a.bin 第 40 字节(包装后包密钥 [32..80]) failed to unwrap package key: wrong decrypt key, or key package tampered
T1-9d 体随机数改 d[240] ^= 0xFF(体随机数 [236..248]) body decryption failed: body tampered, or wrong package key
T1-9e 签名区改 d[260] ^= 0xFF(签名 [248..312]) signature verification failed: wrong verify key, or header/firmware tampered
T1-9f 版本改值 struct.pack_into('<H', d, 208, 2)(主版本 1→2,格式仍合法) signature verification failed: wrong verify key, or header/firmware tampered
T1-9g 型号改值 d[8] = ord('t')(stm32h743vi→tm32h743vi,格式仍合法) signature verification failed: wrong verify key, or header/firmware tampered
T1-9h 固件名改值 d[108] = ord('h')(gateway→hateway,格式仍合法) signature verification failed: wrong verify key, or header/firmware tampered
T1-9i 日期改值 d[220:222] = b'02'(日期合法数字,仍在范围内) signature verification failed: wrong verify key, or header/firmware tampered
# 9j 错云端公钥:不改包,verify 传第二次 gen-keys 的云端公钥
target/release/sign-tool verify --pkg "$PKG" \
  --verify-key <第二次 gen-keys 的云端公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 应退出码 1,报错 signature verification failed: wrong verify key, or header/firmware tampered
# 9k 包体截断:不套模板
head -c 1000 "$PKG" > test-fw-t9.pkg
target/release/sign-tool verify --pkg test-fw-t9.pkg \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 应退出码 1,报错 body length mismatch: body is 688 bytes, expected plaintext 4324 + tag 16
# 9l 包体后追加:不套模板,整条 python 命令直接跑
python3 -c "open('test-fw-t9.pkg','wb').write(open('$PKG','rb').read() + __import__('os').urandom(16))"
target/release/sign-tool verify --pkg test-fw-t9.pkg \
  --verify-key <签名公钥> \
  --decrypt-key <解密私钥A> \
  --key-package keypkg-a.bin
# 应退出码 1,报错 body length mismatch: body is 4356 bytes, expected plaintext 4324 + tag 16

T1-9b/9c 与 T1-3 同一句报错,触发不同:T1-3 是解密私钥不对,T1-9b/9c 是包密钥包里的密钥材料被改。T1-9d 与 T1-2 包体区同一句,触发不同:T1-2 改的是密文本身,T1-9d 改的是体随机数。

T1-10 pack 与版本块

版本块在 bin 的 0x1000 处:魔数 FWVR(0x1000..0x1004)、格式字节(0x1004)、型号(0x1005..0x1069)、固件名(0x1069..0x10CD)、版本(0x10CD..0x10D3)、日期(0x10D3..0x10DB)、时间(0x10DB..0x10E1)、保留(0x10E1..0x10E4)。

# 10a:bin 无版本块(全零 bin 不可能撞上 FWVR)
head -c 4324 /dev/zero > noblock.bin
target/release/sign-tool pack --bin noblock.bin \
  --sign-key <签名私钥种子>
# 应退出码 1,报错 firmware has no version block at bin offset 0x1000: build firmware with the version block,不生成包
# 10b:版本块格式不是 2
python3 -c "d = bytearray(open('test-fw.bin','rb').read()); d[0x1004] = 1; open('badfmt.bin','wb').write(d)"
target/release/sign-tool pack --bin badfmt.bin \
  --sign-key <签名私钥种子>
# 应退出码 1,报错 unsupported version block format: 1 (expected 2),不生成包
# 10c:版本块截断(只有魔数与格式,缺后半段)
head -c 4196 test-fw.bin > shortblk.bin
target/release/sign-tool pack --bin shortblk.bin \
  --sign-key <签名私钥种子>
# 应退出码 1,报错 version block truncated: 4196 bytes, need 4324 bytes,不生成包
# 10d:只有魔数、缺格式字节
head -c 4100 test-fw.bin > magicblk.bin
target/release/sign-tool pack --bin magicblk.bin \
  --sign-key <签名私钥种子>
# 应退出码 1,报错 version block truncated: magic present but format byte missing,不生成包
# 10e:版本块保留字节非零
python3 -c "d = bytearray(open('test-fw.bin','rb').read()); d[0x10E1] = 1; open('resvblk.bin','wb').write(d)"
target/release/sign-tool pack --bin resvblk.bin \
  --sign-key <签名私钥种子>
# 应退出码 1,报错 version block reserved bytes must be zero,不生成包
# 10f:版本块固件名填充坏(走 pack 路径,文案与 T1-2 相同)
python3 -c "d = bytearray(open('test-fw.bin','rb').read()); d[0x1000+150] ^= 0xFF; open('badnameblk.bin','wb').write(d)"
target/release/sign-tool pack --bin badnameblk.bin \
  --sign-key <签名私钥种子>
# 应退出码 1,报错 firmware name has non-NUL bytes after the terminator,不生成包
# 10g:版本块日期月份越界(走 pack 路径)
python3 -c "d = bytearray(open('test-fw.bin','rb').read()); d[0x10D7:0x10D9] = b'13'; open('badmonblk.bin','wb').write(d)"
target/release/sign-tool pack --bin badmonblk.bin \
  --sign-key <签名私钥种子>
# 应退出码 1,报错 build date month out of range: 13,不生成包
# 10h:info --bin 无版本块(文案比 pack 的短)
target/release/sign-tool info --bin noblock.bin
# 应退出码 1,报错 firmware has no version block at bin offset 0x1000
# 10i:info --pkg 只给一个密钥(三个密钥参数必须齐全或全不给)
target/release/sign-tool info --pkg "$PKG" --verify-key <签名公钥>
# 应退出码 1,报错 info --pkg needs --verify-key, --decrypt-key, and --key-package together to decrypt
# 10j:info 既不给 --bin 也不给 --pkg(两者都给同样报错)
target/release/sign-tool info
# 应退出码 1,报错 info takes exactly one of --bin <bin> or --pkg <pkg>

版本块内型号/固件名/日期的其余格式错(全零、非 ASCII、分钟秒越界等)与 T1-8 同文案,走 pack 路径触发,不逐条重复。

T1-11 参数与文件

verify/unpack 命令里的 --key-package keypkg-a.bin 不可省略(缺了先报缺参数,不是验包失败):

# 11a:缺必需参数(缺 --decrypt-key)
target/release/sign-tool verify --pkg "$PKG" --verify-key <签名公钥> --key-package keypkg-a.bin
# 应退出码 1,报错 missing required flag --decrypt-key
# 11a2:缺 --key-package
target/release/sign-tool verify --pkg "$PKG" --verify-key <签名公钥> --decrypt-key <解密私钥A>
# 应退出码 1,报错 missing required flag --key-package
# 11b:参数无值(下一个 token 又是参数)
target/release/sign-tool verify --pkg --verify-key <签名公钥> --decrypt-key <解密私钥A> --key-package keypkg-a.bin
# 应退出码 1,报错 missing value for --pkg
# 11c:裸参数(不是 --flag 形式)
target/release/sign-tool verify foo --pkg "$PKG" --verify-key <签名公钥> --decrypt-key <解密私钥A> --key-package keypkg-a.bin
# 应退出码 1,报错 unexpected argument 'foo': expected --flag value
# 11d:重复参数
target/release/sign-tool verify --pkg "$PKG" --pkg "$PKG" --verify-key <签名公钥> --decrypt-key <解密私钥A> --key-package keypkg-a.bin
# 应退出码 1,报错 duplicate flag --pkg
# 11e:未知短参数
target/release/sign-tool verify -x --pkg "$PKG" --verify-key <签名公钥> --decrypt-key <解密私钥A> --key-package keypkg-a.bin
# 应退出码 1,报错 unexpected argument '-x': expected --flag value
# 11f:未知子命令(裸跑 sign-tool 同样报错)
target/release/sign-tool frobnicate
# 应退出码 1,报错 missing or unknown subcommand
# 11g:密钥非十六进制
target/release/sign-tool verify --pkg "$PKG" --verify-key zz --decrypt-key <解密私钥A> --key-package keypkg-a.bin
# 应退出码 1,报错 bad hex string 'zz': expected 64 hex characters
# 11h:密钥长度不对(62 位十六进制=31 字节)
target/release/sign-tool verify --pkg "$PKG" --verify-key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --decrypt-key <解密私钥A> --key-package keypkg-a.bin
# 应退出码 1,报错 bad key length: 31 bytes, expected 32 bytes (64 hex characters)
# 11i:验证公钥无法解析(y=2 没有对应曲线点;包用好包、解密私钥用设备 A 的)
target/release/sign-tool verify --pkg "$PKG" --verify-key 0200000000000000000000000000000000000000000000000000000000000000 --decrypt-key <解密私钥A> --key-package keypkg-a.bin
# 应退出码 1,报错 invalid verify key
# 注意:全 f 的 32 字节能解析(y 取模后是 18,有对应点),不触发本条,错钥匙时报 signature verification failed
# 11j:文件不存在(三处同 pattern,后缀是系统文案)
target/release/sign-tool verify --pkg 不存在.pkg --verify-key <签名公钥> --decrypt-key <解密私钥A> --key-package keypkg-a.bin
# 应退出码 1,报错 cannot read package file '不存在.pkg': No such file or directory (os error 2)
target/release/sign-tool pack --bin 不存在.bin --sign-key <签名私钥种子>
# 应退出码 1,报错 cannot read firmware file '不存在.bin': No such file or directory (os error 2)
target/release/sign-tool cmp 不存在1 不存在2
# 应退出码 1,报错 cannot read file '不存在1': No such file or directory (os error 2)
# 11k:cmp 参数个数不对(一个或三个都一样报错)
target/release/sign-tool cmp test-fw.bin
# 应退出码 1,报错 cmp takes exactly two file arguments: sign-tool cmp <file1> <file2>
# 11l:cmp 长度不同(other.bin 是 256 字节随机文件)
head -c 256 /dev/urandom > other.bin
target/release/sign-tool cmp test-fw.bin other.bin
# 应退出码 1,报错 files differ in length: 4324 vs 256 bytes
# 11m:cmp 同长首个不同字节(位置从 1 数;hex 值随随机固件而变,只认位置)
cp test-fw.bin same.bin
python3 -c "d = bytearray(open('same.bin','rb').read()); d[100] ^= 0xFF; open('same.bin','wb').write(d)"
target/release/sign-tool cmp test-fw.bin same.bin
# 应退出码 1,报错 files differ: byte 101: 0x.. vs 0x..(101 是改动位置 100 从 1 数)
# 11n:derive-pub 坏 kind(--input 须给合法 64 位十六进制,否则先报密钥错)
target/release/sign-tool derive-pub --kind foo --input <64 位十六进制>
# 应退出码 1,报错 unknown --kind 'foo': expected ed25519 or x25519
# 11o:derive-pub 缺 --input
target/release/sign-tool derive-pub --kind ed25519
# 应退出码 1,报错 missing required flag --input
# 11p:cmp 第二个文件不存在(第一个存在,走到第二个读文件分支)
target/release/sign-tool cmp test-fw.bin 不存在2
# 应退出码 1,报错 cannot read file '不存在2': No such file or directory (os error 2)

T1-12 正向补齐

# 12a:1MB 固件往返(先造一个带版本块的 big-fw.bin,构建时间取当前 UTC,与 test-fw.bin 不同,包名不撞;再追加随机字节到 1048576)
python3 - "1.2.3" <<'EOF'
import os, struct, sys
from datetime import datetime, timezone
major, minor, patch = (int(x) for x in sys.argv[1].split('.'))
now = datetime.now(timezone.utc)
body = bytearray(os.urandom(0x1000 + 228))
blk = bytearray(228)
blk[0:4] = b'FWVR'
blk[4] = 2
blk[5:5+11] = b'stm32h743vi'
blk[105:105+7] = b'gateway'
struct.pack_into('<HHH', blk, 205, major, minor, patch)
blk[211:219] = now.strftime('%Y%m%d').encode()
blk[219:225] = now.strftime('%H%M%S').encode()
body[0x1000:0x1000+228] = blk
open('big-fw.bin', 'wb').write(body)
EOF
python3 -c "open('big-fw.bin','ab').write(__import__('os').urandom(1048576-4324))"
target/release/sign-tool pack --bin big-fw.bin \
  --sign-key <签名私钥种子>
# 应生成 gateway-stm32h743vit6-release-v1.2.3-<big-fw 构建日期>-<big-fw 构建时间>.pkg,输出 packed 1048576 bytes of firmware (v1.2.3, model stm32h743vi, firmware gateway) into 1048904 bytes
# header 312 bytes + body 1048592 bytes (ciphertext + 16-byte tag),外加 package key 一行
BIGPKG=$(ls -t gateway-stm32h743vit6-release-v1.2.3-*.pkg | head -1)
# 给设备 A 包 big 包的包密钥(pack 输出的 package key 行,记下来填进占位符)
target/release/sign-tool wrap-key --package-key <big 包的包密钥> \
  --encrypt-key <设备加密公钥A> --out keypkg-big-a.bin
target/release/sign-tool verify --pkg "$BIGPKG" \
  --verify-key <签名公钥> --decrypt-key <解密私钥A> \
  --key-package keypkg-big-a.bin
# 应打印 verified OK: firmware v1.2.3 (1048576 bytes)
target/release/sign-tool unpack --pkg "$BIGPKG" \
  --verify-key <签名公钥> --decrypt-key <解密私钥A> \
  --key-package keypkg-big-a.bin
target/release/sign-tool cmp big-fw.bin "${BIGPKG%.pkg}-out.bin"
# 应打印 PASS: identical (1048576 bytes)
# 12b:gen-keys 输出形状(值每次随机,只认形状)
target/release/sign-tool gen-keys
# 应打印 1 行表头 === TEST KEYS - 测试密钥,切勿用于量产 ===,再加 4 行:sign key (seed, private key)、verify key (public key)、decrypt key (private key)、encrypt key (public key),每行值 64 位十六进制(private key 是私钥、public key 是公钥)

T1-13 wrap-key:包密钥包

# 13a:缺 --out(包密钥包没有可推导名,--out 必填)
target/release/sign-tool wrap-key --package-key <包密钥> --encrypt-key <设备加密公钥A>
# 应退出码 1,报错 missing required flag --out
# 13b:包密钥长度不对(62 位十六进制=31 字节)
target/release/sign-tool wrap-key --package-key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --encrypt-key <设备加密公钥A> --out keypkg-bad.bin
# 应退出码 1,报错 bad key length: 31 bytes, expected 32 bytes (64 hex characters)
# 13c:同一包密钥包两次,结果不同、但都解出同一包密钥(临时密钥对每次现生,包装密钥永不重复)
target/release/sign-tool wrap-key --package-key <包密钥> --encrypt-key <设备加密公钥A> --out keypkg-a2.bin
# keypkg-a2.bin 与 keypkg-a.bin 字节不同(临时公钥不同)
target/release/sign-tool cmp keypkg-a.bin keypkg-a2.bin
# 应退出码 1(两文件不同,属正常现象,不是不通过)
target/release/sign-tool verify --pkg "$PKG" \
  --verify-key <签名公钥> --decrypt-key <解密私钥A> \
  --key-package keypkg-a2.bin
# 应打印 verified OK: firmware v1.2.3 (4324 bytes)(第二个包密钥包同样解出同一包密钥)
# 13d:给设备 B 包同一包密钥,设备 B 也能解(同一共用包、不同包密钥包)
target/release/sign-tool wrap-key --package-key <包密钥> --encrypt-key <设备加密公钥B> --out keypkg-b.bin
target/release/sign-tool verify --pkg "$PKG" \
  --verify-key <签名公钥> --decrypt-key <解密私钥B> \
  --key-package keypkg-b.bin
# 应打印 verified OK: firmware v1.2.3 (4324 bytes)

二、单元测试

  1. cargo test -p sign-tool:库 27 项。
  2. cargo test --workspace:连服务一起:库 27 + 服务 26 + 命令行单测 3 项。

覆盖(库 27 项):共用包打包再验证与原固件逐字节一致、空固件往返、包体改一字节失败、错解密私钥失败、错云端公钥失败、头部被改失败、错误魔数与格式版本拒绝(格式 2 直接拒绝)、包密钥包包装与解包、推导公钥与生成值一致、包名按块生成(型号完整型号、release 段)与默认路径(打包默认名、解包默认名加 -out.bin)、wrap-key 输出 80 字节、服务接口(未配置拒收、非法配置拒收、版本号非空)。

覆盖(命令行单测 3 项):pack 默认名含完整型号与 release 段;unpack 默认输出为包头生成名加 -out.bin;wrap-key 相关(包名与输出规则)。

覆盖(服务 26 项):文件名重名加后缀、记录增删改查往返、产品名前缀校验、页面转义、种子文件加载(600 权限通过、644 拒绝、坏内容拒绝)、MQTT 话题解析/命令 JSON/状态入库、推送解析与最新版本、批量筛选与命令编号、操作员与分发(分发/重发/禁用规则)、包密钥记录掩码等。

预期:全部通过,0 失败。

四、设备验证

这一步验证 sign-tool 打出的共用包能被设备侧库验证通过,是打包—验包链路的另一端。

  1. cd gateway/bootloader
  2. cargo test

  3. 预期:25 项全过(验证 12 项+provisioning 13 项)。主机测试与 sign-tool 验证同一套 v3 算法。

  4. 路径:gateway 是 clone 下来的默认目录名,在放仓的目录下执行;如果已经在仓根下,直接 cd bootloader 再 cargo test。
  5. 注意:bootloader 主机测试不再依赖 sign-tool(测试打包器直接按包格式文档实现 v3;测试向量是真实 sign-tool v3 生成的共用包与包密钥包:8192 字节固件、8520 字节包、80 字节包密钥包,两边独立实现交叉验证)。
  6. 注意:格式 2 的包在设备侧直接拒绝(BadVersion),与命令行 T1-8d2 一致。
  7. provisioning 串口协议(PROV BEGIN/SERIAL/KEY/COMMIT/ABORT/STATUS)见 gateway 仓 docs/provisioning.md;写入工具设计见 firmware-tool 仓 docs/固件写入工具设计.md。

通过标准与失败处理

附录 A:密钥占位符(你手动生成,文档不提供现成值)

占位符 含义 来源
<签名私钥种子> 云端签名私钥种子(Ed25519) 本轮 gen-keys 输出
<签名公钥> 云端签名公钥 本轮 gen-keys 输出(或 T1-4 推导,应一致)
<解密私钥A> / <设备加密公钥A> 设备 A 的密钥对(X25519) 本轮 gen-keys 输出
<解密私钥B> / <设备加密公钥B> 设备 B 的密钥对(另一台设备) 第二次 gen-keys 输出(只取设备对)
<包密钥> 本版本共用包的包密钥(32 字节,64 位十六进制) 本轮 pack 输出的 package key 行(只进 wrap-key,不进包)
<包密钥包A> 给设备 A 的包密钥包文件(80 字节) 本轮 wrap-key 输出(keypkg-a.bin)

密钥由你手动生成:测试前自己跑 gen-keys,记下各值填进占位符;本文档不提供现成的密钥值。固定参考向量(sign 仓 docs/package-format.md 第 6 节)只作格式参考,用不用由你定;是否每轮重新生成,也由你定。量产密钥与测试密钥生成方法相同(操作系统随机数),区别只在保管:量产种子在服务器本机生成、解密私钥每台设备独立生成、一机一密、后台分发(见固件分发全流程)。

附录 B:命令速查

# 编译
cargo build --release --workspace

# 手动生成密钥(测试前自己跑)
sign-tool gen-keys

# 命令行
sign-tool derive-pub --kind ed25519|x25519 --input <hex 32 字节>
sign-tool pack --bin <bin> --sign-key <hex>
sign-tool wrap-key --package-key <hex> --encrypt-key <hex> --out <key-package-file>
sign-tool verify --pkg <pkg> --verify-key <hex> --decrypt-key <hex> --key-package <key-package-file>
sign-tool unpack --pkg <pkg> --verify-key <hex> --decrypt-key <hex> --key-package <key-package-file>
sign-tool cmp <file1> <file2>
sign-tool info --bin <bin>
sign-tool info --pkg <pkg> [--verify-key <hex> --decrypt-key <hex> --key-package <key-package-file>]

# 单测
cargo test -p sign-tool
cargo test --workspace

# 设备验证(gateway 仓 bootloader 目录)
cargo test

附录 C:报错→用例对照表

实现里每条拒绝路径都有对应手动用例;下表即完整性自查。

命令行报错

报错 用例
package too short(两种:短于 8 字节/短于 312 字节) T1-8b/T1-8a
bad magic: package does not start with GWPK T1-8c
unsupported format version(1 与 2 均拒绝,只认 3) T1-8d/T1-8d2
unsupported flags T1-8e/T1-8f/T1-8g
model is empty / is not ASCII / has non-NUL bytes after the terminator T1-8i/T1-8j/T1-8h
firmware name is empty / is not ASCII / has non-NUL bytes after the terminator T1-8k/T1-8l/T1-2
build date/time must be ASCII digits T1-8m
build date month / day out of range T1-8n/T1-8o
build time hour / minute / second out of range T1-8p/T1-8q/T1-8r
body length mismatch T1-9a/T1-9k/T1-9l
failed to unwrap package key T1-3(解密私钥不对)、T1-9b/T1-9c(包密钥包材料被改)
body decryption failed T1-2 包体区、T1-9d(体随机数改)
signature verification failed T1-9e/T1-9f/T1-9g/T1-9h/T1-9i/T1-9j
invalid verify key T1-11i
firmware has no version block at bin offset 0x1000(pack 长文案/info 短文案) T1-10a/T1-10h
unsupported version block format T1-10b
version block truncated(缺格式字节/缺后半段) T1-10d/T1-10c
version block reserved bytes must be zero T1-10e
missing required flag T1-11a/T1-11a2(缺 --key-package)/T1-11o
missing value for T1-11b
unexpected argument T1-11c(裸参数)、T1-11e(未知短参数)
duplicate flag T1-11d
missing or unknown subcommand T1-11f
bad hex string/bad key length T1-11g/T1-11h/T1-13b
unknown --kind T1-11n
cannot read firmware / package / key package / file T1-11j/T1-11p
cmp takes exactly two file arguments T1-11k
files differ in length/files differ: byte T1-11l/T1-11m
info takes exactly one of --bin / --pkg T1-10j
info --pkg needs --verify-key, --decrypt-key, and --key-package together to decrypt T1-10i
pack takes no --encrypt-key T1-5
pack takes no --out T1-5
verify writes no file; use unpack to write the decrypted firmware T1-5
unpack takes no --out/unpack takes no --expect T1-5
missing required flag --out(wrap-key) T1-13a
PASS: identical(正向) T1-1、T1-5、T1-6、T1-12a、T1-13c/T1-13d

不可手动触发(代码防御,不列用例)