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

262 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设备注册接口说明
适用版本:`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:获取校时挑战
```http
POST /api/v1/device/time-challenges
Content-Type: application/json
```
此接口不要求请求 HMAC。建议使用 HTTPS;服务端为启动校时保留 HTTP 访问能力。设备必须验证响应中的 TIME1 签名,才可采用其中的时间。C02 必须使用 HTTPS。
请求正文:
```json
{
"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`:
```json
{
"payload_b64": "<校时JSON原始字节的Base64URL无填充编码>",
"mac_hex": "<64位小写十六进制签名>"
}
```
校验公式:
```text
raw_key = Base64URLDecode(device_key_b64)
expected_mac = HexLower(HMAC-SHA256(raw_key, UTF8("TIME1\n" + payload_b64)))
```
其中 `\n` 是一个 LF 换行字节。先核对 mac_hex,再解码 payload_b64。解码内容为:
```json
{
"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 发送防重放,后者用于校验对应的校时响应。
**请求正文**
```json
{
"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,末尾不加换行:
```text
DEVREQ1
POST
/api/v1/device/register
<X-Device-ID>
<X-Key-Version>
<X-Timestamp-Ms>
<X-Nonce>
<X-Request-ID>
<body_hash>
```
```text
signature = HexLower(HMAC-SHA256(raw_key, UTF8(上述9行文本)))
Authorization = "Device-HMAC-SHA256 " + signature
```
raw_key 是解码后的 32 字节设备密钥,不能直接用 device_key_b64 的字符串字节。签名中的路径不包含域名。签名后不能再修改正文中的空格、换行或字段顺序。
项目提供 [Python 签名调用工具](../scripts/onboarding-request.py) 和 [固定测试向量](../test/fixtures/onboarding-devreq1.json)。将上面的注册正文保存为 UTF-8 无 BOM 的 `register.json`,使用实际有效的 challenge_id、boot_id 和设备版本:
```powershell
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,以下为一台尚未绑定设备的示例:
```json
{
"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`,创建手机绑定入口所需的临时会话。后续完整字段见 [首次接入接口字段清单](onboarding-api-fields.md),部署与开关见 [实施说明](onboarding-implementation.md)。
本说明依据当前源码核对:[注册处理](../internal/onboarding/setup.go)、[设备认证及校时](../internal/onboarding/auth.go)、[签名计算](../internal/onboarding/crypto.go)、[HTTP响应](../internal/onboarding/http.go)。当前是代码接口说明,实际域名和开放范围由部署配置决定。