13 KiB
设备注册接口说明
适用版本: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 且已放行的设备使用此协议;旧设备维持原有协议。未登记的编号不会通过本注册接口自动建档。
调用顺序:
- 本次启动生成一个
boot_id,整个启动期间保持不变。 - C01 获取校时挑战,验证响应签名及随机数,取得可信时间和
challenge_id。 - C02 使用设备密钥签名并提交注册请求。
- 根据返回状态继续:未绑定设备创建设置会话;已绑定设备进入恢复激活或就绪流程。
下文以 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响应。当前是代码接口说明,实际域名和开放范围由部署配置决定。