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