ESP-IDF 统一配网(Unified Provisioning)框架全解析:协议、传输、安全方案与配网工具
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
统一配网(Unified Provisioning)是 ESP-IDF 为 IoT 设备提供的一套可扩展配网机制:开发者可以通过 Wi-Fi(SoftAP + HTTP 服务器)或蓝牙 LE(GATT)传输通道,配合不同级别的安全方案,将 Wi-Fi 凭据乃至任意自定义配置安全地下发到设备。本文以官方文档为主线,结合仓库内 protocomm 组件的源码、ProtoBuf 定义、Kconfig 配置与测试用例,逐层拆解其协议设计、传输选型、安全握手细节与配套工具链,帮助你在自己的产品中正确选型并落地一套可靠的设备配网方案。
什么是统一配网(Overview)
在 ESP-IDF 中,统一配网支持是一个可扩展机制,它允许开发者通过多种传输通道(transport)和不同安全方案(security scheme),为设备配置 Wi-Fi 凭据和/或其他自定义配置。根据使用场景,它提供了开箱即用的 Wi-Fi 网络配网完整解决方案,并附带 iOS、Android 示例应用。开发者既可以扩展设备端实现,也可以扩展手机 App 端实现,以适配发送额外配置数据的需求。
该实现包含以下四个重要特性:
- 可扩展协议(Extensible Protocol):协议本身完全灵活,允许开发者在配网过程中发送自定义配置,数据的表示方式也交由应用自行决定,框架不做强制约束。
- 传输灵活(Transport Flexibility):协议既可以跑在 Wi-Fi(SoftAP + HTTP 服务器)上,也可以跑在蓝牙 LE 上作为传输协议。只要某种传输能支持"命令-响应"(command-response)行为,框架就很容易为其添加支持。从源码结构看,这一可扩展性正是由 protocomm 抽象出的传输层接口实现的,见 components/protocomm/src/transports/ 下的
protocomm_httpd.c、protocomm_ble.c、protocomm_nimble.c、protocomm_console.c等多个并行实现。 - 安全方案灵活(Security Scheme Flexibility):不同的使用场景可能需要不同的安全方案来保护配网过程中交换的数据。有的应用使用 WPA2 保护的 SoftAP,有的使用蓝牙 LE 的 "just-works" 安全;有的应用认为传输本身不安全,需要应用层安全。统一配网框架允许应用按需选择合适的安全方案。
- 紧凑的数据表示(Compact Data Representation):协议使用 Google Protobufs 作为会话建立与网络配网的数据表示,具有紧凑的数据体积,并且能以原生格式被多种编程语言解析。需要注意的是,这种数据表示方式并不强加于应用自定义数据,开发者可以选择自己习惯的表示方式。
仓库中 protocomm 组件的 proto 目录 保存了这些 Protobuf 定义(session.proto、sec0.proto、sec1.proto、sec2.proto、constants.proto),而 proto-c 目录 与 python 目录 则分别存放了编译生成的 C(protobuf-c)与 Python(pb2)代码,C 固件与配网脚本使用的是同一套消息定义。
典型配网流程(Typical Provisioning Process)
无论采用哪种传输方式与安全方案,一次完整的配网会话都遵循如下四个阶段:
=== 1. Transport-specific discovery and connection === Device -> Client : 某种形式的广播/信标(beaconing) Client -> Device : 客户端发起连接 === 2. Session Establishment(会话建立)=== Client -> Device : Get Version Request Device -> Client : Get Version Response Client -> Device : Session Setup Request Device -> Client : Session Setup Response ...(根据安全协议可能有一个或多个步骤)... === 3. Configuration(配置下发)=== Client -> Device : App-specific Set Config(可选) Device -> Client : Set Config Response(可选) Client -> Device : Wi-Fi/Thread SetConfig(SSID、Passphrase... / ThreadDataset) Device -> Client : Wi-Fi/Thread SetConfig response Client -> Device : Wi-Fi/Thread ApplyConfig cmd Device -> Client : Wi-Fi/Thread ApplyConfig resp Client -> Device : Wi-Fi/Thread GetStatus cmd(可重复) Device -> Client : Wi-Fi/Thread GetStatus resp(可重复) === 4. Close connection(关闭连接)=== Device -> Client : Close Connection从流程中可以看出两点设计思想:其一,版本协商(Get Version)发生在会话建立之前,客户端可以借此确认设备端固件协议版本是否匹配,这一点在代码层面由protocomm_set_version()注册的版本端点承担(见 components/protocomm/include/common/protocomm.h);其二,配置下发与状态查询被拆分为独立的命令,SetConfig只写入配置,ApplyConfig才真正生效,GetStatus支持反复轮询,从而让手机端可以可靠地跟踪配网进度,例如 AP 连接是否成功。
传输方式选型(Deciding on Transport)
统一配网子系统当前支持 Wi-Fi(SoftAP + HTTP 服务器)与蓝牙 LE(基于 GATT)两种传输方案。选择传输方式时需要重点权衡以下几点:
- BLE 的优势——通道保持完整:基于蓝牙 LE 的传输在配网期间能在设备与客户端之间维持一条完整的通信通道,从而保证可靠的配网反馈。
- BLE 的优势——App 内体验好:在 Android 和 iOS 上,手机 App 可以直接发现并连接设备,无需用户跳出 App 去系统设置里切换网络。
- BLE 的代价——内存开销:蓝牙 LE 传输在运行时大约消耗 110 KB 内存。如果产品在配网完成后不再使用蓝牙功能,这几乎全部内存都可以被回收并归还给堆(heap)。
- SoftAP 的优势——互操作性强,但有几点需要考虑:
- 设备使用同一块射频同时托管 SoftAP 并连接目标 AP,由于两者可能处于不同信道,可能导致手机无法可靠地收到连接状态更新;
- 手机(客户端)必须断开当前连接的 AP 才能连上设备的 SoftAP,只有等配网完成、SoftAP 关闭后,手机原网络才会恢复。
- SoftAP 的内存开销小:对于 Wi-Fi 使用场景,SoftAP 传输不需要太多额外内存。
- SoftAP 在 iOS 上的限制:iOS 系统下,基于 SoftAP 的配网要求用户进入"系统设置"手动连接设备托管的 Wi-Fi 网络,因为 iOS 应用无法使用扫描与连接 Wi-Fi 的 API。
安全方案选型(Deciding on Security)
无论选择哪种传输,配网过程中的数据安全都是必须考虑的。从配网安全角度,需要关注以下三点:
- 客户端发送给设备以及返回的配置数据必须被保护(加密);
- 客户端应能认证它连接到的设备;
- 设备制造商可以选择持有证明(Proof of Possession,PoP)——一个每台设备独有的秘密,由用户在配网客户端上输入,从而确保只有持有设备的人才能对设备进行配网。
安全方案分为两个层级,开发者可根据需求选择其一或组合使用:
- 传输层安全(Transport Security):对于 SoftAP 配网,可选择 WPA2 保护并配以每台设备独有的口令,该独有口令本身也可充当 PoP。对于蓝牙 LE,在评估其提供的安全等级后,可使用 "just-works" 安全作为传输层安全。
- 应用层安全(Application Security):如果应用不使用传输层安全,或传输层安全不足以满足需求,统一配网子系统提供应用层安全(即下文"安全方案详解"中的Security 1),通过 PoP 提供数据保护与认证。
设备发现(Device Discovery)
广告与设备发现的方式由应用自行决定,手机 App 与设备固件应用可以根据所选协议选择合适的广播与发现方法:
- 对于SoftAP + HTTP 传输,通常可以直接利用设备托管的 AP 的 SSID(网络名)进行发现;
- 对于蓝牙 LE 传输,可以利用广播中包含的设备名称(device name)、主服务(primary service),或两者组合进行发现。
架构解析(Architecture)
统一配网的整体架构如下图所示,其核心是底层的protocomm框架:
架构要点如下:
- protocomm 是基础层:它为安全方案和传输机制提供框架。Network Provisioning 层(即上层配网逻辑,位于 protocomm 组件之外的 idf-extra-components 的 network_provisioning 中)借助 protocomm 向应用提供简单的回调,用于设置配置与获取网络状态。回调的具体实现由应用掌控。
- 应用可直连 protocomm:除了使用上层配网逻辑,应用也可以直接使用 protocomm 注册自定义处理器(handler)。
- 实例与端点的概念:应用创建一个 protocomm 实例,并将其映射到特定的传输与特定的安全方案。在 protocomm 中,每个传输都有一个"端点(end-point)"概念,端点是某种特定类型信息通信的逻辑通道。例如,安全握手发生在与网络配置端点不同的端点上。
- 端点的标识方式随传输而变:每个端点用字符串标识,但不同传输对端点的内部表示不同——在 SoftAP + HTTP 传输下,端点对应 URI;在蓝牙 LE 下,端点对应具有特定 UUID 的 GATT 特征。开发者可以创建自定义端点,并为通过该端点收发数据实现处理器。
从源码看,protocomm 的核心 API 定义在 components/protocomm/include/common/protocomm.h,包括:
| API | 作用 |
|---|---|
protocomm_new()/protocomm_delete() | 创建 / 销毁一个 protocomm 实例 |
protocomm_add_endpoint()/protocomm_remove_endpoint() | 为指定端点名字绑定 / 解绑请求处理器(含私有数据) |
protocomm_set_security()/protocomm_unset_security() | 为端点绑定 / 移除安全会话建立器(security instance) |
protocomm_set_version()/protocomm_unset_version() | 设置 / 移除用于版本校验的端点 |
protocomm_open_session()/protocomm_close_session() | 为传输会话分配 / 释放内部资源 |
protocomm_req_handle() | 调用某个端点会话已注册的处理器处理输入数据并生成响应 |
protocomm_get_sec_version() | 获取实例的安全版本号与补丁版本号 |
端点处理器的函数原型为protocomm_req_handler_t,接收会话 ID、输入缓冲、输出缓冲与私有数据指针。安全对象则抽象为 protocomm_security.h 中的protocomm_security_t结构体,其中包含版本号ver、补丁版本patch_ver,以及init、cleanup、new_transport_session、close_transport_session、security_req_handler、encrypt、decrypt等一组函数指针——新增一种安全方案,本质就是实现这一组函数。
安全方案详解(Security Schemes)
当前统一配网支持以下三种安全方案,分别对应 protocomm 中的SecSchemeVersion枚举(见 components/protocomm/proto/session.proto):
| 方案 | 密钥交换 | 数据加密 | 说明 |
|---|---|---|---|
| Security 0 | 无 | 无 | 明文通信,仅建议开发测试使用 |
| Security 1 | Curve25519 | AES-256-CTR | 支持 PoP 授权 / Null PoP 两种模式 |
| Security 2 | SRP6a(RFC 5054) | AES-256-GCM | 基于用户名与口令,新设计推荐 |
注意:各安全方案需要通过项目配置菜单启用,详见下文"如何启用安全方案"一节。
Security 0:无安全
Security 0 不提供任何加密与认证(No security / No encryption),仅在开发与测试阶段使用,不应在生产环境使用。在 components/protocomm/Kconfig 中,它对应的配置项ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0默认关闭,并明确提示"This version provides no encryption or authentication and should not be used in production"。
Security 1:Curve25519 + AES-256-CTR
Security 1 基于 Curve25519 密钥交换、共享密钥推导,并使用 AES-256-CTR 模式对数据进行加密。它支持两种模式:
- Authorized(授权模式):使用 Proof of Possession(PoP)字符串来授权会话并推导共享密钥;
- No Auth(Null PoP):仅通过密钥交换推导共享密钥,不进行 PoP 校验。
Security 1 的完整握手流程如下(cli为客户端,dev为设备):
1. Client: 生成密钥对 {cli_privkey, cli_pubkey} = curve25519_keygen() 2. Client -> Device: SessionCmd0(cli_pubkey) 3. Device: 生成密钥对 {dev_privkey, dev_pubkey} = curve25519_keygen() dev_rand = gen_16byte_random() // 16 字节随机数作为初始向量 shared_key(No PoP) = curve25519(dev_privkey, cli_pubkey) shared_key(with PoP)= curve25519(dev_privkey, cli_pubkey) ^ SHA256(pop) 4. Device -> Client: SessionResp0(dev_pubkey, dev_rand) 5. Client: shared_key(No PoP) = curve25519(cli_privkey, dev_pubkey) shared_key(with PoP)= curve25519(cli_privkey, dev_pubkey) ^ SHA256(pop) cli_verify = aes_ctr_enc(key=shared_key, data=dev_pubkey, nonce=dev_rand) 6. Client -> Device: SessionCmd1(cli_verify) 7. Device: 校验 (dev_pubkey == aes_ctr_dec(cli_verify, ...)) dev_verify = aes_ctr_enc(key=shared_key, data=cli_pubkey, nonce=(prev-context)) 8. Device -> Client: SessionResp1(dev_verify) 9. Client: 校验 (cli_pubkey == aes_ctr_dec(dev_verify, ...))可以看到,Security 1 通过双方各自生成的 Curve25519 密钥对完成共享密钥协商;启用 PoP 时,共享密钥还会异或上SHA256(pop),使只有知道该设备独有秘密的客户端才能推导出正确的密钥。随后双方分别用共享密钥加密对方公钥作为验证令牌,完成双向认证。
这一流程在协议消息层面的定义见 sec1.proto:SessionCmd0携带client_pubkey,SessionResp0携带status、device_pubkey、device_random,SessionCmd1携带client_verify_data,SessionResp1携带status、device_verify_data。设备端实现位于 components/protocomm/src/security/security1.c,PoP 参数通过 protocomm_security.h 中的protocomm_security1_params_t(data指针 +len长度)传入protocomm_set_security()。仓库还提供了对应的握手测试 test_security1.c,可用于验证 PoP 正确、PoP 错误等场景下的行为。
Security 2:SRP6a + AES-256-GCM
Security 2 基于安全远程口令协议 SRP6a(见 RFC 5054)。该协议要求事先利用标识用户名I和明文口令p生成 Salt 与 Verifier:
- Salt s:256 位随机值;
- Verifier v:
v = g^x,其中x = H(s | I | p),H为哈希函数,g为生成元。
Salt 与 Verifier 随后存储在设备(ESP)上,而口令p和用户名I需要通过适当途径(例如二维码贴纸)提供给手机 App(配网实体)。
Security 2 的完整握手流程如下:
1. Client: a (cli_privkey) = 256 位随机值 A (cli_pubkey) = g^a (g 为生成元,N 为大型安全素数,所有运算在模 N 的整数环中进行) 2. Client -> Device: SessionCmd0(cli_pubkey A, username I) 3. Device: 从存储中取得 Salt s 与 Verifier v(v = g^x, x = H(s|I|p)) b (dev_privkey) = 256 位随机值 B (dev_pubkey) = k*v + g^b,其中 k = H(N, g) Shared Key K = H(S),其中 S = (A * v^u) ^ b,u = H(A, B) 4. Device -> Client: SessionResp0(dev_pubkey B, dev_rand) 5. Client: shared_key(K) = H(S),其中 S = (B - k*v) ^ (a + ux) u = H(A, B),k = H(N, g),v = g^x,x = H(s | I | p) client_proof M1 = H[H(N) XOR H(g) | H(I) | s | A | B | K] 6. Client -> Device: SessionCmd1(client_proof M1) 7. Device: 计算 M1 = H[H(N) XOR H(g) | H(I) | s | A | B | K] 并与客户端发来的 M1 比对 device_proof M2 = H(A, M1, K) dev_rand = gen_12byte_iv()(session_id 8 字节 + counter 4 字节, 用于 AES-GCM 加解密) 8. Device -> Client: SessionResp1(device_proof M2, dev_rand) 9. Client: 计算 device_proof M2 = H(A, M1, K) 并与设备发来的 M2 比对SRP6a 的核心优势在于:口令p不会在网络上传输,设备端只存储 Salt 与 Verifier;即便 Verifier 泄露,攻击者也无法直接得到口令。客户端与服务端各自推导出相同的共享密钥 K 后,再通过 M1/M2 两个证明值完成双向认证。
协议消息层面定义见 sec2.proto:S2SessionCmd0携带client_username与client_pubkey,S2SessionResp0携带status、device_pubkey、device_salt,S2SessionCmd1携带client_proof,S2SessionResp1携带status、device_proof、device_nonce。设备端实现位于 components/protocomm/src/security/security2.c,其中 SRP 数学运算封装在 components/protocomm/src/crypto/srp6a/esp_srp.c;Salt 与 Verifier 参数通过 protocomm_security.h 中的protocomm_security2_params_t(salt/salt_len、verifier/verifier_len)传入。测试用例见 test_security2.c 与 test_srp.c。
Security 2 的 AES-GCM IV 处理
Security 2 使用 AES-GCM 进行数据加解密,其初始化向量(IV)由8 字节会话 ID(session_id)与4 字节计数器(counter)组成,共 12 字节。计数器从 1 开始,设备与客户端每次加解密操作后都会递增。具体流程如下:
1. Device: Initial IV = session_id (8 字节) || counter (4 字节) session_id = 随机 8 字节值 counter = 0x1(按大端序存储) 2. Device -> Client: 发送 12 字节 IV(session_id || counter) 3. Client: 以设备下发的 IV 为初始值:session_id(来自设备)、counter = 0x1 4. Client -> Device: 使用初始 IV 加密第一条命令 5. Client: 第一条命令发送后 counter 递增为 0x2,新 IV = session_id || counter 6. Device: 发送第一条响应前 counter 递增为 0x2,新 IV = session_id || counter 7. Device -> Client: 使用更新后的 IV 加密响应这种"8 字节会话 ID + 4 字节计数器"的 IV 设计,既保证了同一会话内每次加解密使用不同的 IV(避免 AES-GCM 随机数重用导致的安全风险),又通过会话 ID 区分不同会话,是安全实现中的一个关键细节。
如何启用安全方案
各安全方案需要先在项目配置菜单中启用,相关选项位于 components/protocomm/Kconfig 的 "Protocomm" 菜单下:
| 配置项 | 默认值 | 说明 |
|---|---|---|
ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0 | n | 支持 Security 0(无加密认证),仅限开发测试 |
ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_1 | n | 支持 Security 1(Curve25519 + AES-CTR),文档建议新设计优先考虑 Security 2 |
ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2 | y | 支持 Security 2(SRP6a + AES-GCM),默认开启 |
ESP_PROTOCOMM_SUPPORT_SECURITY_PATCH_VERSION | y | 隐藏选项,供外部组件判断是否支持安全补丁版本及protocomm_get_sec_versionAPI |
关闭不用的安全方案可以节省固件代码体积。另外,如果使用蓝牙 LE 传输,Kconfig 还提供了ESP_PROTOCOMM_KEEP_BLE_ON_AFTER_BLE_STOP(调用protocomm_ble_stop后保持蓝牙开启)与ESP_PROTOCOMM_DISCONNECT_AFTER_BLE_STOP(终止连接)两个与 BLE 生命周期相关的隐藏选项。
示例代码与配网工具(Sample Code & Provisioning Tools)
示例与测试代码
在仓库内,protocomm 自带的测试应用是理解框架用法的直接入口:
- components/protocomm/test_apps/main/test_protocomm.c:覆盖端点注册、会话打开/关闭、请求处理等核心 API 的行为;
- components/protocomm/test_apps/main/test_security1.c:Security 1 的握手与 PoP 授权验证;
- components/protocomm/test_apps/main/test_security2.c 与 test_srp.c:Security 2 的 SRP6a 握手与 GCM 加解密验证;
- components/protocomm/test_apps/sdkconfig.defaults:测试工程的安全方案启用配置参考。
完整的应用实现示例(Wi-Fi/Thread 配网应用)由 idf-extra-components 仓库中的 network_provisioning 组件提供,包含配网示例与用于调试的 Python 命令行工具 esp_prov;上层配网逻辑(Network Provisioning 层)正是通过 protocomm 提供的回调来设置配置并获取网络状态的。protocomm 组件本身还提供了 BLE、HTTPD、控制台三种传输的 API 头文件(components/protocomm/include/transports/),可供直接查阅。
配网工具
官方为各个平台提供了配网应用及对应源代码:
- Android:提供蓝牙 LE 配网 App 与 SoftAP 配网 App(均可在应用商店搜索下载),源码仓库为 esp-idf-provisioning-android;
- iOS:提供 ESP BLE Provisioning 与 ESP SoftAP Provisioning 两款 App(App Store 可下载),源码仓库为 esp-idf-provisioning-ios;
- Linux / macOS / Windows:提供
esp_prov——一个基于 Python 的命令行配网工具(位于 network_provisioning 组件的 tool/esp_prov 目录)。
手机 App 提供简洁的 UI,更面向普通用户;而命令行工具适合开发者作为调试工具使用。
小结
统一配网框架以 protocomm 为基础层,将"传输"与"安全"两大维度彻底解耦:传输端抽象出端点的概念(HTTP 对应 URI、BLE 对应 GATT 特征),安全端抽象出统一的protocomm_security_t接口。上层无论是官方 network_provisioning 还是你自己的自定义配网逻辑,都只需基于这套抽象做组合。本文梳理的四阶段配网流程、三种安全方案的握手细节、Security 2 的 IV 处理以及 Kconfig 启用方法,构成了你在 ESP-IDF 项目中落地配网功能所需的核心知识——接下来,可以直接参考上述测试用例与头文件,在自己的固件里实现一套"传输 + 安全 + 自定义端点"的配网方案。
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考