DPP 扫码配网——从 App 扫二维码到凭据落库
新搬来的住户没有钥匙,物业也不用上门。住户把自家门上贴的 "门禁凭证"(二维码)给物业扫一下,物业核验身份后发给他一张 "门禁卡"(Connector)和一把 "房门钥匙"(Wi-Fi 密码)——全程没有输过一次密码。本文追踪这场 "无密码配网" 在代码里怎么跑通:从 App 扫码到 wpa_supplicant 把凭据写进网络配置库,跨过 Java Framework 与 supplicant C 两个代码世界。
本章导读
DPP(Device Provisioning Protocol,设备配网协议)是 Wi-Fi Alliance 推出的 "扫码配网" 协议,商标名 Wi-Fi Easy Connect。它要解决的是智能家居最痛的问题:无屏设备(灯泡、插座、摄像头)怎么连上家里的 Wi-Fi。传统 WPS 靠 PIN 码或按按钮,安全性饱受诟病(PIN 可被暴力破解);DPP 换成了一套基于公钥密码学的四阶段流程——扫码拿到对方公钥,用 ECDH(Elliptic Curve Diffie-Hellman,椭圆曲线 Diffie-Hellman)交换出会话密钥,在加密隧道里下发 Wi-Fi 凭据。
本文把这条链完整走一遍,分两条路:
- 正向下发:App 扫二维码 →
WifiManager→DppManager→WifiNative→ AIDL →wpa_supplicant→ 解析 URI →dpp_auth_init→ 三帧认证握手 → GAS 配置交换 →wpas_dpp_add_network把凭据写进 supplicant 网络库。 - 反向回报:supplicant 发
DPP-NETWORK-ID事件 → AIDL 回调 →SupplicantStaIfaceCallbackAidlImpl→DppManager的onSuccessConfigReceived→WifiConfigManager落库 →EasyConnectCallbackProxy→ App 的EasyConnectStatusCallback。
本文只讲扫码配网的主干。 DPP-over-TCP(规范 §2.3)、Enterprise provisioning 802.1X(§4.5)、Network Access 的二次握手(§6.6.6)、以及驱动侧帧收发都不展开——DPP 走的是 802.11 通用 Action / GAS 帧,驱动没有 DPP 特化逻辑,到 offchannel_send_action 就离开本文视线了。
手机上的扫一扫可不一定是这玩意儿,那就是单纯解析二维码,你用微信扫一扫都能看出来密码是啥
先看这张全链路分层图,理解扫码配网要跨越的每一层:
1 一张二维码里到底装了什么?
二维码不是 "密码",而是一张 "门禁凭证"——里面是公钥、信道列表和 MAC 地址,扫它的人拿到的是 "这位住户的身份信息",不是开门的钥匙。
DPP 的配网哲学和 WPS 完全不同。WPS 是 "我知道 PIN / 按了按钮,所以我们能配"——安全建立在共享秘密上,PIN 只有 8 位数字,暴力破解只需 ~11000 次尝试(Reaver 攻击)。DPP 是 "我扫码拿到了你的公钥,我们用公钥密码学建立信任"——安全建立在椭圆曲线密码学上,扫描二维码只是拿到对方的身份,真正的信任要通过后面的认证握手建立。
1.1 DPP URI:一串自描述的公钥凭证
扫码得到的字符串叫 DPP URI,格式在规范 §5.2.1(Bootstrapping Information Format)定义。一个典型的 DPP URI 长这样:
1 | DPP:K:MDwwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeyU...;C:81/1,2,3,4,5,6,7,8,9,10,11;M:8cf9420df54c;; |
分号分隔的字段每个以「单字母: 值」出现,规范 §5.2.1 的 ABNF 规则里列出了保留字段:
| 字段 | 含义 | 规范 §5.2.1 |
|---|---|---|
K: | Bootstrapping Public Key,公钥(DER 编码的 ASN.1 SubjectPublicKeyInfo 做 base64) | public-key = "K:" *PKCHAR |
C: | Channel List,全局 operating class / channel 列表,如 81/1,2,3,4,5,6,7,8,9,10,11 | channel-list = "C:" ... |
M: | MAC 地址,6 字节十六进制,如 8cf9420df54c | mac = "M:" 12HEXDIG |
I: | Info,设备描述信息(可选,Android 的 deviceInfo 参数) | information = "I:" *VCHAR |
V: | Version,DPP 版本(R2+ 要求带) | version = "V:" ... |
B: | Supported Curves,支持的椭圆曲线位图 | supported-curves = "B:" 1*HEXDIG |
末尾的 ;; 是 ABNF 里的终止符。这段字符串的本质是:设备把自己的公钥 + 一张 "我大概会在哪些信道出现" 的小地图 + 自己的 MAC 地址,打印成二维码贴在身上。谁扫到它,谁就拿到了发起配网的入口信息。
1.2 wpa_supplicant 怎么解析这串 URI
URI 的解析入口在 dpp_parse_uri()(src/common/dpp.c:432)。它按分号循环切字段,识别出 C:、M:、K:、I:、V:、B:、H: 后分别交给各自的解析函数:
1 | // src/common/dpp.c:432 |
关键设计点:
K:是强制字段,其他都可选。URI 没有公钥直接拒绝——没有身份信息,后续 ECDH 无从谈起。- 未识别字段静默跳过。规范 §5.2.1 明确要求 "解析时忽略未知的分号分隔组件",这是前向兼容设计——未来加新字段,老设备扫到新二维码不会崩,只是用不上新字段。
- 解析结果装进
struct dpp_bootstrap_info,存进 supplicant 的 bootstrapping table,返回一个整数 ID。这个 ID 就是后面所有命令引用 "这个二维码" 的句柄。
解析出来的 struct dpp_bootstrap_info 是关键数据结构,它的 pubkey(对方的公钥)和 chan_list(对方出现的信道)在后面认证握手时会反复用到。
2 App 扫码后 Framework 做了什么?
物业前台收到住户的门禁凭证,先登记在案(加 URI),再通知保安部开始核验(发起认证)。Framework 的
DppManager就是那个 "前台"——它是整个 DPP 会话的单一收口点。
上一节二维码的内容解析发生在 supplicant 侧。但用户扫码的动作发生在 App 侧,App 怎么把 URI 送到 supplicant?中间隔了 Java Framework 和 AIDL(Android Interface Definition Language,Android 接口定义语言)两层。
2.1 App 入口:WifiManager.startEasyConnectAsConfiguratorInitiator
App 侧(通常是系统设置或厂商配网 App)调用 WifiManager 的 Easy Connect 系列方法。Android 上 Easy Connect 是 @SystemApi,需要 NETWORK_SETTINGS 或 NETWORK_SETUP_WIZARD 权限,普通第三方 App 调不了。
以 "Configurator 扫码给 Enrollee 配网" 为例:
1 | // framework/java/android/net/wifi/WifiManager.java:9474 |
要点:
- 参数
enrolleeUri是扫码得到的 DPP URI,selectedNetworkId是 Configurator 要 "发给对方" 的本地网络(即配网的目标 Wi-Fi),enrolleeNetworkRole告诉对方你配好网后是当 STA 还是当 AP。 new EasyConnectCallbackProxy(executor, callback)把 App 的回调包了一层 Binder 代理,跨进程传给系统服务。App 侧的回调接口是EasyConnectStatusCallback。- 还有一个
startEasyConnectAsEnrolleeInitiator(WifiManager.java:9502):反过来,本机是 Enrollee,扫码拿到的是 Configurator 的 URI,请求对方给自己发配置。它走同一套 Framework 链——WifiServiceImpl.startDppAsEnrolleeInitiator(WifiServiceImpl.java:6697)→DppManager.startDppAsEnrolleeInitiator(DppManager.java:408)→WifiNative.startDppEnrolleeInitiator(WifiNative.java:3269),到 supplicant 侧的 AIDL 实现里,命令字符串拼的是role=enrollee而非role=configurator。后面的认证握手、GAS 收配置、onSuccessConfigReceived落库整条链都复用,只有 "谁发配置、谁收配置" 的方向相反。
2.2 系统服务:WifiServiceImpl 转发到 DppManager
mService 是 IWifiManager 的 Binder 服务端,即 WifiServiceImpl。它在 startDppAsConfiguratorInitiator 里做权限检查后,把调用投递到 WiFi 线程,转给 DppManager:
1 | // service/java/com/android/server/wifi/WifiServiceImpl.java:6653 |
DppManager 是 com.android.server.wifi 包下的单个类(不是子包),整个 DPP 会话的状态都收在它内部——这是本仓库源码与老博客的一个关键差异:Framework 没有 wifi/dpp/ 子包,DppConfigurator、DppEnrollee 那些 "多类拆分" 的写法是旧版 / 其他分支的,当前代码是单类 DppManager 集中管理。
2.3 DppManager:单会话的 "配网前台"
DppManager 的核心设计是同一时刻只允许一个 DPP 会话。它内部用一个 DppRequestInfo 对象记录当前会话的所有状态,isSessionInProgress() 就是检查这个对象是否为 null:
1 | // service/java/com/android/server/wifi/DppManager.java:225 |
会话登记完成、超时计时器启动后,函数把要下发的 Wi-Fi 凭据编码进命令参数,然后发起认证:
1 | // Auth init |
这段代码是 Framework 侧下发的核心,拆开看:
- 单会话锁:
isSessionInProgress()为真就立刻回FAILURE_BUSY。配网是重操作,同一时刻只有一个 App 能发起,避免两个会话在 supplicant 侧互相踩踏。 linkToDeath:注册 Binder 死亡回调。如果发起配网的 App 进程挂了,binderDied()里会主动stopDppInitiator并清理会话——防止 App 崩溃后 supplicant 里留一个永远超时的认证。- 超时计时器:
DPP_TIMEOUT_MS = 40_000(40 秒,DppManager.java:75)。Initiator 的认证必须在 40 秒内完成,超时触发timeoutDppRequest()→onFailure(TIMEOUT)。注意 Enrollee-Responder 用的是DPP_RESPONDER_TIMEOUT_MS = 300_000(5 分钟)——Responder 是 "被动等待对方来找我",等的时间理应更长(DppManager.java:76)。 - 两步下发:先
addDppPeerUri把 URI 交给 supplicant 解析、拿到 peer ID;再startDppConfiguratorInitiator用这个 peer ID 发起认证。两步之间把要下发的 Wi-Fi 凭据(SSID、passphrase/PSK、AKM、网络角色)一起传给 supplicant。
DppRequestInfo 是 DppManager 的私有静态内部类(DppManager.java:635),字段如下:
1 | // service/java/com/android/server/wifi/DppManager.java:635 |
authRole 三值(DppManager.java:77-79):DPP_AUTH_ROLE_INACTIVE = -1、DPP_AUTH_ROLE_INITIATOR = 0、DPP_AUTH_ROLE_RESPONDER = 1。它决定了会话结束时清理 supplicant 资源的方式——Initiator 用 removeDppUri 删 URI,Responder 用 stopDppResponder 停监听。
3 参数怎么跨过 AIDL 边界从 Java 到 C?
前台把 "门禁凭证" 和 "配网指令" 写在一张工单上,交给物业内部的安检部门——工单穿越 Java 世界和 C 世界的边界靠的是 AIDL。
DppManager 调用的 mWifiNative 是 Framework 与 supplicant 之间的门面。它本身不干活,只是把调用委托给 SupplicantStaIfaceHal——真正的 Binder 客户端。
3.1 WifiNative:纯转发层
WifiNative 这两个 DPP 方法是一行 return 的纯转发——参数原样交给 mSupplicantStaIfaceHal,真正的跨进程 Binder 调用在下一层才发生:
1 | // service/java/com/android/server/wifi/WifiNative.java:3216 |
3.2 SupplicantStaIfaceHal:HAL 门面
SupplicantStaIfaceHal 是 Framework 侧对 supplicant HAL 的封装。它有 SupplicantStaIfaceHalHidlImpl(HIDL)和 SupplicantStaIfaceHalAidlImpl(AIDL)两个实现,当前 Android 版本默认走 AIDL。addDppPeerUri 在这层做了空检查后转给 AIDL 实现:
1 | // service/java/com/android/server/wifi/SupplicantStaIfaceHal.java:2028 |
AIDL 实现里才是真正跨进程的 Binder 调用——iface 是 ISupplicantStaIface 的 Binder 代理,iface.addDppPeerUri(uri) 把参数序列化后跨进程送到 wpa_supplicant 进程:
1 | // service/java/com/android/server/wifi/SupplicantStaIfaceHalAidlImpl.java:3328 |
3.3 AIDL 边界:ISupplicantStaIface → sta_iface.cpp
AIDL 的 ISupplicantStaIface.addDppPeerUri(uri) 在 supplicant 侧的实现在 wpa_supplicant/aidl/vendor/sta_iface.cpp。这里是 Java 世界和 C 世界的分界线——参数通过 Binder 传入,进入 StaIface::addDppPeerUri,然后调用 supplicant 核心的 wpas_dpp_qr_code:
1 | // wpa_supplicant/aidl/vendor/sta_iface.cpp:1505 |
这个函数揭示了 "加 URI" 的真实含义:它不是把字符串存起来,而是直接调用 wpas_dpp_qr_code() 解析 URI 并登记到 bootstrapping table,返回的 id 就是 struct dpp_bootstrap_info 在表里的索引。Framework 拿到的 peer ID,本质就是这份 URI 解析结果在 supplicant 里的编号。
再看认证发起的 AIDL 实现——startDppConfiguratorInitiatorInternal 把 Framework 传来的结构化参数拼装成一条控制接口命令字符串,再喂给 wpas_dpp_auth_init:
1 | // wpa_supplicant/aidl/vendor/sta_iface.cpp:1541 |
设计意图:
- AIDL 层故意复用 supplicant 的 "命令字符串" 接口,而不是新建结构化接口。这让 AIDL 层保持很薄——它只是把参数序列化成
cmd字符串,真正的解析逻辑全在 supplicant 已有的wpas_dpp_auth_init里。Android 的SupplicantStaIfaceHal每个新特性都可以用这个模式接入,不需要改 supplicant 核心 API。 conf=参数编码了完整的配网目标:sta-psk、sta-sae、sta-dpp、ap-psk等,对应DppNetRole(STA/AP)×DppAkm(PSK/SAE/DPP)的组合。conn_status=1是 DPP R2 的特性——Configurator 要求 Enrollee 配网后回报连接状态(规范 §6.4.5 Connection Status Result)。- DPP-AKM 的特殊处理:如果
security_akm == DppAkm::DPP,说明 Configurator 要下发的是 "DPP Connector"(门禁卡)而不是密码,此时会先dpp_configurator_add创建 / 加载 configurator 私钥,再把这个 configurator 的 ID 拼进命令。
至此,参数跨过了 Java/C 边界,进入了 supplicant 的核心世界。下一节看 wpas_dpp_qr_code 和 wpas_dpp_auth_init 在 C 世界里怎么落地。
4 supplicant 怎么把二维码变成身份信息?
安检部门收到工单,先翻开 "登记簿"——把门禁凭证上的公钥、信道、MAC 逐项登记,发一个登记号。后面所有对话都用登记号指代这位住户。
4.1 wpas_dpp_qr_code:登记门禁凭证
wpas_dpp_qr_code 是二维码 URI 进入 supplicant 的入口(wpa_supplicant/dpp_supplicant.c:76)。它调用 dpp_add_qr_code 完成解析和登记:
1 | // wpa_supplicant/dpp_supplicant.c:76 |
两个非显而易见的点:
response_pending分支:如果本机正在等一个 "需要额外信息的响应"(DPP R2 的 RESPONSE_PENDING 机制,规范 §6.3.3),这时扫到新二维码可能正好补上缺的公钥,于是直接补发 pending 的认证响应。这是 DPP 为 "Enrollee 不带公钥、需要 Configurator 先扫码补信息" 的场景准备的特殊路径。- 返回
bi->id:这个整数 ID 贯穿整个会话——Framework 的mDppRequestInfo.peerId、后续wpas_dpp_auth_init的peer=参数、以及清理时的removeDppUri,全都用这个 ID。
dpp_add_qr_code 内部就是我们在 §1.2 看到的 dpp_parse_uri 加上登记表操作:
1 | // src/common/dpp.c:4437 |
dpp_add_qr_code 直接调用 dpp_parse_uri 完成 URI 解析,然后做登记:标记类型为 DPP_BOOTSTRAP_QR_CODE、分配 ID(dpp_next_id)、挂进 dpp->bootstrap 列表。这里没有显式的公钥合法性校验步骤——它发生在 dpp_parse_uri_pk(§1.2 里 K: 字段的解析函数)内部,也就是规范 §3.3.1 要求的公钥验证在解析时就已经完成。
4.2 认证准备:dpp_auth_init
登记号拿到后,认证发起进入 wpas_dpp_auth_init(dpp_supplicant.c:846)。它解析命令字符串里的 peer=、own=、role=、netrole=、neg_freq= 等参数,然后调用核心的 dpp_auth_init:
1 | // wpa_supplicant/dpp_supplicant.c:846 |
注意状态机的存储方式:这里没有 enum dpp_state。当前版本的 supplicant 里,DPP 认证的状态不是一个大枚举,而是 struct dpp_authentication 里的一组布尔标志位。这是本仓库与老博客的第二个关键差异——老代码有个 enum dpp_state 列举 DPP_STATE_AUTH_REQ 等状态,当前源码已经重构掉了。
4.3 struct dpp_authentication:布尔标志位状态机
struct dpp_authentication 定义在 src/common/dpp.h:286。它同时承担 "认证状态" 和 "密钥材料" 两个角色:
1 | // src/common/dpp.h:286 |
为什么用布尔标志位而不是枚举状态机? 因为 DPP 的认证流程不是一条简单的线性状态链——Initiator 和 Responder 各有自己的等待点,且可能交织:waiting_auth_resp(等 Auth Response)、waiting_auth_conf(等 Auth Confirm)、waiting_conf_result(等 Config Result)、waiting_conn_status_result(等 Connection Status Result)这些是可以同时为真的(比如等 Config Result 的同时记录 auth 已成功)。用一个枚举只能表达 "当前在哪个状态",布尔位可以表达 "我在等哪几件事"。这是 DPP 认证相比 WPS 的复杂性——多轮帧交换 + 多个可重试 / 可中断的等待点,用状态位组合比线性状态机更灵活。
5 三帧握手:Auth Request → Response → Confirm 怎么交换?
物业和住户开始对暗号。暗号分三轮:第一轮物业说 "我是物业,我要给某位住户发门禁卡";第二轮住户说 "我是该住户,这是我的身份证明";第三轮双方确认 "对上了,建立安全通道"。
5.1 Auth Request:Initiator 的第一句话
dpp_auth_init(src/common/dpp_auth.c:1161)是认证的真正起点。它完成三件关键的事:生成临时协议密钥对、计算 ECDH 共享密钥、构造 Auth Request 帧:
1 | // src/common/dpp_auth.c:1161 |
I-nonce 和协议密钥对就绪后,认证的密码学核心是 ECDH——用 Initiator 临时私钥和对端 bootstrapping 公钥算出一个共享点,再派生后续密钥:
1 | // ECDH: M = pI * BR(Initiator 私钥 × 对端 bootstrapping 公钥) |
密码学要点:
- ECDH 共享密钥:
M = pI * BR——Initiator 的临时私钥pI乘以对端 bootstrapping 公钥BR。这个 M 是双方共有的秘密(Responder 侧用pR * BI会得到同一个点),后续所有密钥(k1、k2、ke)都由它派生。注意这里没有输密码、没有共享秘密——只有 "我知道你的公钥,你扫了我的二维码知道了我的公钥"。 - I-nonce:Initiator 随机生成 32 字节 nonce,放在 Auth Request 里发给对方,用于防止重放攻击(Replay Protection,规范 §6.3.2)。
auth->initiator = 1和auth->waiting_auth_resp = 1:这两个布尔位就是状态机——"我是发起方,正在等对方的 Auth Response"。
5.2 帧的发送与三帧全流程
Auth Request 帧构造完成后,wpas_dpp_auth_init_next(dpp_supplicant.c:753)负责把它发出去。它按信道列表逐信道尝试:从 auth->freq[] 里取出下一个频率,调用 offchannel_send_action 发送 Action 帧,同时注册一个 2 秒的等待超时:
1 | // wpa_supplicant/dpp_supplicant.c:753 (节选) |
offchannel_send_action 是 supplicant 发 802.11 Action 帧的通用入口——DPP 帧复用 802.11 标准的 Public Action 帧机制,驱动没有任何 DPP 特化代码。这正是文章开头边界声明里说的 "DPP 走通用 Action/GAS 帧"。
DPP 认证的三帧(规范 §6.3)在源码中的对应:
| 帧 | 规范 | 收 / 发函数 |
|---|---|---|
| Authentication Request | §6.3.2 | 发送:wpas_dpp_auth_init_next;接收:wpas_dpp_rx_auth_req(在 hostapd/AP 侧是另一条路) |
| Authentication Response | §6.3.3 | 发送:dpp_auth_req_rx(dpp_auth.c:668,Responder 构造并回);接收:wpas_dpp_rx_auth_resp(dpp_supplicant.c:2109)→ dpp_auth_resp_rx(dpp_auth.c:1403) |
| Authentication Confirm | §6.3.4 | 发送:dpp_auth_build_conf(dpp_auth.c:956,Initiator 构造并回);接收:dpp_auth_conf_rx(dpp.h:625) |
三帧全部通过后,dpp_notify_auth_success(dpp.c:5123)发出 DPP-AUTH-SUCCESS 事件,同时 wpas_dpp_auth_success(dpp_supplicant.c:2085)决定下一步走 GAS 的哪一侧:
1 | // wpa_supplicant/dpp_supplicant.c:2085 |
注意这里的角色分岔:认证成功后,Configurator 启动 GAS server(等 Enrollee 来要配置),Enrollee 启动 GAS client(去 Configurator 要配置)。认证握手确定了双方的身份,但配网信息(Wi-Fi 凭据)还没下发——那要等 GAS 里的 Config Request/Response 交换。
6 配置怎么通过 GAS 里的 Config Request / Response 下发?
暗号对上了,接下来是 "发钥匙"。住户通过安全通道向物业申请 "房门钥匙",物业在加密信封里把钥匙交给他。这个 "加密信封" 就是 GAS 帧。
6.1 安全通道里传什么
认证握手结束时,双方已经从共享秘密派生出了会话密钥 ke(Key Encryption key)。此后所有配置交换都用 ke 加密——这保证了 Wi-Fi 密码永远不会明文出现在空中(对比 WPS 的 PIN 泄露风险)。
DPP Configuration 协议(规范 §6.4)在 GAS 里交换两条消息:
| 消息 | 方向 | 规范 | 源码 |
|---|---|---|---|
| DPP Configuration Request | Enrollee → Configurator | §6.4.2 | dpp_conf_req_rx(dpp.c:2325,Configurator 侧接收) |
| DPP Configuration Response | Configurator → Enrollee | §6.4.3 | dpp_build_conf_resp(dpp.c:2081,Configurator 侧构造);dpp_conf_resp_rx(dpp.c:3374,Enrollee 侧接收) |
6.2 Enrollee 收到的配置对象:不只是密码
Config Response 里装的是 Configuration Object(规范 §4.5)——一个 JSON 结构,除了 SSID 和密码,还可能是门禁卡(Connector)。Enrollee 侧收到后,dpp_conf_resp_rx 用 ke 解密、解析出配置对象:
1 | // src/common/dpp.c:3374 |
AES-SIV 是一种 "同时加密和认证" 的模式(deterministic authenticated encryption)——它不只用密钥 ke,还绑定了 "额外关联数据"(AD,即帧头的一部分),解密失败直接认定帧被篡改。这里能看到 DPP 对加密模式的选型:SIV 模式天然抗重放、抗错误误用,适合这种 "每个帧独立加解密" 的轻量协议,而不像 TLS 那样需要完整的 record 层状态。
6.3 凭据落库:wpas_dpp_add_network
dpp_conf_resp_rx 成功后,wpas_dpp_gas_resp_cb(dpp_supplicant.c:1890)遍历收到的每个配置对象,调用 wpas_dpp_handle_config_obj → wpas_dpp_process_config → wpas_dpp_add_network,把凭据写进 supplicant 的网络配置库:
1 | // wpa_supplicant/dpp_supplicant.c:1395 |
这步是 "凭据落库" 的最底层——它把配置对象翻译成 struct wpa_ssid(supplicant 的网络配置块),字段映射如下:
| 配置对象字段 | wpa_ssid 字段 | 含义 |
|---|---|---|
| SSID | ssid / ssid_len | 网络名 |
| passphrase / psk | passphrase / psk | PSK/SAE 的密码 |
| connector | dpp_connector | DPP Connector(门禁卡),对应 key_mgmt = WPA_KEY_MGMT_DPP |
| c_sign_key | dpp_csign | Configurator 签名公钥,Connector 验签用 |
| net_access_key | dpp_netaccesskey | 网络访问密钥,后续 Network Introduction 握手用 |
注意 ssid->disabled = 1——新落库的网络默认不自动连接。这是因为 DPP R2 有 Connection Status Result 机制(conn_status=1):Configurator 要求 Enrollee 先回报 "我能不能找到这个网络、连不连得上",回报完才由 Framework 决定是否启用。wpas_dpp_post_process_config(dpp_supplicant.c:1658)里 dpp_config_processing < 2 时直接不连接,== 2 时才 wpas_dpp_try_to_connect。
落库完成后,wpas_dpp_process_config(dpp_supplicant.c:1628)发出关键事件:
1 | // wpa_supplicant/dpp_supplicant.c:1628 |
DPP_EVENT_NETWORK_ID "%d" 就是 DPP-NETWORK-ID 事件(wpa_ctrl.h:219 定义 #define DPP_EVENT_NETWORK_ID "DPP-NETWORK-ID "),它携带刚创建的网络 ID。这是 supplicant 侧 "凭据已落库" 的信号,也是反向回报链的起点。
7 反向回报:DPP-NETWORK-ID 如何回到 App?
物业发完钥匙,前台要给住户打个电话确认 "钥匙收到了吗"。这个确认电话从 C 世界的安保部门一路打到 Java 世界的前台,再打给 App。
7.1 从 wpa_msg 事件到 AIDL 回调
DPP-NETWORK-ID 事件通过 supplicant 的事件机制(wpa_msg)发出,AIDL 层拦截后转成 AIDL 回调。关键链条:wpas_dpp_process_config → wpas_notify_dpp_config_received → wpas_aidl_notify_dpp_config_received → AidlManager::notifyDppConfigReceived:
1 | // wpa_supplicant/aidl/vendor/aidl.cpp:706 |
AidlManager::notifyDppConfigReceived(aidl_manager.cpp:1877)把 struct wpa_ssid 翻译成 AIDL 的 DppConfigurationData,然后回调给注册在 ISupplicantStaIfaceCallback 上的 Java 端:
1 | // wpa_supplicant/aidl/vendor/aidl_manager.cpp:1877 |
这一步完成了 C → Java 的翻译:struct wpa_ssid(C 结构体)→ DppConfigurationData(AIDL 可序列化对象)→ ISupplicantStaIfaceCallback::onDppConfigReceived(Binder 回调,跨进程送到 Framework)。
7.2 Framework 侧接收:SupplicantStaIfaceCallbackAidlImpl
Framework 侧的 Binder 回调实现在 SupplicantStaIfaceCallbackAidlImpl.onDppConfigReceived(SupplicantStaIfaceCallbackAidlImpl.java:531)。它把 AIDL 的配置数据组装回 WifiConfiguration,再转发给 DppManager 注册的回调:
1 | // service/java/com/android/server/wifi/SupplicantStaIfaceCallbackAidlImpl.java:531 |
注意这里把 supplicant 的 AKM 枚举映射回 Framework 的安全类型:DppAkm.SAE → SECURITY_TYPE_SAE、DppAkm.PSK/PSK_SAE → SECURITY_TYPE_PSK、DppAkm.DPP → SECURITY_TYPE_DPP。这个映射在反向链条的两端(supplicant 的 notifyDppConfigReceived 和 Framework 的 processDppConfigReceivedEvent)各做了一次,保证两边字段语义一致。
7.3 DppManager 收口:onSuccessConfigReceived
mStaIfaceHal.getDppCallback() 返回的是 DppManager 在构造时注册的 mDppEventCallback(DppManager.java:84),它把事件 post 到 WiFi 线程,最终落到 DppManager.onSuccessConfigReceived:
1 | // service/java/com/android/server/wifi/DppManager.java:671 |
这是 "Framework 落库":mWifiConfigManager.addOrUpdateNetwork 把 WifiConfiguration 写进 Android 的 WiFi 配置数据库。至此,从二维码扫描到 WiFi 配置落库的完整链路走完——supplicant 的 wpa_ssid 和 Framework 的 WifiConfiguration 都有了这条网络。
7.4 成功回报:Configurator 的 CONFIGURATION_SENT
Enrollee 侧走的是 "收到配置落库"(onSuccessConfigReceived)。Configurator 侧走的是另一条成功路径——DPP-CONF-SENT 事件,表示 "我已经把配置发出去了":
1 | DPP-CONF-SENT 事件 (wpa_ctrl.h:202) |
DppManager.onSuccess 里做了 HAL 状态码到 App 状态码的转换:
1 | // service/java/com/android/server/wifi/DppManager.java:747 |
这里有个隐藏的 R1/R2 能力探测:updateDppR1CapableEnrolleeResponderDevices() 和 updateDppR2CapableEnrolleeResponderDevices()(在 onProgress 的 CONFIGURATION_SENT_WAITING_RESPONSE 分支)——Framework 通过对方是否触发 conn_status / 等待响应来推断 Enrollee 支持 DPP R1 还是 R2,用于统计和后续行为选择。如果对方收到配置就立刻 CONFIGURATION_SENT(不等连接状态回报),说明它是 R1 设备。
7.5 App 收到回调
链条最后一步:mDppRequestInfo.callback 是 IDppCallback 的 Binder 代理,它跨进程回调到 App 侧注册的 EasyConnectCallbackProxy(WifiManager.java:9599):
1 | // framework/java/android/net/wifi/WifiManager.java:9599 |
EasyConnectCallbackProxy 通过 Binder.clearCallingIdentity() 清除 Binder 调用身份后,把回调投递到 App 指定的 Executor 线程,最终调用 App 实现的 EasyConnectStatusCallback。这里完成了从 C 世界到 App 的完整反向链条。
8 异常与失败:配网失败时发生了什么?
住户门禁凭证扫出来是坏码、暗号对不上、钥匙发出去住户说没收到——物业有一套失败话术,每种情况对应一个失败码。
DPP 的失败码分两层:supplicant/HAL 层(DppFailureCode,SupplicantStaIfaceHal.java:92)和 App 层(EasyConnectStatusCallback 的 EASY_CONNECT_EVENT_FAILURE_*)。DppManager.onFailure(DppManager.java:977)负责把前者翻译成后者。
HAL 失败码 (DppFailureCode) | 值 | App 失败码 (EasyConnectStatusCallback) | 值 | 含义 |
|---|---|---|---|---|
INVALID_URI | 0 | EASY_CONNECT_EVENT_FAILURE_INVALID_URI | -1 | 二维码 / URI 解析失败 |
AUTHENTICATION | 1 | EASY_CONNECT_EVENT_FAILURE_AUTHENTICATION | -2 | 认证握手失败 |
NOT_COMPATIBLE | 2 | EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE | -3 | 双方能力不兼容 |
CONFIGURATION | 3 | EASY_CONNECT_EVENT_FAILURE_CONFIGURATION | -4 | 配置交换失败 |
BUSY | 4 | EASY_CONNECT_EVENT_FAILURE_BUSY | -5 | 已有 DPP 会话在进行 |
TIMEOUT | 5 | EASY_CONNECT_EVENT_FAILURE_TIMEOUT | -6 | 超时(Initiator 40s / Responder 300s) |
FAILURE | 6 | EASY_CONNECT_EVENT_FAILURE_GENERIC | -7 | 通用失败 |
NOT_SUPPORTED | 7 | EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED | -8 | 特性不支持 |
CONFIGURATION_REJECTED | 8 | EASY_CONNECT_EVENT_FAILURE_ENROLLEE_REJECTED_CONFIGURATION | -12 | Enrollee 拒绝了配置 |
CANNOT_FIND_NETWORK | 9 | EASY_CONNECT_EVENT_FAILURE_CANNOT_FIND_NETWORK | -10 | Enrollee 找不到网络 |
ENROLLEE_AUTHENTICATION | 10 | EASY_CONNECT_EVENT_FAILURE_ENROLLEE_AUTHENTICATION | -11 | Enrollee 认证失败 |
URI_GENERATION | 11 | EASY_CONNECT_EVENT_FAILURE_URI_GENERATION | -13 | Responder 生成 URI 失败 |
另有两个 App 侧失败码 EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK(-9)与 EASY_CONNECT_EVENT_FAILURE_ENROLLEE_FAILED_TO_SCAN_NETWORK_CHANNEL(-14)没有对应的 HAL DppFailureCode,由其它路径上报,故未列入本表。
超时是配网最常见的失败路径。Initiator 超时 40 秒(DppManager.java:75),超时后 timeoutDppRequest()(DppManager.java:167)先 mWifiNative.stopDppInitiator 清理 supplicant 侧的认证,再回 onFailure(TIMEOUT)。supplicant 侧也有自己的重试逻辑:wpas_dpp_auth_init_next 里每个信道发 Action 帧后等 2 秒(wpa_s->dpp_resp_wait_time ? : 2000),所有信道试完一轮(默认 5 轮 max_tries)没响应就发 DPP-AUTH-INIT-FAILED。
BUSY 是第二个常见失败:DppManager 单会话锁,前一个会话没结束(没超时、没成功、没失败),新请求直接回 FAILURE_BUSY。这要求 App 在发起前先确认没有进行中的会话,或者在收到 BUSY 后提示用户 "上一个配网还没完成"。
9 总结:一条无密码的配网链
把整条链收拢成一张图:
1 | [App] WifiManager.startEasyConnectAsConfiguratorInitiator(uri, networkId, role, callback) |
设计亮点回顾:
- 扫码 ≠ 输密码:二维码里只有公钥、信道、MAC——没有密码。信任建立在 ECDH 共享密钥 + 三帧认证握手 + AES-SIV 加密隧道的组合上,Wi-Fi 密码只在
ke加密的 GAS 响应里出现一次。 - 两端落库、单向收口:supplicant 把凭据写进
wpa_ssid(wpas_dpp_add_network),Framework 再翻译成WifiConfiguration写进WifiConfigManager。DppManager是所有会话状态的单一收口点——单会话锁、超时、Binder 死亡清理、失败码转换,全在它身上。 - 布尔位状态机:认证过程用
struct dpp_authentication里的waiting_auth_resp、waiting_auth_conf、waiting_conf_result、waiting_conn_status_result等标志位组合表达 "正在等哪几件事",比线性枚举状态机更贴合多轮帧交换的异步本质。 - AIDL 层刻意做薄:
sta_iface.cpp只是把结构化参数拼成命令字符串喂给 supplicant 既有接口,让 DPP 新特性可以薄接入,不必改 supplicant 核心 API。
别误以为 DPP 就这些,DPP-over-TCP(规范 §2.3,dpp_tcp_init)用于远程配网;Enterprise provisioning(§4.5,EAP-TLS 证书下发);Network Access(§6.6.6)是配网成功后 Connector 驱动的二次握手,不在此列;驱动侧——DPP 帧走 802.11 通用 Action/GAS,offchannel_send_action 之后就是通用帧收发,驱动没有 DPP 专属代码。
这一章,我们用二维码把一场 "无密码配网" 从 App 追到了 supplicant 的网络库——扫一张 "门禁凭证",换来 "门禁卡" 和 "房门钥匙"。但这里藏着前提:住户得先贴得出那张 "门禁凭证"。那些没有屏幕、贴不了二维码贴纸的设备,连凭证都没有,钥匙怎么发?DPP 3.0(规范 §5.6)给出了答案:PKEX(Public Key Exchange)——双方各自输入同一个 "分享码"(passphrase),在不扫码的情况下完成公钥交换。
源码仓库:
- AOSP wpa_supplicant 8: https://w1.fi/wpa_supplicant/
- AOSP packages/modules/Wifi: https://android.googlesource.com/platform/packages/modules/Wifi
相关规范:
- Wi-Fi Alliance, "Wi-Fi Easy Connect Specification v3.0"
- §4.5 Configuration Object;§5 Bootstrapping of Trust;§5.2.1 Bootstrapping Information Format;§5.6 PKEX
- §6.3 DPP Authentication protocol;§6.4 DPP Configuration protocol;§6.4.5 Connection Status Result;§6.6.6 Network Access Protocols