仓库:https://github.com/ffffxxxjjj/sign
后端服务 server/(Rust)的接口说明。机器调用方直调后端
http://127.0.0.1:3009;浏览器走前端 http://127.0.0.1:3000,
前端经代理转发到后端(同源,会话 Cookie 直接可用)。
服务只绑 127.0.0.1;端口由环境变量 PORT 定,默认 3000——
前后端分离部署时后端必须设 PORT=3009,否则与前端的 3000 冲突
(见仓根 README 与 docs/development-environment.md)。
单个请求体上限 16MB。
POST /login,表单字段 password。成功返回 303 跳 /,
并以 Set-Cookie 发会话;失败返回 401,正文 密码错误。admin_session,HttpOnly,SameSite=Lax,路径 /,
有效期 12 小时。登出:POST /logout,清服务端会话与 Cookie,
返回 303 跳 /。unauthorized: login required;
前端收到 401 自己跳登录页。GET /health 不用登录;POST /login 与
POST /operator/login 不用登录;POST /pack
与 GET /key-package 两种鉴权任一通过(见调用凭证);
POST /devices 只走调用凭证(不走会话,见下);
POST /devices/issue 只走操作员会话(不走管理员会话、
不走调用凭证,见下);
GET /firmwares 与 GET /packages/{文件名} 公开,不用登录(首页下载用)。POST /operator/login,JSON {"username", "password"}。
成功返回 200,JSON {"username"},并以 Set-Cookie 发会话;
失败返回 401,正文 invalid username or password(未知用户、
已禁用、密码错三者同文,不区分)。operator_session,HttpOnly,SameSite=Lax,路径 /,
有效期 12 小时(与管理员会话同一时长、不同 Cookie,互不通用:
操作员会话不能调管理员接口,管理员会话不能调分发接口)。POST /operators)与禁用
(POST /operators/{username}/disable);首个操作员也由此创建,
没有单独的引导接口:管理员先用现有管理员登录建操作员,
写入工具再用操作员账号登录。POST /devices/issue;其余接口一律要管理员
会话(除公开与调用凭证接口外)。POST /api-keys),得 key
(gwk_ + 16 位十六进制)与 secret(32 字节随机,十六进制)。
secret 只在新建成功时返回一次,列表永不返回。X-Gwpack-Key(key)、
X-Gwpack-Timestamp(Unix 秒)、X-Gwpack-Nonce(调用方随机
十六进制)、X-Gwpack-Signature(小写十六进制 HMAC-SHA256)。POST /pack 签名串:7 个字段按换行连接,顺序固定:方法(POST)、
路径(/pack)、时间戳、随机数、固件 SHA-256(十六进制)、产品名
(空已默认成 gateway 后的值)、备注。HMAC 的密钥是 secret 本身的
UTF-8 字节。打包是共用的,不再有设备密钥进签名串。
版本不进签名串:版本由服务端从上传固件的版本块读,调用方根本
不发版本;版本仍被签名管着——签名串里有固件 SHA-256,版本写在
固件块里,改版本等于改固件,哈希一变签名就破。GET /key-package 签名串:7 个字段按换行连接,顺序固定:方法
(GET)、路径(/key-package)、时间戳、随机数、版本、序列号、
产品名(空已默认成 gateway 后的值)。POST /devices(写入工具在线登记)用同一套头与校验顺序,签名串
8 个字段按换行连接,顺序固定:方法(POST)、路径(/devices)、
时间戳、随机数、序列号、设备公钥(小写十六进制)、型号、固件版本。
缺任一头返回 401 unauthorized: api key signature required;其余
401 正文与 /pack 相同。各接口的签名不能互换:/pack 的签名不能
用于 /devices 或 /key-package,反之亦然。POST /pack、GET /key-package 与 POST /devices;
其余接口一律要会话,只有 GET /firmwares 与
GET /packages/{文件名} 公开(首页下载用)。操作员会话只用于
POST /devices/issue;管理员会话与调用凭证都不能调它
(审计必须记到操作员本人)。不用登录。返回 200,正文 ok。
POST /login:表单字段 password(必填)。
成功:303 跳 /,并以 Set-Cookie 发会话。
失败:401,正文 密码错误。POST /logout:要会话。清会话与 Cookie,303 跳 /。鉴权:会话或调用凭证任一通过。无会话且缺任一签名头,返回 401
unauthorized: login or api key signature required。
请求:multipart 表单。打包是共用的:一份 bin 只出一份包文件,
所有设备共用;包密钥由服务端每包现生成、按版本存库,不再要
设备密钥出包。旧客户端仍传 device_pub 字段时返回 400
pack takes no device_pub: pack produces a shared package for all devices,
不静默忽略(静默会让旧客户端误以为包仍是按设备的)。
| 字段 | 必填 | 说明 |
|---|---|---|
bin |
是 | 固件文件(二进制)。空文件返回 400 |
product |
否 | 产品名前缀,默认 gateway;最多 32 位,只许字母、数字、-、_;非法返回 400 |
remark |
否 | 备注,默认空串;原样进签名串 |
处理顺序:先解析字段(缺、错 → 400)→ 再鉴权(会话跳过, 机器按校验顺序)→ 读版本块(无有效格式 2 块 → 400)→ 签名打包 → 存包文件 + 写记录 + 存包密钥 → 返回。
成功:200,Content-Type: application/octet-stream,
Content-Disposition: attachment; filename="{文件名}",正文即包字节。
文件名:{产品}-v{版本}-{YYYYMMDD-HHMMSS}.pkg(服务端本地时间;
重名自动加 -2、-3 后缀)。版本、型号、固件名、构建时间全从
固件版本块读,不取请求字段。
失败:400、401、500(见错误表)。
鉴权:会话或调用凭证任一通过。无会话且缺任一签名头,返回 401
unauthorized: login or api key signature required。
请求:查询参数 version(必填,如 1.2.3)、serial(必填,
设备序列号)、product(可选,默认 gateway,走 valid_product
规则)。缺参数或超长返回 400。
处理:按产品 + 版本取最新一条打包记录的包密钥(同一版本多次打包 时取最新),用登记表里该序列号的设备公钥包成 80 字节包密钥包 返回。原包密钥绝不返回,只返回包过的密钥包。
成功:200,Content-Type: application/octet-stream,正文即 80 字节
包密钥包。设备用自己的私钥解出包密钥,再解共用包、验签。
失败:序列号无登记 → 404 no such device;该产品 + 版本无打包
记录 → 404 no such firmware;400、401、500(见错误表)。
包密钥包的下发通道(推送还是设备来取)另定;本接口只提供按需取, 不定推送流程。
要会话。返回 200,JSON 数组(新记录在前,按记录编号倒序)。 只返回元数据,绝不含原包密钥值:
| 字段 | 说明 |
|---|---|
firmware_id |
整数,打包记录编号(与 /firmwares 的 id 对应) |
product |
产品名前缀 |
version |
版本,如 1.2.3 |
key_fingerprint |
密钥指纹:原包密钥 32 字节的 SHA-256,十六进制 |
created_at |
存入时间,服务端本地时间,YYYY-MM-DD HH:MM:SS |
status |
active(该产品 + 版本的最新记录,取包密钥包接口给的就是它)或 superseded(同产品 + 版本的旧打包记录;包文件仍可下载,但新取包密钥包用新密钥) |
要会话。管理后台点眼睛查看原值时调:返回 200,JSON
{"firmware_id", "package_key"},package_key 为原包密钥 64 位
十六进制。页面默认掩码显示,点眼睛调此接口取一次显示一次,
显示后 30 秒自动重新隐藏。记录不存在、或该记录无存入的包密钥
(包密钥存库之前打的包),返回 404 no such package key。
不用登录(首页下载卡直接下载加密包;包本身加密,无设备密钥包解不开)。
成功:200,包字节流,Content-Disposition: attachment。
失败:文件名含 /、\ 或等于 ..,返回 404 no such firmware;
数据库无此记录,返回 404 no such firmware;磁盘文件丢失,
返回 500 stored package file is missing: ...。
不用登录(首页下载卡读网关型号最新版)。返回 200,JSON 数组(新记录在前,按编号倒序)。包密钥不在此响应里(包密钥只走 GET /package-keys,管理员会话 + 掩码)。
记录字段:
| 字段 | 说明 |
|---|---|
id |
整数,记录编号(改备注、删除用它) |
product |
产品名前缀 |
version |
版本,如 1.2.3 |
model |
型号 |
firmware_name |
固件名 |
filename |
包文件名 |
size_bytes |
包字节数 |
sha256 |
包 SHA-256,十六进制 |
device_pub |
目标设备加密公钥,十六进制;共用包记录为空串(包密钥存库之前的按设备打包记录仍显示原值) |
remark |
备注 |
uploaded_at |
上传时间,服务端本地时间,YYYY-MM-DD HH:MM:SS |
要会话。请求:表单字段 remark(必填)。
成功:303 跳 /。编号不存在:404 no such firmware。
GET 调此路径:405。
要会话。无请求体。成功:删除记录与其包文件,303 跳 /。
编号不存在:404 no such firmware。GET 调此路径:405。
要会话。请求:JSON {"name": "..."},名称去首尾空白后 1–64 位;
空或超长返回 400 bad name: use 1-64 characters。
成功:200,JSON {"key_id", "secret"};secret 只返回这一次。
要会话。返回 200,JSON 数组(新建在前)。
字段:key_id、name、created_at、last_used_at(未用过为
null)、revoked(是否已吊销)。永不含 secret。
要会话。无请求体(表单提交亦可)。成功:吊销(只改标志,
记录保留备查),303 跳 /。key 不存在:404 no such api key。
吊销不可恢复。
要会话(管理员)。请求:JSON {"username", "password"},用户名去
首尾空白后 1–64 位;密码至少 8 位,否则 400。密码经 Argon2id
哈希后存库,明文永不落盘。成功:200,JSON {"username",
"disabled": false, "created_at"}(永不含密码或哈希)。用户名已存在:
400 operator already exists。每次创建记一条审计(动作
operator_create,操作员栏为 admin)。
要会话(管理员)。无请求体。成功:200,JSON {"username",
"disabled": true, "created_at"},同时清掉该操作员的全部在用会话
(禁用立即生效,不等会话过期)。用户名不存在:404
no such operator。每次禁用记一条审计(动作 operator_disable,
操作员栏为 admin)。本版本没有恢复接口。
要会话。请求:JSON {"count": N},count 须为 1–1000 的整数,
否则 400 bad count: use 1-1000。成功:200,JSON {"serials": [...]}。
序列号格式 GW{YYYYMMDD}{序号:04}(按天从 0001 起,服务端本地日期);
领出即标记已领出,同一序列号只能登记一次。管理台只有列表与领号,
没有手工登记设备的表单——登记只接受写入工具在线上报。
不走会话,只走调用凭证(见鉴权一节)。请求:JSON
{"serial", "device_pub", "model", "firmware_version"}。
serial 1–64 位;device_pub 须为 64 位十六进制(即设备公钥的
32 字节十六进制,不回显提交值);model 1–64 位;
firmware_version 1–32 位;长度非法返回 400。
序列号不在已领出批次里:400 serial not allocated;已登记过:
400 serial already registered。成功:200,设备记录 JSON。
登记方式固定为 writer(写入工具在线上报)。
只走操作员会话(不走管理员会话、不走调用凭证;无有效操作员
会话返回 401 unauthorized: operator login required)。请求:JSON
{"serial", "model", "product"?, "reissue"?}。serial 1–64 位;
model 1–64 位;product 可选,走 valid_product 规则(默认
gateway,仅接受、不存库);reissue 可选布尔,默认 false。
后端现生成一对设备 X25519 密钥,绑定序列号存库(公钥与私钥
都存),返回给写入工具。成功:200,JSON {"serial", "model",
"device_pub", "device_priv", "issued_at", "issued_by"},公私钥均为
64 位十六进制;issued_at 为本次分发时间(服务端本地时间),
issued_by 为分发的操作员用户名。私钥只在这里返回:设备列表、
包密钥包、审计里绝不出现。
序列号不在已领出批次里:400 serial not allocated;已绑定过且
未传 reissue: true:400 serial already issued: pass reissue=true to re-deliver stored keys;
传了 reissue: true:返回已存的那对密钥(不轮换,设备仍用
一次性可编程存储区里的旧密钥),并记 reissue 审计;已登记但
无后端存的私钥(旧写入工具上报流程登记的):400
serial registered without backend-issued keys: no stored private key to re-deliver;
已领出但无设备记录(异常残留):400 serial already registered。
每次分发与重复分发都记一条审计(动作 issue/reissue,
操作员栏为分发的操作员)。
要会话。返回 200,JSON 数组(新登记在前,按编号倒序)。
记录字段:
| 字段 | 说明 |
|---|---|
id |
整数,记录编号 |
serial |
序列号 |
model |
型号 |
device_pub |
设备公钥,十六进制 |
online |
是否在线;由设备状态上报更新(从未上报过为 false) |
firmware_version |
当前固件版本;登记时上报,之后由状态上报更新 |
latest_version |
该型号最新共用包版本;该型号无共用包时为空 |
registered_at |
登记时间,服务端本地时间,YYYY-MM-DD HH:MM:SS |
register_method |
登记方式:writer(写入工具在线上报)或 issue(后台分发) |
要会话。成功:200,设备记录 JSON。序列号无记录:404 no such device。
要会话(只走会话,不走调用凭证:推送固件是操作员动作,
不是机器打包调用)。向一台设备发升级命令。请求:JSON
{"version": "..."}(可选;不传时取该设备型号的最新共用包)。
版本按型号解析,不按产品:推送面向已登记的设备,该设备的
型号决定它拿哪个包(与 /key-package 的产品 + 版本维区分开)。
服务端按该设备登记的公钥把该版本的包密钥包成 80 字节
(与 /key-package 同一套包逻辑),经 MQTT 发 firmware_update
命令到该设备的 cmd 话题(QoS 1、不保留),params 含
version、pkg_url、key_package(80 字节包密钥包的十六进制,
160 字符)。成功:200,JSON:
| 字段 | 说明 |
|---|---|
serial |
序列号 |
model |
设备型号 |
version |
实际推送的版本(解析后的版本) |
filename |
共用包文件名 |
cmd_id |
本次命令编号(push- + 16 位十六进制),设备回执里的 cmd_id 与此配对 |
topic |
发往的 cmd 话题 |
pkg_url |
共用包下载地址({公开地址}/packages/{文件名}) |
响应里不含 key_package 本身(设备经 MQTT 收到即可)。
单个推送总是发送,即使设备当前已是目标版本(操作员明确
点了这台设备)。回执不等待:命令发出即返回;命令先记库
再发布(发布失败则删记录,不留幽灵待回执),回执状态经
GET /devices/push/{cmd_id} 查询(见“回执配对跟踪”一节)。
请求头 Idempotency-Key(可选):同一键重发返回上次响应,
不再重复推送(重试安全)。序列号无登记:404 no such device;
指定版本无该型号的包:404 no such firmware;未指定版本且
该型号无共用包:404 no firmware for this model。
要会话(只走会话,同单个推送)。按筛选条件圈设备、逐台发 升级命令。请求:JSON(全部可选):
| 字段 | 说明 |
|---|---|
version |
目标版本;不传时每台取各自型号的最新共用包 |
model |
型号,精确匹配 |
online |
在线状态,true 只推在线设备、false 只推离线设备 |
only_outdated |
为 true 时跳过已是目标版本的设备(记为 skipped,不是失败) |
单次最多 500 台;匹配超限返回 400
batch too large: {匹配数} devices match, narrow filters (max 500),
调用方须收窄条件再调。一台设备出问题不中断整批:该台记为
failed 并带原因,其余照常发出。成功:200,JSON:
| 字段 | 说明 |
|---|---|
version |
请求的目标版本;未指定为 null(表示按型号最新) |
batch_id |
本批编号(batch- + 16 位十六进制),本批所有命令共享,供批量回执查询用 |
matched |
匹配的设备数 |
pushed |
成功发出的台数 |
failed |
失败的台数 |
skipped |
因已是目标版本而跳过的台数(仅 only_outdated 为真时出现) |
results |
逐台结果数组:{serial, status, cmd_id?, version?, error?};status 为 pushed / failed / skipped,skipped 的 error 写跳过原因(already on target version) |
每台推送的命令先记库再发布(同单个推送),共享本批的
batch_id;回执状态经 GET /devices/push-batch/{batch_id}
查询(见“回执配对跟踪”一节)。请求头 Idempotency-Key
(可选):同一键重发返回上次整批响应,不再重复推送。
服务端订阅 v1/tenants/+/devices/+/cmd_ack(QoS 1),按
cmd_id 把设备回执与已发出的推送命令配对。回执不含密钥
材料:入库前删掉键名含 key_package、package_key、
private、device_priv、secret、password、token、
seed 的字段与 key 字段本身,回执数据另限 4096 字符;
命令记录本身也
不存包密钥包(只存公开下载地址与版本)。配对规则:
cmd_id 须是本服务端发过的命令,且话题里的
序列号须与该命令发往的序列号一致,否则跳过(不配对到
错的设备上);载荷里的 device_id 与话题不一致时同样
跳过;未知 cmd_id 静默忽略;malformed 载荷与缺 cmd_id
的回执只记日志、不改库。timeout 纠正为
acked(设备确实回了,只是晚了;超时标记是临时的)。clean_session=false):Broker 会把
服务端离线期间发布的 QoS 1 回执排队,重连后重发补上;
服务端每次连接都重新订阅(幂等),Broker 重启丢会话
时也靠重订阅恢复。重发 duplicates 靠“首个回执为准”
消化,不会产生重复记录。要会话。成功:200,命令记录 JSON:
| 字段 | 说明 |
|---|---|
cmd_id |
命令编号 |
batch_id |
所属批编号;单个推送为空串 |
serial |
发往的序列号 |
version |
推送的版本 |
pkg_url |
共用包下载地址 |
sent_at |
发出时间(Unix 秒) |
ack_status |
pending(等待回执)/ acked(已回执)/ timeout(超时未回执) |
ack_ok |
回执的 ok;未回执为 null |
ack_message |
回执的 message;未回执为空串 |
ack_data |
回执的 data(已脱敏,见上);未回执为空串 |
acked_at |
回执到达时间(Unix 秒);未回执为 null |
超时:发出后 SIGN_SERVER_PUSH_ACK_TIMEOUT_SECS 秒(默认
300)内无回执,查询时即读为 timeout(查询前先落盘标记,
读与存一致)。超时不是“设备升级失败”:超时只说明该窗口
内没收到回执,设备是否装上以其后的状态上报
(firmware_version)为准;操作员可重推。未知 cmd_id:
404 no such push command;cmd_id 为空或超 128 字符:400。
要会话。成功:200,JSON:
| 字段 | 说明 |
|---|---|
batch_id |
批编号 |
total |
本批命令数 |
acked |
已回执的台数 |
timeout |
超时的台数 |
pending |
仍在等待的台数 |
results |
逐台命令记录(同上表结构,按发出顺序) |
无该批命令(未发过该批,或该批一台也没发出):404
no such push batch;batch_id 为空或超 128 字符:400。
已终态(acked / timeout)的命令记录与幂等键在
SIGN_SERVER_PUSH_RETENTION_DAYS 天(默认 30)后裁剪;
裁剪在每次推送记库与每次回执查询时顺带做,不另起后台
任务。待回执行不直接裁(先经超时标记成终态,再裁),
所以表不会随推送无限涨;单批查询最多 500 行(批上限),
单条查询 1 行。
推送与状态接入只在配置了 MQTT 时启用(明文 1883 先行, TLS 后做)。环境变量:
| 变量 | 说明 |
|---|---|
SIGN_SERVER_MQTT_USERNAME |
云端账号(必填,缺了则 MQTT 整体关闭:推送返回 503,不跑订阅) |
SIGN_SERVER_MQTT_PASSWORD |
云端账号密码 |
SIGN_SERVER_MQTT_BROKER |
Broker 地址,默认 127.0.0.1 |
SIGN_SERVER_MQTT_PORT |
Broker 端口,默认 1883 |
SIGN_SERVER_MQTT_CLIENT_ID |
客户端编号,默认 sign-server |
SIGN_SERVER_MQTT_TENANT |
话题里的租户段,默认 default;设备表无租户列,须与设备端连接的租户一致 |
SIGN_SERVER_PUBLIC_BASE_URL |
推送命令里 pkg_url 的公开地址前缀;缺了则推送返回 500(无可达下载地址的命令会把设备晾住,不发) |
SIGN_SERVER_PUSH_ACK_TIMEOUT_SECS |
回执超时窗口(秒),默认 300;非法或零回落 300 |
SIGN_SERVER_PUSH_RETENTION_DAYS |
回执记录与幂等键保留天数,默认 30;非法或零回落 30 |
状态接入:服务端订阅 v1/tenants/+/devices/+/status(QoS 1),
收到状态后以话题里的设备段为序列号,更新该设备记录的
online 与 firmware_version。未登记的序列号静默忽略;
载荷里的 device_id 与话题不一致时跳过更新(不写到错的
设备上);malformed 载荷只记日志、不改库。重连后自动重新
订阅。设备列表的 latest_version 由服务端在返回时按型号
现算(该型号最新共用包版本),不存库。
回执接入:服务端同时订阅 v1/tenants/+/devices/+/cmd_ack
(QoS 1),配对规则见“回执配对跟踪”一节;订阅会话是持久
的(clean_session=false),离线期间的回执由 Broker 排队、
重连后重发补上。推送失败关闭:未配 MQTT 返回 503
MQTT not configured;已配但 Broker 不可达(连接未建立)
同样返回 503 MQTT broker not connected,不把命令收进
断开的连接里静默丢掉;恢复连接后推送自动恢复。
前端首页 /(免登录,大气简约落地页):置顶细栏头部
([Logo] 产品 | 下载 | 支持 | 关于 | 联系,Logo 点回 /,
头部不放登录链接、不做搜索);首屏满视口高度,产品名 + 一句
短标语 + 两个按钮(“了解产品”→#product,“下载最新版”→
#downloads),首屏只用文字排版(仓里无真实网关产品照片,
不用假照片);产品区(#product)只放注册过的型号(stm32h743vi /
gateway,见 docs/models.md);下载区(#downloads)恰好两张
精选卡,只放最新版,不列目录、不放旧版:固件卡是打包加密后的
pkg 包(走 GET /firmwares 取网关型号最新版,走 GET /packages/{文件名}
直接下载,免登录,绝不是原始固件),写入工具卡是最新
firmware-tool 发布版(链到 https://github.com/ffffxxxjjj/firmware-tool/releases,
版本、日期、大小只在发布数据有时显示,校验值发布数据里没有就省略,
无发布版时只显示提示语,不报错);下载区另有一句安静的
“旧版与手册 →”链到支持条(归档放支持条,不在首页列)。
支持条是安静的支持链接条;页脚有导航链接 + 管理登录链接(→/login)+
版权,管理登录只放页脚,不占主导航。
管理员登录页在 /login(以前在 /,行为不变:输入清错、
关闭按钮、零位移;登录成功跳 /dashboard,401 跳 /login)。
后端 POST /login 成功仍 303 跳 /(前端 fetch 跟随,拿到 ok 后
跳 /dashboard)。旧 /download 只跳转到 /#downloads,旧链接不断。
管理台头部有 Logo(与首页头部同一文字 Logo),点它跳回首页 /。
发布原始固件已删:管理台不再有“发布初始固件”区,
POST /published、GET /published、GET /published/{文件名}
三接口已删(不单独做发布页;发布走管理台打包,即 POST /pack)。
静态导出部署(STATIC_EXPORT=1)时前端 rewrites 被忽略,OpenResty
须保证以下路径代理到后端:/key-package、/package-keys(及其子路径)、
/devices(及其子路径)、/firmwares、/packages(及其子路径)、
/api-keys(及其子路径);部署本身已冻结,只记规则、不改部署。
| 正文 | 触发条件 |
|---|---|
bad multipart form: ... |
multipart 解析失败 |
cannot read bin file: ... |
读 bin 字段失败 |
cannot read device_pub: ... |
读 device_pub 字段失败(字段出现即拒绝,不读值) |
cannot read product: ... |
读 product 字段失败 |
cannot read remark: ... |
读 remark 字段失败 |
missing bin file |
没传 bin |
bin file is empty |
bin 为空文件 |
pack takes no device_pub: pack produces a shared package for all devices |
/pack 传了 device_pub 字段 |
missing version |
取包密钥包没传 version |
missing serial |
取包密钥包没传 serial |
bad version: use up to 32 characters |
取包密钥包的 version 超长 |
bad serial: use up to 64 characters |
取包密钥包的 serial 超长 |
bad product: use up to 32 letters, digits, '-' or '_' |
product 非法 |
firmware has no version block at bin offset 0x1000: build firmware with the version block |
固件无有效格式 2 版本块(另有版本块本身的校验报错,见包格式文档) |
bad name: use 1-64 characters |
新建凭证的名称空或超长 |
bad count: use 1-1000 |
领号数量不在 1–1000 内 |
bad serial: use 1-64 characters |
登记的序列号空或超长 |
bad model: use 1-64 characters |
登记的型号空或超长 |
bad firmware_version: use 1-32 characters |
登记的固件版本空或超长 |
serial not allocated |
登记的序列号不在已领出批次里 |
serial already registered |
序列号已登记过 |
batch too large: ... (max 500) |
批量推送匹配的设备超 500 台,须收窄筛选条件 |
bad username: use 1-64 characters |
操作员用户名空或超长 |
bad password: use at least 8 characters |
操作员密码短于 8 位 |
operator already exists |
操作员用户名已存在 |
serial already issued: pass reissue=true to re-deliver stored keys |
序列号已分发过,未传 reissue: true |
serial not issued: issue without reissue first |
序列号未分发过却传了 reissue: true |
serial registered without backend-issued keys: no stored private key to re-deliver |
序列号已登记但无后端存的私钥可重发 |
| 正文 | 触发条件 |
|---|---|
unauthorized: login required |
非 GET 请求无有效会话(/pack 与 GET /key-package 除外,见下) |
unauthorized: login or api key signature required |
/pack 或 GET /key-package 无会话且缺任一签名头 |
unauthorized: api key signature required |
POST /devices 缺任一签名头 |
unauthorized: unknown api key |
签名头的 key 不存在 |
unauthorized: api key revoked |
key 已吊销 |
unauthorized: bad timestamp |
时间戳不是数字 |
unauthorized: timestamp outside window |
时间戳与服务端时间相差超 300 秒 |
unauthorized: bad signature |
签名不对 |
unauthorized: nonce replay |
随机数已用过 |
unauthorized: operator login required |
POST /devices/issue 无有效操作员会话(管理员会话与调用凭证均无效) |
invalid username or password |
操作员登录失败(未知用户、已禁用、密码错三者同文) |
POST /login 密码错也返回 401,正文是 密码错误 纯文本。
POST /operator/login 失败也返回 401,正文是
invalid username or password。
| 正文 | 触发条件 |
|---|---|
no such firmware |
下载的文件名无记录(含 /、\、.. 的文件名);改备注、删除的编号不存在;取包密钥包的产品 + 版本无打包记录 |
no such api key |
吊销的 key 不存在 |
no such device |
查的序列号无设备记录;取包密钥包的序列号无登记;推送的序列号无登记 |
no firmware for this model |
推送未指定版本、且该型号无共用包 |
no such package key |
查的包密钥记录不存在,或该记录无存入的包密钥 |
no such operator |
禁用的操作员不存在 |
no such push command |
查的 cmd_id 无推送命令记录 |
no such push batch |
查的 batch_id 下无推送命令 |
POST 专用的路径(改备注、删除、吊销)用 GET 调,返回 405。
| 正文 | 触发条件 |
|---|---|
MQTT not configured |
推送时未配置 MQTT(SIGN_SERVER_MQTT_USERNAME 未设) |
MQTT broker not connected |
推送时已配 MQTT 但 Broker 连接未建立(断线中) |
MQTT publish failed: ... |
推送时 MQTT 发布失败 |
database lock failed(数据库锁失败)、cannot create packages dir: ...、
cannot store package file: ...、stored package file is missing: ...、
打包失败(签名打包库的报错原样返回)、cannot create api key: ...、
cannot list api keys: ...、
push requires SIGN_SERVER_PUBLIC_BASE_URL to be configured(推送时
未配公开地址)、stored device public key is malformed(登记的设备
公钥已损坏)、cannot wrap package key: ... 等。500 说明服务端
或数据有问题,不是调用方传错。
下面地址直调后端 127.0.0.1:3009。firmware.bin 是待打包的固件
(须含格式 2 版本块)。取包密钥包的例子用已登记的测试设备序列号
(先领号、再用固定测试向量公钥
493e82fc74464a59268817623d2053c5eb8e2cc4a988b4fee179ec6b010d531d
登记,见 POST /devices 一节)。
# 1. 登录,存会话 Cookie(密码换成自己的管理员密码)
curl -c cookies.txt -d 'password=你的管理员密码' http://127.0.0.1:3009/login
# 2. 上传打包,包字节存为 out.pkg(服务端文件名见响应头 Content-Disposition)
# 打包是共用的,不再传 device_pub;包密钥由服务端按版本存库
curl -b cookies.txt -F bin=@firmware.bin \
-F product=gateway -F remark=测试 -o out.pkg http://127.0.0.1:3009/pack
# 3. 列记录(JSON)
curl -b cookies.txt http://127.0.0.1:3009/firmwares
# 4. 列包密钥记录(JSON,只有元数据:版本、指纹、创建时间、状态)
curl -b cookies.txt http://127.0.0.1:3009/package-keys
# 5. 取某设备的包密钥包(80 字节;序列号换成已登记的)
curl -b cookies.txt -o device.keypkg \
'http://127.0.0.1:3009/key-package?version=1.2.3&serial=GW202611020001&product=gateway'
# 6. 点眼睛查看某条记录的原包密钥(编号换成 package-keys 里的 firmware_id)
curl -b cookies.txt http://127.0.0.1:3009/package-keys/1
# 7. 下载(文件名换成记录里的 filename)
curl -b cookies.txt -o down.pkg http://127.0.0.1:3009/packages/gateway-v1.2.3-20261101-201016.pkg
# 8. 改备注(编号换成记录里的 id)
curl -b cookies.txt -d 'remark=新备注' http://127.0.0.1:3009/firmwares/1/remark
# 9. 删除(连带删除该记录的包密钥)
curl -b cookies.txt -X POST http://127.0.0.1:3009/firmwares/1/delete
# 10. 登出
curl -b cookies.txt -X POST http://127.0.0.1:3009/logout
机器调用方取包密钥包时按 GET /key-package 7 字段签名串算
HMAC-SHA256 签名(见鉴权一节),带四个头调同一地址;会话流程
的第 5 步即不签名的等价形式。
# 1. 管理员登录(存管理员会话 Cookie)
curl -c admin.txt -d 'password=你的管理员密码' http://127.0.0.1:3009/login
# 2. 管理员创建操作员(密码换成自己的操作员密码,至少 8 位)
curl -b admin.txt -H 'Content-Type: application/json' \
-d '{"username": "op1", "password": "你的操作员密码"}' \
http://127.0.0.1:3009/operators
# 3. 管理员领号(序列号换成返回里的值)
curl -b admin.txt -H 'Content-Type: application/json' \
-d '{"count": 1}' http://127.0.0.1:3009/devices/allocate
# 4. 操作员登录(存操作员会话 Cookie;管理员 Cookie 不能调分发)
curl -c op.txt -H 'Content-Type: application/json' \
-d '{"username": "op1", "password": "你的操作员密码"}' \
http://127.0.0.1:3009/operator/login
# 5. 操作员分发(响应含公钥与私钥;私钥只在这里返回,复制后交给写入工具)
curl -b op.txt -H 'Content-Type: application/json' \
-d '{"serial": "GW202611020001", "model": "GW-100"}' \
http://127.0.0.1:3009/devices/issue
# 6. 未传 reissue 重发:400;传 reissue 重发:200,返回已存的那对密钥
curl -b op.txt -H 'Content-Type: application/json' \
-d '{"serial": "GW202611020001", "model": "GW-100", "reissue": true}' \
http://127.0.0.1:3009/devices/issue
# 7. 管理员禁用操作员(之后操作员登录与旧会话都失效)
curl -b admin.txt -X POST http://127.0.0.1:3009/operators/op1/disable
完整可跑的程序见 tool/examples/machine_pack.rs(仓里同一份,
文档不另贴副本防漂移):读真实固件文件,按 7 字段签名串算
HMAC-SHA256 签名,multipart 上传打包,存为服务端返回的文件名。
在仓根跑(服务已起在 3009、凭证已在管理页分配;产品、备注可省,
默认 gateway、空备注):
cargo run --release --example machine_pack -- \
http://127.0.0.1:3009 <分配的key> <分配的secret> firmware.bin
完整可跑的程序见 tool/native/java/GwpackServiceDemo.java(仓里同一份,
文档不另贴副本防漂移;直调 HTTP,不用原生库)。
原生库调法见仓根 README 原生库一节(configure/pack/
last_filename)。
种子、管理员密码、数据目录、端口的完整说明见
server/README.md(运行)与 docs/development-environment.md。
启动缺种子或密码拒绝启动;服务只绑 127.0.0.1,
上服务器走反向代理或隧道,不得直暴露公网。