ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
559 lines
38 KiB
Markdown
559 lines
38 KiB
Markdown
# BLE-PROV-1.0 蓝牙配网与账号绑定协议
|
||
|
||
版本:1.0 草案|日期:2026-09-13|面向:网页前端、设备固件、SaaS 后端
|
||
|
||
**协议选型:手机/电脑与设备之间使用 BLE GATT;安全层采用 ESP-IDF Protocomm Security 2(patch 1);业务层使用本协议定义的 UTF-8 JSON 消息。设备联网后,通过 HTTPS + DEVREQ1 设备认证完成账号绑定。**
|
||
|
||
本文是一份新增协议契约,不表示功能已经实现或完成实机联调。当前工作区为 ESP32-S3 EchoEar、ESP-IDF v5.5.1;固件仍使用配置热点,当前 sdkconfig 未启用蓝牙。本文新增的 UUID、BLE 帧、命令和 `/ble-*` 云端接口均为拟定值,可据此开展实现与评审;不会自动兼容原来的热点配网页。
|
||
|
||
“必须 / 不得”表示互操作或安全的必要条件;“建议”表示可按产品调整的默认值。所有字段、字节序及枚举按本文执行,不允许前端和固件各自解释。
|
||
|
||
## 1. 范围与参与方
|
||
|
||
目标流程:用户登录平台,发现并识别附近设备,建立安全蓝牙会话,发送 Wi-Fi 配置与一次性绑定凭证,设备确认操作后联网,由 SaaS 提交绑定,页面显示结果。
|
||
|
||
| 链路 | 协议 | 职责 |
|
||
|---|---|---|
|
||
| 网页 ↔ SaaS | HTTPS + 当前用户登录会话 | 签发绑定凭证、查询归属结果 |
|
||
| 手机/电脑 ↔ 设备 | BLE GATT + Security 2 + BLE-PROV JSON | 身份核对、配网参数传输、读取进度 |
|
||
| 设备 ↔ SaaS | HTTPS + DEVREQ1 HMAC-SHA256 | 证明设备身份、提交绑定、恢复结果 |
|
||
|
||
网页通过用户手机/电脑的蓝牙与设备通信。SaaS 服务器不直接扫描用户身边的蓝牙设备。网页是 BLE Central / GATT Client,设备是 Peripheral / GATT Server。GATT 的 Server 在此指设备,不是云端服务器。[参考 R1、R2]
|
||
|
||
本版包含首次绑定,不包含 OTA、音频传输、企业 Wi-Fi、设备转让和已绑定设备重配网。已绑定设备返回 `ALREADY_BOUND`;不能通过重新配网或恢复网络设置覆盖云端归属。AI 服务激活沿用平台独立流程,不作为蓝牙配网完成条件。
|
||
|
||
## 2. 标识与生产数据
|
||
|
||
| 字段 | 格式 | 含义 |
|
||
|---|---|---|
|
||
| `factory_device_id` | `KP01-` + 12 位大写十六进制 | 工厂身份,须与当前平台库存及 DEVREQ1 身份一致 |
|
||
| `platform_device_id` | UUID | SaaS 内部设备主键,绑定结果中返回 |
|
||
| `product_id` | uint32,1~4294967295 | 产品编号,平台维护编号与型号映射 |
|
||
| `model` | ASCII,1~32 字节 | 型号,例如 `ECHOEAR-S3`;这是示例命名,不是已登记型号 |
|
||
| `short_id` | 末 6 位工厂编号 | 只用于名称展示,不是认证依据,也不保证全局唯一 |
|
||
| `setup_nonce` | 16 个随机字节的 Base64URL,无 `=`,22 字符 | 本次实体配网窗口标识 |
|
||
| `session_id` | UUID v4 | SaaS 签发的绑定申请标识 |
|
||
| `request_id` | UUID v4 | 一次业务操作的幂等标识 |
|
||
| `mid` | uint16,1~65535 | 当前 BLE 连接内的一次请求/应答编号 |
|
||
|
||
产线必须写入工厂身份、产品编号、型号、现有的单设备云端认证密钥及版本,并生成独立的 SRP 配网凭据。不能用蓝牙地址、设备名称或用户登录密码作为密钥。
|
||
|
||
SRP 用户名固定为 `factory_device_id`,配网密码是每设备独立生成的 32 个随机字节的 Base64URL 字符串。这里的“用户名/密码”仅是本地安全握手凭据,与平台账号、Wi-Fi 密码无关。设备保存按官方算法生成的 16 字节 salt 和 verifier;二维码保存配网用户名和密码。二维码不得包含云端设备密钥。[参考 R3、R5]
|
||
|
||
二维码内容示意如下;尖括号内容必须在生产时替换,不能用于联调或生产认证:
|
||
|
||
```json
|
||
{
|
||
"v": 1,
|
||
"factory_device_id": "KP01-000000000001",
|
||
"product_id": 1001,
|
||
"setup_user": "KP01-000000000001",
|
||
"setup_password": "<每设备独立的43字符Base64URL配网密码>"
|
||
}
|
||
```
|
||
|
||
二维码采用设备本地可解析的数据,不把秘密放入网页 URL、查询参数或后台日志。前端仅在本次页面内存保存配网密码。本版要求扫码或输入该设备配网凭据;仅凭附近蓝牙名称不能完成安全认证。
|
||
|
||
## 3. 进入配网及设备发现
|
||
|
||
1. 用户在平台完成登录,选择“添加设备”。
|
||
2. 用户长按设备指定触摸区域或按键 3 秒;未绑定设备打开 300 秒配网窗口,生成新 `setup_nonce`,显示配网提示。
|
||
3. 设备发送可连接 BLE 广播;窗口内最多接受一个客户端连接。
|
||
4. 网页通过本协议 Service UUID 过滤设备,用户在浏览器设备选择框中选择目标。
|
||
5. 网页读取协议能力、建立安全会话,读取可信会话内的设备信息并核对二维码;随后可发送 `IDENTIFY` 让设备闪灯/显示识别动画。
|
||
|
||
广播名称为 `EchoEar-` + `short_id`,例如 `EchoEar-000001`。广播包包含 Flags 和完整的 128 位 Service UUID;完整名称放入 Scan Response,避免超过传统 31 字节广播包限制。不得要求浏览器在选择设备前读取任意厂家自定义广播字段。名称与信号强度只辅助选择;型号及身份以安全会话内信息、二维码和后台库存核对结果为准。
|
||
|
||
成功、窗口超时或用户退出后停止配网广播。设备普通重启不生成新的可绑定入口;需要明确的首次启用策略或上述实体操作。窗口到期后不得受理新配置,但已经受理的联网/云端确认任务继续收尾。BLE 断开不取消该任务。
|
||
|
||
## 4. GATT 服务与特征
|
||
|
||
以下为本协议拟定的固定 128 位 UUID,不是蓝牙 SIG 分配的标准配网服务。UUID 字符串按表使用;固件 SDK 若接收小端字节数组,必须按 SDK 规则转换并通过实际服务发现核对。[参考 R1]
|
||
|
||
| 名称 | UUID | 属性 | 数据与权限 |
|
||
|---|---|---|---|
|
||
| `PROVISION_SERVICE` | `8e7a1000-6f4b-4a2d-9c31-7d824f5e6100` | Service | 服务入口 |
|
||
| `PROTOCOL_INFO` | `8e7a1001-6f4b-4a2d-9c31-7d824f5e6100` | Read | 明文 16 字节协议能力,不含秘密 |
|
||
| `RX` | `8e7a1002-6f4b-4a2d-9c31-7d824f5e6100` | Write with response | 客户端发送握手或加密业务分片 |
|
||
| `TX` | `8e7a1003-6f4b-4a2d-9c31-7d824f5e6100` | Indicate | 设备返回对应应答分片,带标准 CCCD `0x2902` |
|
||
|
||
前端必须先启用 TX 的 Indication,再发送 RX。Web Bluetooth 对应 `startNotifications()` 和 `writeValueWithResponse()`;方法名中的 Notifications 不代表本特征必须设置 Notify,本协议 TX 只设置 Indicate。[参考 R1、R2]
|
||
|
||
设备逐片发送 Indication,收到 ATT confirmation 后再发送下一片。确认只证明对端蓝牙协议栈接收,不表示网页业务已处理。GATT 写成功也不表示配网或绑定成功。
|
||
|
||
`PROTOCOL_INFO` 固定 16 字节,所有多字节整数为小端:
|
||
|
||
| 偏移 | 长度 | 字段 | v1 值 |
|
||
|---:|---:|---|---|
|
||
| 0 | 2 | magic | `42 50`,ASCII `BP` |
|
||
| 2 | 1 | major | `01` |
|
||
| 3 | 1 | minor | `00` |
|
||
| 4 | 1 | security_version | `02` |
|
||
| 5 | 1 | security_patch | `01` |
|
||
| 6 | 2 | max_gatt_value | 20,即 `14 00` |
|
||
| 8 | 2 | max_record | 4096,即 `00 10` |
|
||
| 10 | 2 | capabilities | `01 00`:支持实体确认;其余位必须为 0 |
|
||
| 12 | 4 | product_id | uint32,例如 1001 为 `E9 03 00 00` |
|
||
|
||
客户端只接受本文的 major、minor、security_version、security_patch 组合;不兼容时显示升级提示并断开。本特征未经认证,不能用于决定降级加密或证明设备型号。后续 `GET_INFO` 必须再次返回并核对这些能力。
|
||
|
||
## 5. 安全会话
|
||
|
||
### 5.1 固定安全配置
|
||
|
||
采用 ESP-IDF **v5.5.1 的 Protocomm Security 2,patch 1**:SRP6a 双向证明建立会话,业务使用 AES-256-GCM。握手消息严格使用该版本 `session.proto`、`sec2.proto` 及其依赖定义;算法、大整数编码和证明校验以同版本官方实现为互操作依据。[参考 R3~R6]
|
||
|
||
不得使用 Security 0,不得回退 Security 1,不得接受 Security 2 patch 0。BLE 链路配对可以作为附加保护,但不能替代此安全会话。设备不得把单设备云端认证密钥发给网页。
|
||
|
||
### 5.2 握手交换
|
||
|
||
双方建立 BLE 连接后,设备创建独立 protocomm transport session,并注册两个逻辑 endpoint:`prov-session` 和 `prov-data`。它们是固件内部路由名,不是 HTTP 路径或新增 GATT UUID。
|
||
|
||
| 次序 | 客户端请求 | 设备应答 | 本协议帧 SECURITY 位 |
|
||
|---|---|---|---|
|
||
| 1 | `SessionData / SecScheme2 / S2Session_Command0` | `S2Session_Response0` | 1 |
|
||
| 2 | `SessionData / SecScheme2 / S2Session_Command1` | `S2Session_Response1` | 1 |
|
||
| 3 | 已加密的 `GET_INFO` JSON | 已加密的设备信息 JSON | 0 |
|
||
|
||
每个 protobuf 请求/应答作为一个完整 record,再按第 6 节分片。收到设备证明并验证成功前,前端不得发送 Wi-Fi 或绑定凭证。设备在握手完成前收到业务帧必须拒绝并断开。
|
||
|
||
### 5.3 业务加密及顺序
|
||
|
||
业务 record 是官方 Security 2 加密函数输出的 `ciphertext || 16-byte GCM tag`,不额外 Base64,不附加自定义 nonce。会话 nonce、密钥取得及计数器行为必须与 patch 1 一致:握手返回的 12 字节 nonce 由 8 字节会话值和 4 字节大端计数器组成;计数器从 1 开始,每完成一次加密或解密后递增。客户端请求、设备应答共用这条有序计数序列。[参考 R5、R6]
|
||
|
||
因此每个连接只能有一个未完成请求;设备不得主动插入加密进度推送。进度由客户端每 2 秒发送 `GET_STATUS` 获取,设备仍通过 TX Indication 返回。禁止同时轮询和发送配置,也不能跳过某条响应后继续发送下一条请求。
|
||
|
||
断线、解密失败、请求或应答传输超时后销毁安全会话,重新连接并握手。不得在原会话上自行重发整条密文或重置计数器。重新握手使用新随机数和新密钥;恢复业务通过原 `request_id` 和云端 `session_id` 实现。连续 5 次握手失败后设备暂停接受握手 60 秒。
|
||
|
||
## 6. BLE 分片格式与收发规则
|
||
|
||
v1 固定每次 GATT Value **不超过 20 字节**,即使链路协商了更大 ATT MTU 也不改变。这样网页不需要读取或控制 MTU。20 字节含 8 字节本协议头,单片净载荷最多 12 字节。
|
||
|
||
| 偏移 | 长度 | 字段 | 定义 |
|
||
|---:|---:|---|---|
|
||
| 0 | 1 | magic | 固定 `A5` |
|
||
| 1 | 1 | flags | bit0 START;bit1 END;bit2 RESPONSE;bit3 SECURITY;bit4~7 必须为 0 |
|
||
| 2 | 2 | mid | uint16 小端;请求从 1 递增,应答原值回显 |
|
||
| 4 | 2 | total_len | uint16 小端,完整 record 长度,1~4096 |
|
||
| 6 | 2 | offset | uint16 小端,本片载荷在 record 中的字节偏移 |
|
||
| 8 | 1~12 | payload | record 的这一段原始字节;不得按字符切割 |
|
||
|
||
规则:
|
||
|
||
1. 每个请求占用一个 `mid`,握手也占用;同连接下不回绕,达到 65535 后完成应答再重连。
|
||
2. 客户端 RESPONSE=0,设备 RESPONSE=1;双方相同交换使用相同 SECURITY 位。SECURITY=1 是握手 protobuf,不是“已经加密”;SECURITY=0 是安全会话中的加密业务 record。
|
||
3. 首片 offset=0 且 START=1;末片必须满足 offset+payload_len=total_len 且 END=1。单片完整消息 START、END 同时为 1。
|
||
4. 除末片外 payload 必须恰好 12 字节,offset 每次增加 12。相同 record 的 mid、total_len、方向及 SECURITY 位必须一致。接收端只保留一个在组装 record。
|
||
5. 必须检查长度上限、越界、空载荷、保留位及次序。重复、缺失或错序片不得交给业务处理;结束连接并通过新会话恢复,不能猜测拼接。
|
||
6. 完整重组以后才调用 protobuf 或安全解密。加密业务 record 至少 17 字节;解密前必须检查这一点,避免把不足 GCM tag 长度的输入交给 SDK。
|
||
7. 每片按蓝牙栈能力使用 GATT 应答/确认推进。本协议不另外增加分片 ACK、CRC 或重传命令;完整业务由 GCM 校验完整性。传输失败按第 5.3 节重连。
|
||
8. 完整业务 JSON 的 `mid` 必须等于帧头 mid;响应还须匹配原 request_id 和 cmd。帧头不作为身份依据。
|
||
|
||
仅用于分片算法校验的向量:record 是 ASCII `0123456789ABCDEFGHIJ`,共 20 字节;mid=1,方向为请求,SECURITY=1。它不是合法握手 protobuf,也不能用于发送真实配网数据。
|
||
|
||
```text
|
||
第一片(20字节):a509010014000000303132333435363738394142
|
||
第二片(16字节):a50a010014000c00434445464748494a
|
||
```
|
||
|
||
product_id=1001 的 PROTOCOL_INFO 向量:
|
||
|
||
```text
|
||
425001000201140000100100e9030000
|
||
```
|
||
|
||
## 7. 业务 JSON 公共结构
|
||
|
||
JSON 使用 UTF-8、无 BOM;禁止重复键、非有限数字、额外顶层值和未知字段。最大明文 4080 字节,嵌套最多 8 层。长度以编码后的字节数计。所有 UUID 使用标准小写带连字符格式;Base64URL 无填充。新增字段或修改含义必须升级协议,不能静默改变 v1。
|
||
|
||
请求统一结构:
|
||
|
||
```json
|
||
{
|
||
"v": 1,
|
||
"mid": 3,
|
||
"request_id": "11111111-1111-4111-8111-111111111111",
|
||
"cmd": "GET_INFO",
|
||
"params": {}
|
||
}
|
||
```
|
||
|
||
响应统一结构;成功时 error=null,失败时 result=null,错误结构固定为 code、message、retryable:
|
||
|
||
```json
|
||
{
|
||
"v": 1,
|
||
"mid": 3,
|
||
"request_id": "11111111-1111-4111-8111-111111111111",
|
||
"cmd": "GET_INFO",
|
||
"ok": false,
|
||
"result": null,
|
||
"error": {
|
||
"code": "SETUP_EXPIRED",
|
||
"message": "配网窗口已结束",
|
||
"retryable": false
|
||
}
|
||
}
|
||
```
|
||
|
||
只有能完整解密并解析公共字段时才发送业务错误;无法认证或解析公共结构的消息直接断开,不能返回未加密业务错误。`message` 仅作展示,前端判断依据是稳定的 `code`。
|
||
|
||
## 8. 命令定义
|
||
|
||
### 8.1 GET_INFO:读取设备身份
|
||
|
||
params 固定为 `{}`。成功 result:
|
||
|
||
```json
|
||
{
|
||
"factory_device_id": "KP01-000000000001",
|
||
"product_id": 1001,
|
||
"model": "ECHOEAR-S3",
|
||
"firmware_version": "<固件实际版本>",
|
||
"protocol": {"major": 1, "minor": 0, "security_version": 2, "security_patch": 1},
|
||
"wifi_bands": ["2.4GHz"],
|
||
"wifi_security": ["WPA2_PSK"],
|
||
"binding_state": "UNBOUND",
|
||
"setup_nonce": "AAECAwQFBgcICQoLDA0ODw",
|
||
"setup_expires_in_s": 240,
|
||
"key_version": 1,
|
||
"setup_proof": "<64字符小写十六进制HMAC>"
|
||
}
|
||
```
|
||
|
||
`firmware_version` 为 ASCII 1~64 字节。`binding_state` 为 UNBOUND / BOUND / UNKNOWN;UNKNOWN 时后端仍须检查实际归属,不以设备自报未绑定作为授权。窗口剩余时间使用设备单调时钟。`setup_nonce` 在同一窗口重连时保持不变,重新进入窗口必须刷新。
|
||
|
||
setup_proof 用现有原始 32 字节设备认证密钥计算 HMAC-SHA256,结果小写 hex。签名正文是以下五行 UTF-8,LF 分隔,末尾无 LF;数字按无前导零十进制编码:
|
||
|
||
```text
|
||
BLESETUP1
|
||
{factory_device_id}
|
||
{key_version}
|
||
{product_id}
|
||
{setup_nonce}
|
||
```
|
||
|
||
这是本协议新增的域隔离证明,用于后台核验客户端拿到了设备生成的窗口信息。它不是绑定授权,也没有独立的可信时间;真正提交时必须再次校验当前窗口、设备请求认证和实体确认。网页核对二维码中的工厂编号及 product_id,不匹配时终止。
|
||
|
||
### 8.2 IDENTIFY:识别实体设备
|
||
|
||
params 为 `{"duration_ms":3000}`,允许 1000~5000。设备闪灯或显示识别动画,返回 `{"accepted":true}`。设备不输出账号或网络秘密;同连接最多每 5 秒执行一次。硬件没有可用指示能力时返回 `IDENTIFY_UNSUPPORTED`。
|
||
|
||
### 8.3 CONFIGURE:提交 Wi-Fi 与账号绑定凭证
|
||
|
||
params 示例;claim_token 是占位值,真实值由 SaaS 生成:
|
||
|
||
```json
|
||
{
|
||
"session_id": "22222222-2222-4222-8222-222222222222",
|
||
"factory_device_id": "KP01-000000000001",
|
||
"setup_nonce": "AAECAwQFBgcICQoLDA0ODw",
|
||
"claim_token": "<43字符Base64URL一次性令牌>",
|
||
"wifi": {
|
||
"ssid_b64": "TXlIb21l",
|
||
"security": "WPA2_PSK",
|
||
"password": "example-password",
|
||
"hidden": false
|
||
}
|
||
}
|
||
```
|
||
|
||
| 参数 | 必须 | 校验 |
|
||
|---|---|---|
|
||
| session_id | 是 | UUID v4,与云端申请一致 |
|
||
| factory_device_id | 是 | 必须等于本机身份 |
|
||
| setup_nonce | 是 | 必须等于当前有效实体配网窗口 |
|
||
| claim_token | 是 | Base64URL 解码为 32 字节;不包含平台登录密码或长期登录令牌 |
|
||
| wifi.ssid_b64 | 是 | Base64URL,解码后 1~32 字节;此例解码为 `MyHome` |
|
||
| wifi.security | 是 | v1 仅 `WPA2_PSK` |
|
||
| wifi.password | 是 | 8~63 个可打印 ASCII 字节;v1 不支持 64 位十六进制原始 PSK |
|
||
| wifi.hidden | 是 | boolean;true 表示按给定 SSID 主动连接隐藏网络 |
|
||
|
||
网页输入的常规 SSID 按 UTF-8 编码后再 Base64URL。设备最终向 Wi-Fi 驱动传原始 SSID 字节和明确长度,不把 SSID 当以零结尾的字符串。v1 不支持开放网络、WPA3-only、WPA-Enterprise 或 5GHz;WPA2/WPA3 混合网络只有允许 WPA2-PSK 时才可尝试。其他安全模式必须通过新版本扩展,不能当作 WPA2 强行连接。
|
||
|
||
设备完成校验并把待处理操作安全写入持久存储后,返回:
|
||
|
||
```json
|
||
{
|
||
"accepted": true,
|
||
"config_request_id": "33333333-3333-4333-8333-333333333333",
|
||
"stage": "WAITING_CONFIRMATION"
|
||
}
|
||
```
|
||
|
||
`config_request_id` 必须等于 CONFIGURE 公共结构中的 request_id。此响应表示已受理,不表示联网成功。设备显示“确认本次配网与绑定”,用户在 60 秒内短按指定实体区域确认;固件生成 press_event_id(UUID v4),把该事件关联本次 session_id、config_request_id、setup_nonce 并持久化,才开始联网。先前进入窗口的长按不能复用作这次申请确认。
|
||
|
||
同一设备同时只运行一个配置任务。活动任务期间新 request_id 的 CONFIGURE 返回 BUSY;相同 request_id 且相同 params 返回原任务当前结果,不重复落盘或联网;相同 request_id 但 params 不同返回 REQUEST_CONFLICT。幂等摘要只包含 cmd 与 params 的确定性字段值,不包含会随重连变化的 mid;实现可用有序字段编码,不能依赖不同 JSON 空白/键序。
|
||
|
||
凭证在受保护存储中保存为待处理任务;Wi-Fi 取得 IP 后再提交为可用网络配置,失败保留旧网络配置。账号绑定以服务器事实为准。Wi-Fi 密码不上传 SaaS、不回显、不写业务日志;终态后清除临时 claim_token 和待处理 Wi-Fi 副本。
|
||
|
||
### 8.4 GET_STATUS:查询进度和恢复
|
||
|
||
params 为 `{"config_request_id":"33333333-3333-4333-8333-333333333333"}`。每次查询使用新的公共 request_id 与新的 mid;被查询的 config_request_id 保持不变。
|
||
|
||
成功 result 的固定字段如下;尚无结果时 binding_id 和 platform_device_id 为 null,未绑定 generation=0:
|
||
|
||
```json
|
||
{
|
||
"config_request_id": "33333333-3333-4333-8333-333333333333",
|
||
"session_id": "22222222-2222-4222-8222-222222222222",
|
||
"stage": "BINDING",
|
||
"wifi_state": "CONNECTED",
|
||
"binding_state": "UNKNOWN",
|
||
"local_saved": false,
|
||
"platform_device_id": null,
|
||
"binding_id": null,
|
||
"binding_generation": 0,
|
||
"error": null
|
||
}
|
||
```
|
||
|
||
wifi_state 为 NOT_STARTED / CONNECTING / CONNECTED / FAILED;binding_state 为 UNBOUND / UNKNOWN / BOUND。`error` 为 null 或第 7 节统一错误对象。stage=FAILED 时必须带错误;stage=PENDING_RECONCILIATION 时可带暂时故障。查询到失败任务的外层 ok 仍为 true,因为查询本身成功。
|
||
|
||
无本地记录返回 STATUS_NOT_FOUND,此时页面应查询云端 session_id。断线重连后的状态查询须先完成新的安全握手,并仍在原窗口内;窗口已关闭时页面通过云端查询。云端提交成功但设备未保存时,页面应显示“已绑定,设备正在同步”,不能提示用户重新绑定。
|
||
|
||
### 8.5 FINISH:确认页面已收到结果
|
||
|
||
params 为 `{"config_request_id":"33333333-3333-4333-8333-333333333333"}`。仅 stage=SUCCESS 时允许,返回 `{"accepted":true,"close_after_ms":3000}`。设备必须先发送完整响应,再于 3 秒后结束 BLE 配网。
|
||
|
||
没有 FINISH 时,设备成功后保留 BLE 最多 30 秒用于读取结果,随后自行关闭。FINISH 只结束本地会话,不新增、取消或修改云端绑定。v1 不提供在途取消命令;网络异常不能被当作取消成功。
|
||
|
||
## 9. 云端绑定契约(新增)
|
||
|
||
以下四个路径均为拟新增接口,不与现有 `/api/v1/web/claims` 或 `/api/v1/device/claim-confirmations` 混用。成功响应固定包含 `ok=true`、`code="OK"`、request_id、server_time_ms(Unix 毫秒整数)及 data;失败响应固定包含 `ok=false`、code、message、request_id,不包含 data。这与当前平台封装一致,不能用 BLE 的 result/error 外壳解析 HTTP 响应。data 字段定义见下文,业务错误码见第 11 节。
|
||
|
||
### 9.1 创建绑定申请
|
||
|
||
`POST /api/v1/web/ble-setup-sessions`,使用当前客户登录 Cookie、`Content-Type: application/json`、`X-CSRF-Token` 及 X-Request-ID(UUID v4)。CSRF token 从已有 `GET /api/v1/web/account/session` 获取,服务端执行既有同源及账号 CSRF 校验。body:
|
||
|
||
```json
|
||
{
|
||
"contract": "BLE-PROV-1.0",
|
||
"factory_device_id": "KP01-000000000001",
|
||
"product_id": 1001,
|
||
"key_version": 1,
|
||
"setup_nonce": "AAECAwQFBgcICQoLDA0ODw",
|
||
"setup_proof": "<GET_INFO返回的64字符HMAC>",
|
||
"profile_id": "44444444-4444-4444-8444-444444444444"
|
||
}
|
||
```
|
||
|
||
profile_id 是平台使用者档案,必须属于当前账号有权管理的家庭/租户。账号主体由服务端从登录态取得,禁止请求方提交 user_id、owner_id、手机号或密码来决定归属。
|
||
|
||
服务端检查库存放行、型号、密钥版本、setup_proof、归属及档案权限。成功 HTTP 201 的 data 为:
|
||
|
||
```json
|
||
{
|
||
"session_id": "22222222-2222-4222-8222-222222222222",
|
||
"claim_token": "<CSPRNG生成的32字节Base64URL令牌>",
|
||
"expires_in_s": 300,
|
||
"binding_state": "PENDING"
|
||
}
|
||
```
|
||
|
||
令牌绑定当前账号、租户/家庭、profile_id、设备、setup_nonce、预期 binding_generation;只有设备认证请求能兑换。普通业务记录保存令牌哈希。相同 X-Request-ID 的创建重试必须返回同一申请和原有效令牌:可以在单独的受保护短期存储保存加密响应至到期,不把令牌放进普通幂等缓存。所有令牌响应加 `Cache-Control: no-store`。
|
||
|
||
### 9.2 设备提交绑定
|
||
|
||
`POST /api/v1/device/ble-claims`,设备联网、完成实体确认及可信校时后调用,使用第 10 节 DEVREQ1。body:
|
||
|
||
```json
|
||
{
|
||
"contract": "BLE-PROV-1.0",
|
||
"session_id": "22222222-2222-4222-8222-222222222222",
|
||
"claim_token": "<CONFIGURE收到的一次性令牌>",
|
||
"setup_nonce": "AAECAwQFBgcICQoLDA0ODw",
|
||
"config_request_id": "33333333-3333-4333-8333-333333333333",
|
||
"press_event_id": "55555555-5555-4555-8555-555555555555"
|
||
}
|
||
```
|
||
|
||
服务端必须在同一事务中锁定设备和申请,核验:认证设备与申请相同、令牌正确、申请未过期/撤销、窗口标识匹配、目标权限仍有效、实体确认事件对应本次申请、绑定代数未变化且不存在其他有效归属。完成后消费令牌、建立绑定、保存 generation 和申请终态,并安排独立 AI 激活任务。press_event_id 只能确认一个 session_id;这是可信固件上报的实体操作证据,不是服务器独立读取到了 GPIO。
|
||
|
||
HTTP 200 的 data:
|
||
|
||
```json
|
||
{
|
||
"session_id": "22222222-2222-4222-8222-222222222222",
|
||
"config_request_id": "33333333-3333-4333-8333-333333333333",
|
||
"platform_device_id": "66666666-6666-4666-8666-666666666666",
|
||
"binding_id": "77777777-7777-4777-8777-777777777777",
|
||
"binding_generation": 1,
|
||
"binding_state": "BOUND"
|
||
}
|
||
```
|
||
|
||
相同申请、设备、配置请求和 press_event_id 的重试返回原提交结果,不再创建绑定;先检查是否为已提交的同一业务,再判断未提交申请的到期时间。若期间发生解绑、换绑或设备吊销,返回 STATE_CHANGED,不能把旧绑定当作当前有效结果。不得以“令牌已消费”为由让已成功的设备无限重绑。
|
||
|
||
### 9.3 页面查询与设备恢复
|
||
|
||
| 接口 | 认证 | 输入 | data |
|
||
|---|---|---|---|
|
||
| `GET /api/v1/web/ble-setup-sessions/{session_id}` | 原创建账号登录态 | 路径 session_id | session_id、status、binding_state、config_request_id、platform_device_id、binding_id、binding_generation、error |
|
||
| `POST /api/v1/device/ble-setup-status` | DEVREQ1 | `{"session_id":"…","config_request_id":"…"}` | 同上,只允许申请对应设备查询 |
|
||
|
||
status 为 PENDING / COMMITTED / EXPIRED / REJECTED。PENDING 时 binding_state=UNKNOWN;COMMITTED 时 binding_state=BOUND;其余状态按实际事实返回,不能从“申请失败”推导设备不存在其他绑定。没有值的 config_request_id、设备及绑定 UUID 为 null;generation 未知/未绑定为 0,error 为 null 或统一错误对象。
|
||
|
||
页面每 2 秒查询,120 秒仍未确认则显示“等待设备确认”,后续由用户刷新或每 10 秒查询;不自动生成新申请。SaaS 未收到设备 claim 时不知道其本地 Wi-Fi 阶段,云端接口不得伪造“密码错误”等本地进度。
|
||
|
||
设备在提交前持久化 session_id、config_request_id、press_event_id 及 pending 标记。POST 响应丢失时查询同一申请;重启后先恢复原任务并查询云端,COMMITTED 则保存既有结果。只清理本地 Wi-Fi 不能撤销已经提交的账号绑定。
|
||
|
||
## 10. 设备 HTTPS 请求认证
|
||
|
||
复用当前 `internal/onboarding/crypto.go` 与 `auth.go` 的 DEVREQ1,不使用普通用户 Bearer token,也不使用名为 `Device-Certificate` 的自定义 Authorization 占位值。
|
||
|
||
必填请求头:
|
||
|
||
```text
|
||
Content-Type: application/json
|
||
X-Device-ID: KP01-000000000001
|
||
X-Key-Version: 1
|
||
X-Timestamp-Ms: <Unix毫秒时间,十进制>
|
||
X-Nonce: <16随机字节的22字符Base64URL>
|
||
X-Request-ID: <UUID v4>
|
||
Authorization: Device-HMAC-SHA256 <64字符小写hex签名>
|
||
```
|
||
|
||
签名为 `hex_lower(HMAC_SHA256(raw_device_key_32bytes, canonical_UTF8))`。canonical 固定九行,LF 分隔且末尾无 LF:
|
||
|
||
```text
|
||
DEVREQ1
|
||
POST
|
||
{请求路径,不带query}
|
||
{X-Device-ID}
|
||
{X-Key-Version}
|
||
{X-Timestamp-Ms}
|
||
{X-Nonce}
|
||
{X-Request-ID}
|
||
{实际HTTP原始body字节的SHA256小写hex}
|
||
```
|
||
|
||
密钥是原始 32 字节,不是 hex 字符串的 UTF-8 字节。允许时间误差 ±120 秒;nonce 防重放至少保留 5 分钟。重试保持相同业务 request_id 和原 body 字节,但必须更新时间、nonce 并重新签名。body 上限 8 KiB,不能携带 Wi-Fi 密码。
|
||
|
||
设备必须验证 TLS 服务端证书和预置/受控配置的 SaaS 域名,禁止通过 BLE 任意替换目标服务器。没有可信时间时复用既有 time-challenges 的签名校时契约,随后严格验证 HTTPS;不得为解决时间问题关闭证书校验。现有校时细节以工作区 `openapi/onboarding.yaml` 与实现为准。[参考 R7]
|
||
|
||
## 11. 状态、错误与超时
|
||
|
||
### 11.1 本地任务状态
|
||
|
||
| stage | 含义 | 下一步 |
|
||
|---|---|---|
|
||
| WAITING_CONFIRMATION | 已安全保存配置,等待本次实体确认 | 确认后 WIFI_CONNECTING;超时 FAILED |
|
||
| WIFI_CONNECTING | 正在关联路由器及获取 IP | 成功 CLOUD_CONNECTING;确定失败 FAILED |
|
||
| CLOUD_CONNECTING | 已取得 IP,准备可信校时和 HTTPS | BINDING 或网络故障 |
|
||
| BINDING | 已开始向云端提交账号归属 | SAVING_RESULT、确定拒绝 FAILED,或结果不明 |
|
||
| PENDING_RECONCILIATION | 请求可能已提交,暂时无法确认 | 查询同一 session,不能自动重绑 |
|
||
| SAVING_RESULT | 已确认云端绑定,保存到设备 | SUCCESS 或 LOCAL_SAVE_FAILED |
|
||
| SUCCESS | Wi-Fi 可用、云端 BOUND、本地保存完成 | FINISH / 自动关闭 |
|
||
| FAILED | 本次确定失败;详见 error | 修复后重新申请或重试允许的阶段 |
|
||
|
||
LOCAL_SAVE_FAILED 用 stage=FAILED、binding_state=BOUND、local_saved=false 表示;只修复本地保存,不能解除/重复建立绑定。Wi-Fi 成功与账号成功分别展示,AI 激活失败不改写 SUCCESS。
|
||
|
||
### 11.2 错误码
|
||
|
||
| code | 含义 | 处理 |
|
||
|---|---|---|
|
||
| UNSUPPORTED_VERSION | 版本不兼容 | 升级客户端/固件,不能降级安全机制 |
|
||
| AUTH_FAILED | SRP 认证失败 | 检查设备二维码;握手层失败直接断开 |
|
||
| INVALID_FRAME | 分片不合法 | 断开并新建安全会话 |
|
||
| INVALID_PARAM | 字段、长度或格式不合法 | 修正参数 |
|
||
| DEVICE_MISMATCH | 选择设备与目标不一致 | 重新识别设备 |
|
||
| SETUP_EXPIRED | 配网窗口已关闭 | 再次实体进入配网 |
|
||
| CONFIRM_TIMEOUT | 60 秒内未确认当前申请 | 重新提交新申请,旧申请不自动执行 |
|
||
| BUSY | 已有进行中的配置任务 | 查询原任务 |
|
||
| REQUEST_CONFLICT | 相同请求号对应不同参数 | 修正调用方幂等实现 |
|
||
| STATUS_NOT_FOUND | 设备没有该本地任务 | 查云端申请,再决定恢复 |
|
||
| IDENTIFY_UNSUPPORTED | 无可用识别指示器 | 按铭牌/二维码核对 |
|
||
| WIFI_NOT_FOUND | 未发现目标网络 | 检查 SSID、频段和距离 |
|
||
| WIFI_AUTH_FAILED | 驱动明确报告认证失败 | 检查密码及路由器认证方式 |
|
||
| WIFI_CONNECT_FAILED | 原因无法精确归类 | 显示连接失败,不能一律报密码错误 |
|
||
| WIFI_IP_TIMEOUT | 已尝试关联但未及时取得 IP | 检查 DHCP/路由器 |
|
||
| WIFI_SECURITY_UNSUPPORTED | 非本版支持的网络类型 | 使用支持网络或升级协议 |
|
||
| CLOUD_UNREACHABLE | 云端暂时不可达 | 按是否可能提交进入重试或对账 |
|
||
| TOKEN_INVALID / TOKEN_EXPIRED | 一次性令牌非法或未提交即到期 | 登录页面重新申请 |
|
||
| ALREADY_BOUND | 设备已有有效归属 | 进入独立解绑/转移流程 |
|
||
| PERMISSION_DENIED | 当前账号/目标档案权限不满足 | 修正账号或档案选择 |
|
||
| DEVICE_NOT_RELEASED | 设备未放行、停用或凭据无效 | 由平台处理库存/凭据 |
|
||
| STATE_CHANGED | 提交期间权限或绑定代数变化 | 查询当前归属 |
|
||
| LOCAL_SAVE_FAILED | 本地持久化失败 | 保留云端结果,修复保存 |
|
||
| INTERNAL_ERROR | 内部处理失败 | 记录不含秘密的关联 ID,按状态恢复 |
|
||
|
||
AUTH_FAILED、INVALID_FRAME 可由客户端根据断开阶段归类,不要求设备在未认证通道发送错误 JSON。云端 HTTP 状态约定:400 参数/协议错误;401 未登录/设备认证失败;403 无权限/未放行;404 申请不存在或不可见;409 已绑定/请求冲突/状态变化;410 申请过期;429 限流;503 暂时不可用。业务 code 优先于 message。
|
||
|
||
### 11.3 默认时限
|
||
|
||
| 项目 | 默认值 | 超时行为 |
|
||
|---|---:|---|
|
||
| 新配置受理窗口 | 300 秒 | 停止受理;已开始任务继续 |
|
||
| BLE 连接后未完成握手 | 90 秒 | 断开并销毁会话 |
|
||
| 相邻分片无进展 | 10 秒 | 断开;不能继续使用原安全会话 |
|
||
| 一条 record 传输 | 45 秒 | 断开 |
|
||
| 完整请求后设备开始应答 | 5 秒 | 断开后查询/恢复;配置后台异步执行 |
|
||
| 未挂起请求的连接空闲 | 90 秒 | 断开;不取消任务 |
|
||
| 配置后的实体确认 | 60 秒 | 本地确认超时,不提交云端 |
|
||
| Wi-Fi 关联 / DHCP | 45 秒 / 15 秒 | 确定失败,保留旧网络 |
|
||
| 云端请求单次 | 15 秒 | 按同一申请重试/查询 |
|
||
| 云端自动重试总窗口 | 90 秒 | 不确定则转 PENDING_RECONCILIATION |
|
||
| 令牌有效期 | 服务端签发起 300 秒 | 不接受新的绑定提交;已提交结果仍可查询 |
|
||
| 成功后等待 FINISH | 30 秒 | 关闭 BLE,保留绑定与网络 |
|
||
|
||
云端重试退避为 1、2、4、8、16 秒,之后最多每 16 秒一次,可增加不超过 20% 的随机抖动;确定的权限、设备归属或参数错误不自动重试。PENDING_RECONCILIATION 后设备在网络可用时每 30 秒查询原申请,得到 COMMITTED 或确定终态后停止。物理窗口结束不阻止设备恢复已有申请,但不能据此新建绑定。
|
||
|
||
## 12. 实现边界与现有系统衔接
|
||
|
||
| 端 | 需要实现的内容 |
|
||
|---|---|
|
||
| 前端 | 登录态复用、浏览器能力检查、扫码、GATT 连接/订阅、分片、Security 2 客户端、命令串行队列、状态与断线恢复 |
|
||
| 设备 | BLE 广播和 GATT 服务、分片重组、自定义 protocomm transport 适配、实体确认、Wi-Fi 后台任务、DEVREQ1、受保护持久化 |
|
||
| 后端 | 四个新增接口、BLESETUP1 校验、短期令牌、事务绑定与幂等、权限复查、状态恢复、审计 |
|
||
| 产线 | 库存身份映射、独立 SRP 凭据/二维码、云端单设备密钥烧录、放行与秘密保护 |
|
||
|
||
设备 RX 回调只做有界校验、复制和投递工作队列,不能在蓝牙回调中等待 Wi-Fi/DHCP/HTTPS。完整 SECURITY record 交给 `prov-session`,完整业务密文交给 `prov-data`;通过 `protocomm_req_handle` 处理,由 protocomm 完成业务解密、handler 调用与响应加密,外层再分片返回,不能额外加密第二遍。[参考 R3]
|
||
|
||
本版使用**自定义 GATT 分片 transport + 官方 protocomm 安全层**,不是原生 `wifi_prov_mgr` 的 BLE 线协议。不能直接拿 ESP Provisioning 通用 App 与本文 UUID 通信;前端及调试工具需要本协议适配。官方 Security 2 Python 客户端可作为握手互操作对照,但其默认 transport 不理解本文帧头。
|
||
|
||
首次落地目标为支持 Web Bluetooth 的桌面/Android 浏览器。页面必须 HTTPS 且发现设备由用户点击触发;浏览器负责设备选择。Safari/iPhone 不能直接承诺网页蓝牙可用,需要小程序或原生客户端适配同一 BLE 协议,实际支持矩阵以验收为准。[参考 R2、R8]
|
||
|
||
当前平台已存在 ONBOARDING-1.1 的 DEVREQ1 及实体确认绑定流程;旧设计文档的“接口未实现”不能作为当前状态依据。本协议保留其原始设备密钥格式与当前归属约束,但新增 BLE 入口和令牌交换,不修改旧客户端字段或允许绕过实体确认。应新增明确的接入契约配置;当前工厂身份查询还限制 enrollment_mode,适配时必须显式允许本契约并保留设备/租户门禁,不能简单放开所有设备。
|
||
|
||
现有测试板的 `echoear-…` 硬件标识与 ONBOARDING 使用的 `KP01-…` 工厂身份不是同一个字段。量产/迁移时建立明确映射;不能由前端截断 MAC 临时制造已认证身份。当前 sdkconfig 中 `CONFIG_BT_ENABLED` 未开启,需要固件开发与内存/射频共存验证,这不是仅修改网页即可实现的功能。
|
||
|
||
## 13. 联调验收
|
||
|
||
以下是实施后的验收要求,不代表已经通过。正式发布前必须有浏览器和实机证据。
|
||
|
||
| 场景 | 必须观察到的结果 |
|
||
|---|---|
|
||
| 正常首次配网 | 识别正确设备,实体确认后联网;云端只有一条有效绑定;本地保存后 SUCCESS |
|
||
| 附近两台同型号 | 按短编号/二维码选择,认证失败时绝不发送 Wi-Fi;IDENTIFY 指示所选设备 |
|
||
| 官方安全互操作 | 前端与 v5.5.1 Security 2 patch 1 双向握手、加解密一致,错误配网密码失败 |
|
||
| 降级及篡改 | Security 0/1、patch 0、篡改密文、非法分片全部拒绝,无配置副作用 |
|
||
| 默认 MTU | 20 字节 Value 下完成最长合法配置;不能依赖 MTU=247 |
|
||
| 分片边界 | 第 6 节向量重组正确;测试 1、12、13、4096 字节 record;超长、重复、越界拒绝 |
|
||
| 串行加密 | GET_STATUS 不与 CONFIGURE 并发;超时必须重握手,不能重置 nonce 续传 |
|
||
| Wi-Fi 错误 | 认证失败、无网络、DHCP 超时分别处理;不删除原可用配置 |
|
||
| 重复配置 | 相同 request_id 和参数只执行一次;不同参数返回 REQUEST_CONFLICT |
|
||
| BLE 中途断开 | 云端任务不被取消;新安全会话或云端页面可以恢复原状态 |
|
||
| 请求已提交但回包丢失 | 查询原 session 得到原绑定,不产生第二条绑定 |
|
||
| 绑定后断电/落盘失败 | 云端 BOUND 保留,设备恢复保存,页面不要求重新绑定 |
|
||
| 令牌过期/跨设备 | 未提交的过期申请被拒绝,其他设备不能兑换令牌 |
|
||
| 账号越权/并发抢绑 | 无权限 profile 被拒绝;并发申请最多一个提交成功 |
|
||
| 未实体确认 | 不能联网后自动替用户提交新绑定;确认事件不能复用于另一申请 |
|
||
| 已绑定/恢复出厂 | BLE 首次绑定入口不覆盖已有云端归属 |
|
||
| 秘密检查 | 广播、日志、云端配网 body、浏览器持久存储中无 Wi-Fi 密码和配网秘密 |
|
||
| AI 激活失败 | 页面保持“已绑定”,AI 激活单独重试 |
|
||
|
||
## 14. 依据与规范性引用
|
||
|
||
- R1:[Bluetooth SIG:GATT 规范](https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-61/out/en/host/generic-attribute-profile--gatt-.html)。GATT 读写、特征与 indication 的标准依据;本文业务 UUID、分片和 JSON 是自定义内容。
|
||
- R2:[Chrome:Web Bluetooth 开发文档](https://developer.chrome.com/docs/capabilities/bluetooth)。浏览器访问约束及 GATT 调用方式。
|
||
- R3:[ESP-IDF v5.5.1 / ESP32-S3:Protocomm](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/esp32s3/api-reference/provisioning/protocomm.html)。安全组件与 endpoint/transport 集成方式。
|
||
- R4:[session.proto](https://github.com/espressif/esp-idf/blob/v5.5.1/components/protocomm/proto/session.proto)、[sec2.proto](https://github.com/espressif/esp-idf/blob/v5.5.1/components/protocomm/proto/sec2.proto)、[constants.proto](https://github.com/espressif/esp-idf/blob/v5.5.1/components/protocomm/proto/constants.proto)。握手 wire schema 以该固定 tag 及导入依赖为准。
|
||
- R5:[官方 Security 2 客户端](https://github.com/espressif/esp-idf/blob/v5.5.1/tools/esp_prov/security/security2.py)、[SRP6a 实现](https://github.com/espressif/esp-idf/blob/v5.5.1/tools/esp_prov/security/srp6a.py)。客户端握手、凭据生成与密码学编码的互操作参考。
|
||
- R6:[官方 Security 2 固件实现](https://github.com/espressif/esp-idf/blob/v5.5.1/components/protocomm/src/security/security2.c)。patch 1、nonce 顺序、GCM 输出与安全会话行为。
|
||
- R7:本地现有实现:[DEVREQ1](../internal/onboarding/crypto.go)、[设备认证](../internal/onboarding/auth.go)、[当前路由](../internal/onboarding/http.go)、[Onboarding API](../openapi/onboarding.yaml)、[现有认证测试向量](../test/fixtures/onboarding-devreq1.json)。这些已有契约不能与新增 BLE 接口混为一谈。
|
||
- R8:[WebKit:未实现的相关能力说明](https://webkit.org/tracking-prevention/)。网页客户端覆盖范围的限制依据。
|
||
- 设备基线:[EchoEar 固件说明](../firmware/echoear-platform-agent/README.md)、[当前 sdkconfig](../firmware/echoear-platform-agent/sdkconfig)。工作区读取日期为 2026-09-13,未将本协议部署到设备或服务器。
|
||
|
||
修订记录:1.0 草案确定 BLE GATT transport、Security 2 patch 1、JSON 配网消息、一次性账号绑定令牌及失败恢复约定;实施并完成互操作验收后再冻结为发布版本。
|