sign-server 接口文档

仓库: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。

鉴权

会话(浏览器管理台)

操作员会话(写入工具)

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

端点

GET /health

不用登录。返回 200,正文 ok。

POST /login、POST /logout

POST /pack

鉴权:会话或调用凭证任一通过。无会话且缺任一签名头,返回 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(见错误表)。

GET /key-package

鉴权:会话或调用凭证任一通过。无会话且缺任一签名头,返回 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(见错误表)。

包密钥包的下发通道(推送还是设备来取)另定;本接口只提供按需取, 不定推送流程。

GET /package-keys

要会话。返回 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(同产品 + 版本的旧打包记录;包文件仍可下载,但新取包密钥包用新密钥)

GET /package-keys/{记录编号}

要会话。管理后台点眼睛查看原值时调:返回 200,JSON {"firmware_id", "package_key"},package_key 为原包密钥 64 位 十六进制。页面默认掩码显示,点眼睛调此接口取一次显示一次, 显示后 30 秒自动重新隐藏。记录不存在、或该记录无存入的包密钥 (包密钥存库之前打的包),返回 404 no such package key。

GET /packages/{文件名}

不用登录(首页下载卡直接下载加密包;包本身加密,无设备密钥包解不开)。

成功:200,包字节流,Content-Disposition: attachment。

失败:文件名含 /、\ 或等于 ..,返回 404 no such firmware; 数据库无此记录,返回 404 no such firmware;磁盘文件丢失, 返回 500 stored package file is missing: ...。

GET /firmwares

不用登录(首页下载卡读网关型号最新版)。返回 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

POST /firmwares/{编号}/remark

要会话。请求:表单字段 remark(必填)。 成功:303 跳 /。编号不存在:404 no such firmware。 GET 调此路径:405。

POST /firmwares/{编号}/delete

要会话。无请求体。成功:删除记录与其包文件,303 跳 /。 编号不存在:404 no such firmware。GET 调此路径:405。

POST /api-keys

要会话。请求:JSON {"name": "..."},名称去首尾空白后 1–64 位; 空或超长返回 400 bad name: use 1-64 characters。 成功:200,JSON {"key_id", "secret"};secret 只返回这一次。

GET /api-keys

要会话。返回 200,JSON 数组(新建在前)。

字段:key_id、name、created_at、last_used_at(未用过为 null)、revoked(是否已吊销)。永不含 secret。

POST /api-keys/{key}/revoke

要会话。无请求体(表单提交亦可)。成功:吊销(只改标志, 记录保留备查),303 跳 /。key 不存在:404 no such api key。 吊销不可恢复。

POST /operators

要会话(管理员)。请求:JSON {"username", "password"},用户名去 首尾空白后 1–64 位;密码至少 8 位,否则 400。密码经 Argon2id 哈希后存库,明文永不落盘。成功:200,JSON {"username", "disabled": false, "created_at"}(永不含密码或哈希)。用户名已存在: 400 operator already exists。每次创建记一条审计(动作 operator_create,操作员栏为 admin)。

POST /operators/{username}/disable

要会话(管理员)。无请求体。成功:200,JSON {"username", "disabled": true, "created_at"},同时清掉该操作员的全部在用会话 (禁用立即生效,不等会话过期)。用户名不存在:404 no such operator。每次禁用记一条审计(动作 operator_disable, 操作员栏为 admin)。本版本没有恢复接口。

POST /devices/allocate

要会话。请求:JSON {"count": N},count 须为 1–1000 的整数, 否则 400 bad count: use 1-1000。成功:200,JSON {"serials": [...]}。 序列号格式 GW{YYYYMMDD}{序号:04}(按天从 0001 起,服务端本地日期); 领出即标记已领出,同一序列号只能登记一次。管理台只有列表与领号, 没有手工登记设备的表单——登记只接受写入工具在线上报。

POST /devices

不走会话,只走调用凭证(见鉴权一节)。请求: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(写入工具在线上报)。

POST /devices/issue

只走操作员会话(不走管理员会话、不走调用凭证;无有效操作员 会话返回 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, 操作员栏为分发的操作员)。

GET /devices

要会话。返回 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(后台分发)

GET /devices/{序列号}

要会话。成功:200,设备记录 JSON。序列号无记录:404 no such device。

POST /devices/{序列号}/push

要会话(只走会话,不走调用凭证:推送固件是操作员动作, 不是机器打包调用)。向一台设备发升级命令。请求: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。

POST /devices/push-batch

要会话(只走会话,同单个推送)。按筛选条件圈设备、逐台发 升级命令。请求: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 字符; 命令记录本身也 不存包密钥包(只存公开下载地址与版本)。配对规则:

GET /devices/push/{cmd_id}

要会话。成功: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。

GET /devices/push-batch/{batch_id}

要会话。成功: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 配置与状态接入

推送与状态接入只在配置了 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(及其子路径);部署本身已冻结,只记规则、不改部署。

错误总表

400(请求错)

正文 触发条件
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 序列号已登记但无后端存的私钥可重发

401(未鉴权)

正文 触发条件
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。

404(无此记录)

正文 触发条件
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 下无推送命令

405(方法错)

POST 专用的路径(改备注、删除、吊销)用 GET 调,返回 405。

503(服务暂不可用)

正文 触发条件
MQTT not configured 推送时未配置 MQTT(SIGN_SERVER_MQTT_USERNAME 未设)
MQTT broker not connected 推送时已配 MQTT 但 Broker 连接未建立(断线中)
MQTT publish failed: ... 推送时 MQTT 发布失败

500(服务端错)

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 一节)。

会话流程(curl)

# 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 步即不签名的等价形式。

操作员分发流程(curl)

# 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

机器打包(Rust 示例)

完整可跑的程序见 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

Java

完整可跑的程序见 tool/native/java/GwpackServiceDemo.java(仓里同一份, 文档不另贴副本防漂移;直调 HTTP,不用原生库)。 原生库调法见仓根 README 原生库一节(configure/pack/ last_filename)。

配置指针

种子、管理员密码、数据目录、端口的完整说明见 server/README.md(运行)与 docs/development-environment.md。 启动缺种子或密码拒绝启动;服务只绑 127.0.0.1, 上服务器走反向代理或隧道,不得直暴露公网。