使用步骤

从零开始,跑通一次“服务签名打包 + 机器调用”的完整流程。 测试密钥只用于本流程演示,量产密钥由工厂灌装流程产生。

准备

1. 编译

git clone https://github.com/ffffxxxjjj/sign
cd sign

# workspace 一次编完:tool 包 sign-tool(库 + 命令行 + 原生库 .so)
# 与 server 包 sign-server(服务),产物都在仓根 target/release/
cargo build --release --workspace

产物:target/release/libsign_tool.so(Linux;Windows 是 sign_tool.dll、macOS 是 libsign_tool.dylib,由 CI 三平台构建), 服务在 target/release/sign-server(workspace 共用仓根 target/)。

2. 准备种子与管理员密码(两个 600 文件)

种子是签名私钥,管理员密码只给浏览器管理页用, 两者都不要打在命令行上(会进历史),用 read -s 写文件:

# 在仓根写两个文件(起服务时从仓根引用)
read -s seed
printf '%s' "$seed" > seed.txt && chmod 600 seed.txt && unset seed
read -s adminpw
printf '%s' "$adminpw" > admin-password.txt && chmod 600 admin-password.txt && unset adminpw

3. 起服务

# 在仓根跑服务(-p 指定 workspace 成员)
SIGN_SERVER_SIGNING_SEED_FILE=seed.txt \
SIGN_SERVER_ADMIN_PASSWORD_FILE=admin-password.txt \
cargo run --release -p sign-server

启动输出三行:监听地址(默认 http://127.0.0.1:3000)、 签名公钥(验包时用)、数据库路径。 服务只绑本机;上服务器时走反向代理加密连接或只用隧道访问, 不要直接把端口暴露到公网。PORT、SIGN_SERVER_DATA_DIR 可改端口与数据目录(默认 data/,里面是数据库与包文件)。

4. 浏览器管理页(先跑通一次打包)

  1. 打开 http://127.0.0.1:3000/,用管理员密码登录。
  2. 上传 bin、填版本(如 1.2.3)、填目标设备加密公钥、点打包, 浏览器下载 pkg,记录出现在列表里。
  3. 列表可下载包文件、改备注、删除记录与文件。

这一步确认服务本身是好的,再往下接机器调用。

5. 分配调用凭证(机器调用方用,不用管理员登录)

管理页往下拉到“调用凭证”:

  1. 填个名称(如 java-backend),点新建。
  2. 页面显示 key(gwk_ + 16 位)与 secret(32 字节随机)—— secret 只显示这一次,复制后保存,列表里不再显示。
  3. 列表里能看到各凭证的名称、创建时间、最近使用;点吊销后 该凭证立即失效,无法恢复。

6. 机器调用

6a. Java(直调 HTTP,不用原生库)

# 从仓库根目录,编演示(纯 Java 11+,不用 JNA、不用原生库)
mkdir -p /tmp/javademo
javac -d /tmp/javademo tool/native/java/GwpackServiceDemo.java
java -cp /tmp/javademo GwpackServiceDemo http://127.0.0.1:3000 <api-key> <api-secret>

演示做两件事:配地址、key、secret,然后打包并打印服务给的 文件名。Java 这边只有三样东西:地址、key、secret;打包是共用的, 不再传设备密钥;Java 不持有签名种子和任何解密私钥。

生产里照抄演示的调法:按 6c 节的签名方法组装 multipart 与 4 个头调 /pack; Java 不持有签名种子和任何解密私钥。经原生库调服务的 Java 映射见 Gwpack.java(JNA),调法同 6b 节。

6b. 其他语言(加载 .so / .dll,调 C 服务接口)

头文件 tool/native/gwpack.h,三个函数:

configure("http://127.0.0.1:3000", api_key, api_secret);
pack(firmware, len, "gateway", "", out, &out_len);
last_filename(name_buf, sizeof(name_buf));

问长度那次调用不发请求(包长由格式定死:头 312 + 固件 + 标签 16),所以一次逻辑打包只向服务发一次请求、只存一条记录。

6c. 裸调接口(不用库,自己算签名)

POST /pack,multipart 字段:bin(文件)、可选 product (默认 gateway)、可选 remark。打包是共用的,不再传 device_pub(传了返回 400)。版本、构建时间、型号、固件名 不传,由服务端从上传固件的格式 2 版本块读取;没有有效格式 2 块返回 400。另带四个头:

签名串是七个字段按换行连接、顺序固定:方法(POST)、 路径(/pack)、时间戳、随机数、固件 SHA-256(十六进制)、 产品名(gateway 默认已应用)、备注;HMAC 的密钥是 secret 本身的 UTF-8 字节。参考实现(Python 标准库,无第三方依赖):

import hashlib, hmac, time, urllib.request

KEY, SECRET = "<api-key>", "<api-secret>"
fw = open("firmware.bin", "rb").read()
ts, nonce = str(int(time.time())), "0123456789abcdef"
fw_hash = hashlib.sha256(fw).hexdigest()
product, remark = "gateway", ""
canonical = "\n".join(["POST", "/pack", ts, nonce, fw_hash,
                        product, remark])
sig = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
body = (b'--B\r\nContent-Disposition: form-data; name="bin"; filename="firmware.bin"\r\n'
        b'Content-Type: application/octet-stream\r\n\r\n' + fw +
        b'\r\n--B\r\nContent-Disposition: form-data; name="product"\r\n\r\n' + product.encode() +
        b'\r\n--B\r\nContent-Disposition: form-data; name="remark"\r\n\r\n' + remark.encode() +
        b'\r\n--B--\r\n')
req = urllib.request.Request("http://127.0.0.1:3009/pack", data=body, headers={
    "Content-Type": "multipart/form-data; boundary=B",
    "X-Gwpack-Key": KEY, "X-Gwpack-Timestamp": ts,
    "X-Gwpack-Nonce": nonce, "X-Gwpack-Signature": sig})
res = urllib.request.urlopen(req)
print(res.status, len(res.read()), "bytes")

取某设备的包密钥包用 GET /key-package?version=&serial[&product=], 签名串七个字段:方法(GET)、路径(/key-package)、时间戳、 随机数、版本、序列号、产品名(gateway 默认已应用);返回 80 字节 包密钥包,原包密钥绝不返回。

7. 验证 pkg

拿回 pkg 后,用命令行解包验证明文与原 bin 逐字节一致 (签名公钥是服务启动时打印的那个,解密私钥只在设备侧/测试里有; 包密钥包向服务端按版本 + 序列号取,或本地用 wrap-key 包; 只验证不写文件用 verify):

./target/release/sign-tool unpack \
  --pkg gateway-v1.2.3-20261029.pkg \
  --verify-key <签名公钥,64 位十六进制> \
  --decrypt-key <解密私钥,64 位十六进制> \
  --key-package device.keypkg
# 明文写到包旁边(unpack 没有 --out 参数),文件名为包头生成名加 -out.bin;
# 下面把 unpack 输出里写的文件名换进来,与原 bin 逐字节比对
./target/release/sign-tool cmp firmware.bin <包头生成名>-out.bin

改过一个字节的包必须被拒绝,绝不写出错误明文。

常见问题

文档索引