Files
story/docs/设备注册接口说明.md
T
2026-09-12 08:57:29 +08:00

13 KiB
Raw Blame History

设备注册接口说明

适用版本:ONBOARDING-1.2-SAAS;对应当前仓库实现,OpenAPI 0.78.0。

设备注册用于验证设备身份、登记本次启动和固件/SDK版本,并返回当前绑定及激活进度。首次联网和每次重新启动时使用本流程。注册成功后,用户绑定与语音服务激活继续按照返回状态处理。

1. 调用前提与顺序

设备必须已在 SaaS 完成生产登记、认证凭据交付和放行,并持有以下信息:

信息 示例 用途
device_id KP01-AABBCCDDEEFF 生产设备编号,格式为 KP01- 加 12 位大写 MAC
device_key_b64 工装交付的密钥编码 解码为 raw 32 字节后,用于 HMAC 签名和校时验签
key_version 1 当前生效认证密钥的版本
hardware_model KP01 库存登记的设备型号
hardware_revision A1 库存登记的硬件修订号

只有生产模式为 physical_confirm 且已放行的设备使用此协议;旧设备维持原有协议。未登记的编号不会通过本注册接口自动建档。

调用顺序:

  1. 本次启动生成一个 boot_id,整个启动期间保持不变。
  2. C01 获取校时挑战,验证响应签名及随机数,取得可信时间和 challenge_id。
  3. C02 使用设备密钥签名并提交注册请求。
  4. 根据返回状态继续:未绑定设备创建设置会话;已绑定设备进入恢复激活或就绪流程。

下文以 https://saas.example 表示部署后的 SaaS 域名。示例中的编号、时间和版本均为说明用值,联调时须使用实际返回值。

2. C01:获取校时挑战

POST /api/v1/device/time-challenges
Content-Type: application/json

此接口不要求请求 HMAC。建议使用 HTTPS;服务端为启动校时保留 HTTP 访问能力。设备必须验证响应中的 TIME1 签名,才可采用其中的时间。C02 必须使用 HTTPS。

请求正文:

{
  "device_id": "KP01-AABBCCDDEEFF",
  "key_version": 1,
  "boot_id": "10000000-0000-4000-8000-000000000001",
  "client_nonce": "AAECAwQFBgcICQoLDA0ODw"
}
字段 类型 必填 说明
device_id string 是 与生产登记一致的设备编号
key_version integer 是 当前生效密钥版本,必须大于等于 1
boot_id string 是 标准小写 UUID v4;本次启动标识
client_nonce string 是 16 字节随机数的 Base64URL 无填充编码;用于确认校时响应属于本次请求

X-Request-ID 可选;传入时必须是标准小写 UUID v4,省略时由平台生成。它用于追踪 C01 请求;校时挑战本身按设备、密钥版本、boot_id、client_nonce 去重。

成功响应的 data:

{
  "payload_b64": "<校时JSON原始字节的Base64URL无填充编码>",
  "mac_hex": "<64位小写十六进制签名>"
}

校验公式:

raw_key = Base64URLDecode(device_key_b64)
expected_mac = HexLower(HMAC-SHA256(raw_key, UTF8("TIME1\n" + payload_b64)))

其中 \n 是一个 LF 换行字节。先核对 mac_hex,再解码 payload_b64。解码内容为:

{
  "device_id": "KP01-AABBCCDDEEFF",
  "boot_id": "10000000-0000-4000-8000-000000000001",
  "client_nonce": "AAECAwQFBgcICQoLDA0ODw",
  "challenge_id": "10000000-0000-4000-8000-000000000002",
  "server_time_ms": 1789099200000,
  "expires_at_ms": 1789099320000
}

设备核对 device_id、boot_id、client_nonce 与本地请求完全一致,保存 challenge_id,并用 server_time_ms 建立时间基准。挑战从签发起有效 120 秒,不能重复用于不同的成功注册操作;同一 client_nonce 重试不会延长有效期。过期后换新的 client_nonce 重新获取。

3. C02:提交设备注册

项目 内容
请求地址 POST https://saas.example/api/v1/device/register
调用方 已登记、已放行的设备
认证方式 DEVREQ1 / HMAC-SHA256
请求正文 JSON,UTF-8 无 BOM,最大 8 KiB
URL参数 不接受 query 参数
时间要求 请求时间与服务端时间差不超过正负 120 秒

请求头

请求头 必填 示例/用途
Content-Type 是 application/json
X-Device-ID 是 KP01-AABBCCDDEEFF,生产设备编号
X-Key-Version 是 1,签名使用的当前密钥版本
X-Timestamp-Ms 是 当前可信 UTC 毫秒时间戳
X-Nonce 是 每次发送新生成的 16 字节随机数,Base64URL 无填充编码
X-Request-ID 是 标准小写 UUID v4,标识本次注册业务请求;网络重试保持不变
Authorization 是 Device-HMAC-SHA256 <64位小写十六进制签名>

整数请求头使用标准十进制,不带正号、空格或前导零。X-Nonce 与 C01 的 client_nonce 用途不同:前者用于每次 HTTP 发送防重放,后者用于校验对应的校时响应。

请求正文

{
  "challenge_id": "10000000-0000-4000-8000-000000000002",
  "boot_id": "10000000-0000-4000-8000-000000000001",
  "hardware_model": "KP01",
  "hardware_revision": "A1",
  "firmware_version": "1.2.0",
  "sdk_version": "1.0.0"
}
字段 类型 必填 数据用途和约束
challenge_id string 是 C01 响应中取得的挑战 UUID v4;必须属于同一设备、密钥版本和 boot_id,且尚未使用、未过期
boot_id string 是 本次启动的 UUID v4;与 C01 相同,用于识别重启和关闭旧临时设置流程
hardware_model string 是 设备型号;必须与 SaaS 生产登记完全一致
hardware_revision string 是 硬件修订号;必须与 SaaS 生产登记完全一致
firmware_version string 是 当前固件版本,1–32 个 UTF-8 字节;用于版本记录和诊断
sdk_version string 是 当前 SDK 版本,1–64 个 UTF-8 字节;用于版本记录和诊断

device_id 通过请求头传入。设备注册由库存确定租户,不需要用户登录令牌、手机号、家庭编号或使用者编号。不要提交设备明文密钥、供应商 ProductSecret 或 Wi-Fi 密码。请求中不接受未定义的字段。

4. C02 签名计算

先把最终要发送的 JSON 编码为 UTF-8 原始字节,再计算 body_hash = SHA256(body) 的小写十六进制值。按以下顺序构造 9 行文本,行间只用 LF,末尾不加换行:

DEVREQ1
POST
/api/v1/device/register
<X-Device-ID>
<X-Key-Version>
<X-Timestamp-Ms>
<X-Nonce>
<X-Request-ID>
<body_hash>
signature = HexLower(HMAC-SHA256(raw_key, UTF8(上述9行文本)))
Authorization = "Device-HMAC-SHA256 " + signature

raw_key 是解码后的 32 字节设备密钥,不能直接用 device_key_b64 的字符串字节。签名中的路径不包含域名。签名后不能再修改正文中的空格、换行或字段顺序。

项目提供 Python 签名调用工具 和 固定测试向量。将上面的注册正文保存为 UTF-8 无 BOM 的 register.json,使用实际有效的 challenge_id、boot_id 和设备版本:

python scripts/onboarding-request.py --device-id KP01-AABBCCDDEEFF --key-file device-key.b64 --key-version 1 --path /api/v1/device/register --body-file register.json --request-id 10000000-0000-4000-8000-000000000003

命令默认只输出本次签名请求头;加 --send https://你的SaaS域名 才会发送一次请求。工具使用运行它的电脑当前时间,使用前须保证电脑时间准确。固定测试向量使用合成密钥和固定时间,用于校验算法,不能直接发送到真实设备。

5. C02 成功响应

HTTP 200,以下为一台尚未绑定设备的示例:

{
  "ok": true,
  "code": "OK",
  "message": "",
  "request_id": "10000000-0000-4000-8000-000000000003",
  "server_time_ms": 1789099201000,
  "recovery_action": "NONE",
  "retry_after_ms": 0,
  "data": {
    "contract_version": "ONBOARDING-1.2-SAAS",
    "state": {
      "device_id": "KP01-AABBCCDDEEFF",
      "platform_device_id": "10000000-0000-4000-8000-000000000004",
      "device_revision": 3,
      "binding_generation": 0,
      "binding_state": "UNBOUND",
      "activation_state": "NOT_STARTED",
      "next_action": "WAIT_BIND",
      "state_hash": "<当前状态的64位小写SHA256摘要>"
    }
  }
}

示例中的 state_hash 是占位说明;设备须使用真实响应中的摘要。

响应字段 用途
ok / code / message 请求是否成功及业务结果说明
request_id 与请求对应,方便联调和日志定位
server_time_ms 响应生成时的服务器 UTC 毫秒时间
recovery_action / retry_after_ms 错误恢复建议及推荐退避时间,成功时为 NONE / 0
data.contract_version 本次接口使用的协议版本
state.device_id 设备认证使用的生产编号
state.platform_device_id 既有 SaaS 管理接口使用的设备公开 UUID,与生产编号分开
state.device_revision 当前状态版本,设备同步和状态 ACK 使用
state.binding_generation 当前绑定代数;解绑、重新绑定后变化,用于使旧绑定请求和配置失效
state.binding_state UNBOUND=未绑定;CLAIM_PENDING=等待设备物理确认;BOUND=已绑定
state.activation_state NOT_STARTED=未开始开通;CONTACT_POSSIBLE=供应商执行中或结果待确认;REGISTERED_OBSERVED=已观察到注册成功、待设备验收;READY=验收完成;RECONCILE_REQUIRED=待对账;BLOCKED=当前开通受阻;RESERVED 为预留状态
state.next_action WAIT_BIND=继续设置和绑定流程;RESUME_ACTIVATION=恢复激活;ENTER_READY=进入就绪流程
state.state_hash 当前绑定和激活状态摘要,用于后续状态持久化回执

注册成功表示 SaaS 已接受设备本次启动认证。是否已经绑定看 binding_state,是否完成通话验收看 activation_state。C02 不返回新的设备密钥或供应商语音凭据。

6. 服务端处理和重试规则

服务端先校验设备、放行状态、租户、密钥、HMAC、时间和随机数,再校验挑战归属及版本字段。成功后消费挑战,记录 boot_id、最近注册时间、固件版本和 SDK 版本,并返回设备状态。

新的 boot_id 会关闭上一次启动的临时设置会话并取消待确认的绑定申请;已经完成的正式绑定保留。

情况 设备端处理
请求超时、回包丢失 保持原 X-Request-ID 和原始正文,更新 X-Timestamp-Ms、X-Nonce 并重新签名
前一次已经成功 在同一启动、绑定代数及有效权限范围内,24 小时内可返回缓存的原业务结果;外层响应时间重新生成
相同请求号更改正文 返回 REQUEST_BODY_CONFLICT;不得用同一个请求号提交另一份版本信息或挑战
挑战无效或过期 重新 C01,取得新挑战后使用新的 X-Request-ID 发起新的注册请求
设备重启 生成新的 boot_id,重新执行 C01/C02,不重放上一次启动的注册请求
注册缓存已过期或绑定代数已变化 恢复当前启动流程并查询最新状态,不依赖旧缓存结果

7. 常见错误

HTTP code 说明与处理
400 REQUEST_ID_REQUIRED C02 缺少或使用了非 UUID v4 请求号
400 HTTPS_REQUIRED C02 必须通过 HTTPS 访问
400 QUERY_FORBIDDEN 去掉 URL query 参数
400 INVALID_JSON / VALIDATION_FAILED 检查正文结构、重复字段、型号、硬件版本和版本字符串长度
401 DEVICE_AUTHENTICATION_FAILED 检查编号、已放行状态、密钥版本、raw key、签名、时间和 nonce;重复发送同一 nonce 也会被拒绝
403 RESOURCE_ACCESS_DENIED 当前租户、设备或业务操作不可访问,核对设备状态
409 CHALLENGE_INVALID 挑战不属于当前设备/密钥/启动,或已消费、过期;重新 C01
409 REQUEST_BODY_CONFLICT 同一请求号对应的正文发生变化
409 BINDING_GENERATION_CONFLICT 当前启动或绑定状态已变化,恢复当前流程
410 CHALLENGE_EXPIRED C01 返回的同一校时挑战已到期;换新的 client_nonce
410 REQUEST_RESULT_EXPIRED 注册请求的缓存结果已超过有效期
413 REQUEST_TOO_LARGE 请求正文超过 8 KiB
415 JSON_REQUIRED Content-Type 必须为 application/json
429 RATE_LIMITED 按 Retry-After / retry_after_ms 退避;当前建议等待 60 秒
503 AUTHENTICATION_UNAVAILABLE / RATE_LIMIT_UNAVAILABLE 认证防重放或限速依赖暂不可用,保留业务请求并退避重试
500 INTERNAL_ERROR 内部处理异常,记录 request_id,按原请求号与正文重试

失败时统一返回 ok=false、对应 code,data 为 null。恢复动作是通用建议;对于 CHALLENGE_INVALID 等具体错误,应按上表执行。

8. 注册后的接口

未绑定设备接着调用 POST /api/v1/device/setup-sessions,创建手机绑定入口所需的临时会话。后续完整字段见 首次接入接口字段清单,部署与开关见 实施说明。

本说明依据当前源码核对:注册处理、设备认证及校时、签名计算、HTTP响应。当前是代码接口说明,实际域名和开放范围由部署配置决定。