262 lines
13 KiB
Markdown
262 lines
13 KiB
Markdown
# 设备注册接口说明
|
||||
|
|
|
|||
|
|
适用版本:`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)。当前是代码接口说明,实际域名和开放范围由部署配置决定。
|