sign-server

仓库:https://github.com/ffffxxxjjj/sign

Rust 签名打包网页服务(GWPK 包格式 v3),服务端做的测试版: 网页上传固件 bin,服务端用服务端持有的云端签名密钥签名打包, 保存包文件与一条固件记录(SQLite),返回 pkg 文件下载; 固件管理页可查看记录、下载包文件、改备注、删除记录与文件。 只管签名打包;机器调用方(Java 后台等)调 它的 POST /pack,用分配的调用凭证签名请求(见下“调用凭证”), 不用管理员登录。

打包逻辑复用本仓 tool/ 的 sign-tool 库(同一 workspace 的 tool 成员, 同一份代码,设备侧已交叉验证),本服务只负责通过网页暴露它 + 服务端持有签名密钥。

私钥处理铁律:云端签名私钥种子只以种子文件(600 权限)或注入的 环境变量进服务进程内存,绝不进数据库、不进日志、不出现在页面, 不上命令行。数据库存公开信息、目标设备加密公钥(解密私钥从不出设备), 外加机器调用凭证的密钥——密钥必须能取回才能验签,所以数据库文件 本身按 600 权限保护,视同种子文件;密钥绝不进日志,列表接口也 绝不返回密钥(只在新建成功时显示一次)。

运行(Mac 测试,前后端分离)

种子怎么来

本地测试自己生成一个新鲜种子,不要用别人给的、不要复用生产种子。 在仓根跑,工具打印测试密钥,取 sign key (seed, private key) 那行 (64 位十六进制,行内代码点一下就复制):

cargo run --release -p sign-tool -- gen-keys

这几行是测试密钥(工具自己标明切勿用于量产),仅本地测试用; 量产种子上服务器时按量产流程在服务器本机生成,现在不用管。

写种子与密码文件(逐行跑)

下面 4 条逐行跑,不能整段粘贴:带 read 的行会停住等输入, 整段粘贴时第一个 read 会把下一条当成输入吃掉,写出的文件就是空的。 read -s 是读一行且不回显(种子、密码不显示、不进命令历史)。 行内代码点一下就复制。文件名任意,环境变量指对即可。

  1. read -s seed:这行停住后粘贴种子,按回车。
  2. printf '%s' "$seed" > seed.txt && chmod 600 seed.txt && unset seed
  3. read -s admin_password:这行停住后粘贴管理员密码,按回车。
  4. printf '%s' "$admin_password" > admin-password.txt && chmod 600 admin-password.txt && unset admin_password

验一下(种子应是 64,密码是它的长度;跑完上面两条变量已清掉, 再 echo ${#seed} 显示 0 属正常,以文件为准):

wc -c seed.txt admin-password.txt

起后端与前端(整段粘贴)

下面两条不含 read,可以整段粘贴。在仓根跑后端 (-p 指定 workspace 成员,端口 3009):

PORT=3009 SIGN_SERVER_SIGNING_SEED_FILE=seed.txt SIGN_SERVER_ADMIN_PASSWORD_FILE=admin-password.txt cargo run --release -p sign-server

再起前端(端口 3000;先装依赖):

cd server/web && npm install && npm run dev

打开 http://127.0.0.1:3000/(首页),点页脚管理登录(/login)登录后进管理台,上传 bin 点打包。

本地调试

生产部署(以后上服务器时):web 端放 OpenResty 里面,直接出静态(见 docs/openresty-deployment.md)。开发用 npm run dev 直接跑;npm run start 只用于本地验证生产构建。

测试用固定测试密钥见 docs/package-format.md 第 6 节 (云端签名私钥种子、设备加密公钥、解密私钥、云端签名公钥), 仅测试用,绝不用于量产。启动时会打印云端签名公钥, 验包时用它 + 解密私钥跑 sign-tool verify 交叉验证。

管理员登录

管理页与全部接口(除存活检查外)都要先登录。单管理员、只设一个 密码,不设用户名。

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

管理员在管理页“调用凭证”一节给调用方分配凭证:填个名称点新建, 服务生成 key(gwk_ + 16 位十六进制)与 secret(32 字节随机, 十六进制),secret 只在新建成功时弹窗显示一次,复制后保存,列表里 不再显示;列表可查看各凭证的名称、创建时间、最近使用、吊销。 吊销后该凭证立即失效,无法恢复。

机器请求带四个头:X-Gwpack-Key(key)、 X-Gwpack-Timestamp(Unix 秒)、X-Gwpack-Nonce(调用方随机 十六进制)、X-Gwpack-Signature(小写十六进制 HMAC-SHA256)。 POST /pack 签名串是七个字段按 \n 连接、顺序固定:方法、路径、 时间戳、随机数、固件 SHA-256(十六进制)、产品名(gateway 默认 已应用)、备注;HMAC 的密钥是 secret 本身的 UTF-8 字节。打包是 共用的,不再有设备密钥进签名串。GET /key-package 签名串也是 七个字段:方法、路径、时间戳、随机数、版本、序列号、产品名 (gateway 默认已应用)。版本不进 /pack 签名串:版本由服务端 从上传固件的版本块读,调用方根本不发版本;版本仍被签名管着—— 签名串里有固件 SHA-256,版本写在固件块里,改版本等于改固件, 哈希一变签名就破。服务端验签外还查:时间戳与服务端时间 相差 300 秒内、随机数未用过(重放直接 401)。任何语言加载本仓 的 .so / .dll 就能调完整流程(库里已封装签名),不用自己写 HTTP 客户端;手写客户端按上面这套签名即可。

操作员(写入工具,不用管理员登录)

管理员在管理页(或直调接口)创建操作员账号:用户名 1–64 位、 密码至少 8 位,密码经 Argon2id 哈希后存库,明文永不落盘; 首个操作员也由此创建,没有单独的引导接口。写入工具用操作员 账号登录(POST /operator/login,JSON 用户名 + 密码),成功发 operator_session 会话 Cookie(HttpOnly、12 小时),与管理员 会话互不通用:操作员会话不能调管理员接口,管理员会话不能调 分发接口;登录失败一律 401 invalid username or password (未知用户、已禁用、密码错三者同文,不区分)。

分发(POST /devices/issue)只走操作员会话:序列号须在已领出 批次里;首次分发后端现生成一对设备 X25519 密钥,绑定序列号 存库(公钥与私钥都存),返回给写入工具;重复分发须显式传 reissue: true,返回已存的那对密钥(不轮换、不静默换密钥)。 私钥只在分发响应里出现:设备列表、包密钥包、审计里绝不出现。 每次分发、重复分发、操作员创建、操作员禁用都记一条审计 (时间、操作员、动作、序列号、详情)。管理员禁用操作员后, 该操作员的全部在用会话立即失效;本版本没有恢复接口。

接口

除 GET /health 外,管理接口都要先登录(未登录返回 401,前端收到 401 跳登录页)。例外:POST /pack 与 GET /key-package 浏览器管理页用会话 Cookie,机器调用方用上面的调用凭证签名,二者任一通过即可; POST /devices 只走调用凭证;POST /operator/login 不用登录; POST /devices/issue 只走操作员会话(不走管理员会话、不走调用凭证); POST /devices/{序列号}/push 与 POST /devices/push-batch 只走会话,不走调用凭证; GET /firmwares 与 GET /packages/{文件名} 公开(首页下载用)。

接口全量(参数表、返回表、错误表、调用示例)见 ../docs/sign-server-api.md。

固件记录字段:产品、版本、型号、固件名、文件名、包大小、包指纹(SHA-256)、 目标设备加密公钥(共用包记录为空串)、备注、上传时间。固件记录里没有私钥字段; 包密钥另存在包密钥表里,只给后端包密钥包与管理员点眼睛查看用, 列表接口只给指纹不给原值。

测试

单测:

cd sign
cargo test -p sign-server

覆盖:文件名重名加后缀、记录增删改查往返、产品名前缀校验、 页面转义、种子文件加载(600 文件通过、644 文件拒绝、坏内容拒绝)、 调用凭证新建/列表/吊销往返、请求验签(通过、错密钥、固件被改、 时间戳过期、未知 key、已吊销、重放)。

端到端用 curl 测(单测 cargo test --workspace 全过): 起服务后上传一个 bin,拿回共用包;按版本 + 已登记序列号取包密钥包, 再用 sign-tool unpack(云端签名公钥 + 解密私钥 + 包密钥包文件) 写出明文,以 sign-tool cmp 与原 bin 比对一致; 再测列表页出现记录、包密钥记录出现指纹、下载包文件、改备注、 删除记录与文件(连带删除包密钥)。

回执配对单测(cargo test --workspace 全过:服务端 38 项、工具库 27 项、 命令行 3 项;服务端原 26 项 + 新增 12 项):命令记库往返、重复回执首胜、 超时后迟回执纠正、超时标记与派生状态、回执脱敏与截断、裁剪保待办、 批量顺序、幂等存取与裁剪;回执话题解析、配对与忽略、串号不匹配与重复、 回执脱敏。

回执配对故障注入与 soak(真实服务端二进制 + 迷你 MQTT Broker,5 台假设备, 回执超时测试值 5 秒):单台回执配对正确;静默设备 5 秒窗后标 timeout; 重复回执首个为准(后到的不覆盖);回执数据脱敏后只剩 {"version":"9.9.9"}; 批量 5 台全推,汇总 total 5、acked 3、timeout 2、pending 0,逐台与预期一致; 同 Idempotency-Key 重放返回逐字节相同响应、设备只多收 1 条命令; Broker 挂掉时推送 503 失败关闭,重启后重订阅恢复、推送与回执正常; 服务端重启后离线期回执经持久会话排队补回(catchup 配对成功)。 Soak 300 秒:592 轮推送 + 回执轮询,丢失 0 轮;内存 RSS 11888KB → 6512KB (增长 -5376KB,无增长趋势);命令记录行 12 → 604,增量 592 与轮数一致、无重复行。