Wi-Fi EAP网络验证过程与Android平台拓展实例(二)

文章参考的是Android 7.1的源码
本文主要研究EAP类型网络的身份验证过程,从而简化Android平台EAP类型网络的身份验证过程。

上篇文章Wi-Fi EAP网络验证过程与Android平台拓展实例(一)已经分析出IOS和Android平台在EAP类型网络认证上的差异,即提供给认证服务器的EAP方法不同。那么这篇文章就通过分析Android源码来大概了解下整个认证过程。


1. wpa_supplicant.conf

  在看源码之前,我们先看看Android中AP配置信息的保存。在Android7.1及其以下的版本中,AP的基本配置信息,如身份,密码,加密类型等,都保存在data/misc/wifi/wpa_supplicant.conf这个文件中,那么我们来看看连接TPLINK-TTLS(加密方式EAP-TTLS)的情况下,该AP的保存信息:

unknown:/ # cat data/misc/wifi/wpa_supplicant.conf 
...
network={
    ssid="TPLINK-TTLS"
    key_mgmt=WPA-EAP IEEE8021X
    eap=PEAP
    identity="ulangch"
    password="12345678"
    phase2=""
    priority=3
    proactive_key_caching=1
    disabled=1
    id_str="%7B%22creatorUid%22%3A%221000%22%2C%22configKey%22%3A%22%5C%22TPLINK-TTLS%5C%22-WPA_EAP%22%7D"
}

  如上所示,在我们选择了PEAP方法时,wpa_supplicant.conf中保存了"eap=PEAP"的信息,如果我们把该文件pull出来修改成"eap=TTLS"push回去再重启Wi-Fi连接该AP,则也可以正常连上。

2. EAP网络-Framework

  我们先看看EAP网络信息在framework中的保存过程是什么样的,如果大家对framework中AP的保存过程非常熟悉,建议跳过该节内容,直接查看wpa_supplicant中的部分。
  WifiStateMachine在收到WifiService传递过来的SAVE_NETWORK消息后,调用WifiConfigManager进行了保存操作:

/** WifiStateMachine.java*/
case WifiManager.SAVE_NETWORK:
case WifiStateMachine.CMD_AUTO_SAVE_NETWORK:
    ...
    // 调用WifiConfigManager.saveNetwork来保存WifiConfiguration
    result = mWifiConfigManager.saveNetwork(config, WifiConfiguration.UNKNOWN_UID);
    ...

  WifiConfigManager开始进行保存和更新,经过一系列的检查,最终会使用WifiConfigStore来保存

/** WifiConfigManager.java*/
NetworkUpdateResult saveNetwork(WifiConfiguration config, int uid) {
    ...
    NetworkUpdateResult result = addOrUpdateNetworkNative(config, uid);
}
/** WifiConfigManager.java*/
private NetworkUpdateResult addOrUpdateNetworkNative(WifiConfiguration config, int uid) {
    ...
    if (!mWifiConfigStore.addOrUpdateNetwork(config, currentConfig)) {
        return new NetworkUpdateResult(INVALID_NETWORK_ID);
    }
}

  WifiConfigStore在经过检查和保存AP的基本信息后,对于EAP相关的配置信息,如身份,密码,CA证书等会通过WifiEnterpriseConfig中提供的接口进行保存:

/**WifiConfigStore.java*/
public boolean addOrUpdateNetwork(WifiConfiguration config, WifiConfiguration existingConfig) {
    ...
    if (!saveNetwork(config, netId)) {
        if (newNetwork) {
            mWifiNative.removeNetwork(netId);
            loge("Failed to set a network variable, removed network: " + netId);
        }
        return false;
    }
    if (config.enterpriseConfig != null
            && config.enterpriseConfig.getEapMethod() != WifiEnterpriseConfig.Eap.NONE) {
        return updateNetworkKeys(config, existingConfig);
    }
}
/** WifiConfigStore.java*/
private boolean updateNetworkKeys(WifiConfiguration config, WifiConfiguration existingConfig) {
    ...
    // WifiConfigStore#SupplicantSaver继承自WifiEnterpriseConfig#SupplicantSaver,
    // 因此最终还是会回到WifiConfigStore中进行保存操作
    if (!enterpriseConfig.saveToSupplicant(
            new SupplicantSaver(config.networkId, config.SSID))) {
        removeKeys(enterpriseConfig);
        return false;
    }
    return true;
}

  WifiEnterpriseConfig将实例中的EAP的属性信息保存:

/** WifiEnterpriseConfig.java*/
public boolean saveToSupplicant(SupplicantSaver saver) {
    ...
    // 这里我们关心EAP方法的保存,其他属性的保存与eap method保存的方式是一样的
    // Eap.strings[mEapMethod])返回当前mEapMethod对应的EAP方法字符串名称
    // 前面说过,SupplicantSaver是WifiConfigStore.SupplicantSaver, 因此继续回到WifiConfigStore
    if (!saver.saveValue(EAP_KEY, Eap.strings[mEapMethod])) {
        return false;
    }
}

  从上面的"mEapMethod"可以看出,framework中的EAP方法保存在这个变量中,对应一种类型的EAP方法。
  下面是WifiConfigStore#SupplicantSaver调用WifiNative中提供的方法进行保存:

/** WifiConfigStore.java*/
private class SupplicantSaver implements WifiEnterpriseConfig.SupplicantSaver {
    @Override
    public boolean saveValue(String key, String value) {
        if (!mWifiNative.setNetworkVariable(mNetId, key, value)) {
            loge(mSetterSSID + ": failed to set " + key + ": " + value);
            return false;
        }
        return true;
    }
}

  WifiNative中通过发送"SET_NETWORK"命令到 wpa_supplicant

/** WifiNative.java*/
public boolean setNetworkVariable(int netId, String name, String value) {
    if (TextUtils.isEmpty(name) || TextUtils.isEmpty(value)) return false;
    if (name.equals(WifiConfiguration.pskVarName)
                || name.equals(WifiEnterpriseConfig.PASSWORD_KEY)) {
        return doBooleanCommandWithoutLogging("SET_NETWORK " + netId + " " + name + " " + value);
    } else {
        // EAP 方法最终使用 SET_NETWORK 0 eap PEAP 这种命令来实现
        return doBooleanCommand("SET_NETWORK " + netId + " " + name + " " + value);
    }
}

  所以从上面的流程可以看出,在framework中,EAP属性信息缓存在WifiEnterpriseConfig中,而EAP方法保存在"mEapMethod"这个整型变量中。当要进行连接或者保存时,都会通过 SET_NETWORK指令通过将其配置信息设置到wpa_supplicant。同样的,我们可以直接在adb shell中使用wpa_cli工具来修改EAP方法或者AP的其他信息。

3. EAP 网络-wpa_supplicant

  至于system_server与wpa_supplicant,他们是通过建立socket来通信的,具体流程这里就不赘述了,我们直接跳转到wpa_supplicant中处理 "SET_NETWORK"的相关逻辑,并且,我们只关心EAP部分EAP方法的配置内容,假设WifiNative的命令是 "SET_NETWORK 0 eap PEAP"

  首先从wpa_supplicant socket server处理消息入手:

/** external/wpa_supplicant_8/wpa_supplicant/ctrl_iface.c*/
char * wpa_supplicant_ctrl_iface_process(struct wpa_supplicant *wpa_s,
                     char *buf, size_t *resp_len)
{
    ....
    } else if (os_strncmp(buf, "SET_NETWORK ", 12) == 0) {
        // buf + 12表示从 "network id" 开始
        // 所以剩下的命令是"  "
        // 在我们的这个例子中就是 "0 eap PEAP"
        if (wpa_supplicant_ctrl_iface_set_network(wpa_s, buf + 12))
            reply_len = -1;
    }
}

  下面就是设定wpa_ssid各个key-value的地方(wpa_ssid是wpa_supplicant中保存网络配置的一个结构体,类似于framework中的WifiConfiguration):

/** external/wpa_supplicant_8/wpa_supplicant/ctrl_iface.c*/
static int wpa_supplicant_ctrl_iface_set_network(
    struct wpa_supplicant *wpa_s, char *cmd)
{
    int id, ret, prev_bssid_set, prev_disabled;
    struct wpa_ssid *ssid;
    char *name, *value;
    u8 prev_bssid[ETH_ALEN];

    /* cmd: "  " */
    // 取出,注意这里的"name"还不是真正的name,
    // 应该是除去 剩下的部分,具体可以研究下os_strchr方法
    name = os_strchr(cmd, ' ');
    if (name == NULL)
        return -1;
    // 这里的作用是为了正确的取出id
    *name++ = '\0';

    value = os_strchr(name, ' ');
    if (value == NULL)
        return -1;
    // 这里的作用是为了正确取出name
    *value++ = '\0';

    id = atoi(cmd);
    wpa_printf(MSG_DEBUG, "CTRL_IFACE: SET_NETWORK id=%d name='%s'",
           id, name);
    wpa_hexdump_ascii_key(MSG_DEBUG, "CTRL_IFACE: value",
                  (u8 *) value, os_strlen(value));

    // 根据取出对应的wpa_ssid
    ssid = wpa_config_get_network(wpa_s->conf, id);
    if (ssid == NULL) {
        wpa_printf(MSG_DEBUG, "CTRL_IFACE: Could not find network "
               "id=%d", id);
        return -1;
    }

    prev_bssid_set = ssid->bssid_set;
    prev_disabled = ssid->disabled;
    os_memcpy(prev_bssid, ssid->bssid, ETH_ALEN);
    // 更新已保存的wpa_ssid,很类似WifiConfigManager中的addOrUpdateNetwork
    ret = wpa_supplicant_ctrl_iface_update_network(wpa_s, ssid, name,
                               value);
}
/** external/wpa_supplicant_8/wpa_supplicant/ctrl_iface.c*/
static int wpa_supplicant_ctrl_iface_update_network(
    struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid,
    char *name, char *value)
{
    int ret;

    // 设置name & value
    ret = wpa_config_set(ssid, name, value, 0);
    ...
}

  所有的variable都会通过下面的"wpa_config_set"方法进行不同的处理,对于不同的variable,config.c中采用了巧妙的方式:"#define _FUNC(f) #f, wpa_config_parse_ ## f, wpa_config_write_ ## f, NULL, NULL, NULL, NULL"。如处理”eap”这个variable,就采用wpa_config_parse_eap来处理:

/** external\wpa_supplicant_8\wpa_supplicant/config.c*/
int wpa_config_set(struct wpa_ssid *ssid, const char *var, const char *value,
           int line)
{
    ...
    for (i = 0; i < NUM_SSID_FIELDS; i++) {
        const struct parse_data *field = &ssid_fields[i];
        if (os_strcmp(var, field->name) != 0)
            continue;

        // 重点查看paser这个方法,在我们"SET_NETWORK 0 eap PEAP"这个例子中,
        // parser对应方法应该是"wpa_config_parse_eap",对于不同的variable,
        // config.c中有不同的函数去处理
        ret = field->parser(field, ssid, line, value);
    }
}

  接下来就是我们重点要看的地方,如何将PEAP这个eap方法配置到wpa_ssid中,从中,我们也可以获得一些启发:

/** external\wpa_supplicant_8\wpa_supplicant/config.c*/
static int wpa_config_parse_eap(const struct parse_data *data,
                struct wpa_ssid *ssid, int line,
                const char *value)
{
    int last, errors = 0;
    char *start, *end, *buf;
    /** 从methods这个定义来看,就初见蹊跷,是否是支持多个method呢?*/
    struct eap_method_type *methods = NULL, *tmp;
    size_t num_methods = 0;

    // 将value拷贝到buf中
    buf = os_strdup(value);
    if (buf == NULL)
        return -1;
    start = buf;

    // 循环查找methods
    while (*start != '\0') {
        // 从这个地方,就已经可以确定,wpa_supplicant是支持多个eap method的,
        // 多个eap 方法只需要使用' '或者'\t'分开就可以,
        // 我们只需要类似使用"SET_NETWORK 0 eap PEAP TTLS"这种方式就可以
        // 配置多个eap method,wpa_supplicant会对methods进行解析并且存储
        while (*start == ' ' || *start == '\t')
            start++;
        if (*start == '\0')
            break;
        end = start;
        while (*end != ' ' && *end != '\t' && *end != '\0')
            end++;
        last = *end == '\0';
        *end = '\0';
        tmp = methods;
        // 每找到一个method,就增大methods的长度,
        methods = os_realloc_array(methods, num_methods + 1,
                       sizeof(*methods));
        ...
        // 校验设定的eap method是否是有效可支持的,wpa_supplicant中支持的eap方法
        // 都可以在"external\wpa_supplicant_8\src\eap_common\eap_defs.h"中查看
        methods[num_methods].method = eap_peer_get_type(
            start, &methods[num_methods].vendor);
        if (methods[num_methods].vendor == EAP_VENDOR_IETF &&
            methods[num_methods].method == EAP_TYPE_NONE) {
            wpa_printf(MSG_ERROR, "Line %d: unknown EAP method "
                   "'%s'", line, start);
            ...
            errors++;
        } 
    }
    // 省略的部分是对wpa_ssid中存在的eap_methods和正在配置的eap_methods进行匹配,
    // 如果能够完全匹配,则不需要重新配置,具体可以看源码,这里不做详解
    ...
    // 直接将methods设置到network_id对应的wpa_ssid中
    ssid->eap.eap_methods = methods;
    return errors ? -1 : 0;
}

  根据上面的分析,现在已经很清楚的知道,wpa_supplicant 中是支持配置多种eap method的,但是 framework 中却只配置一种方法,WifiEnterpriseConfig.java中只定义了一个"mEapMethod",为了进一步确认 wpa_supplicant 这个支持是否可行,我们接下来进行试验求证。

4. 实验求证

  方案一:直接修改data/misc/wifi/wpa_supplicant.conf中保存的network

// 1. 将wpa_supplicant.conf pull出来
xyzc@xyzc-ul:~/ulangch/wifi$ adb pull data/misc/wifi/wpa_supplicant.conf
8 KB/s (689 bytes in 0.083s)

/// 2. 修改wpa_supplicant.conf中对应的eap network:
//     将"eap=PEAP"修改成"eap=PEAP TTLS PWD":
network={
        ssid="TPLINK-TTLS"
        key_mgmt=WPA-EAP IEEE8021X
        eap=PEAP TTLS PWD
        identity="ulangch"
        password="12345678"
        phase2=""
        priority=2
        proactive_key_caching=1
        disabled=1
        id_str="%7B%22creatorUid%22%3A%221000%22%2C%22configKey%22%3A%22%5C%22TPLINK-TTLS%5C%22-WPA_EAP%22%7D"
}

// 3. 修改完成后push到原来的位置:
xyzc@xyzc-ul:~/ulangch/wifi$ adb push wpa_supplicant.conf data/misc/wifi
12 KB/s (689 bytes in 0.054s)

// 4. 开关WiFi重新进行连接

// 5. 结论:可以正常连接上EAP方法为TTLS的网络

  
  方案二:使用wpa_cli工具修改 wpa_ssid 实现多个EAP方法验证

/// 1. 进入adb shell
xyzc@xyzc-ul:~/ulangch/wifi$ adb shell
// 2. 使用wpa_cli 工具
unknown:/ # wpa_cli
wpa_cli v2.6-devel-7.1.1
Copyright (c) 2004-2016, Jouni Malinen  and contributors
This software may be distributed under the terms of the BSD license.
See README for more details.
Using interface 'wlan0'
Interactive mode
>
// 3. list 当前的网络,方便我们找到"TPLINK-TTLS"的network_id
> list_networks
network id / ssid / bssid / flags
0   TPLINK-TTLS any [TEMP-DISABLED]
> 
// 4. 将EAP方法设定为"PEAP TTLS PWD"
> set_network 0 eap PEAP TTLS PWD
OK
> 
// 5. 重新主动连接该网络
> select_network 0
OK
// 6. 结论:可以正常连接上EAP方法为TTLS的网络

5. Legacy Nak

  上面的实验已经完全证明Android和IOS一样也支持设定多种EAP方法,根据认证服务器回应的认证挑战,回复多个EAP方法提供给认证服务器进行选择。在Wi-Fi EAP网络验证过程与Android平台拓展实例(一)中,不知有没有注意到"Legacy Nak",这是在手机(STA)发给AP(或者是认证服务器)的报文中:

/** Android*/
Extensible Authentication Protocol
    Code: Response (2)
    Id: 1
    Length: 6 /** 注意这里的长度是6*/
    Type: Legacy Nak (Response Only) (3)
    Desired Auth Type: Protected EAP (EAP-PEAP) (25)

  这份报文内容也是由wpa_supplicant进行组织的,其中说明wpa_supplican将会把所有设定的多个eap method封装成802.11消息发送给AP,下面我们简单来看一下:

/** external\wpa_supplicant_8\src\eap_peer\eap.c*/
static struct wpabuf * eap_sm_buildNak(struct eap_sm *sm, int id)
{
    struct wpabuf *resp;
    u8 *start;
    int found = 0, expanded_found = 0;
    // count是对应network中配置的eap method的个数
    size_t count;
    const struct eap_method *methods, *m;

    wpa_printf(MSG_DEBUG, "EAP: Building EAP-Nak (requested type %u "
           "vendor=%u method=%u not allowed)", sm->reqMethod,
           sm->reqVendor, sm->reqVendorMethod);
    // 循环获取eap_methods
    methods = eap_peer_get_methods(&count);
    if (methods == NULL)
        return NULL;
    if (sm->reqMethod == EAP_TYPE_EXPANDED)
        return eap_sm_build_expanded_nak(sm, id, methods, count);
    /* RFC 3748 - 5.3.1: Legacy Nak */
    // 这里可以参考上份报文,对于"SET_NETWORK 0 eap PEAP",这里count=1,
    // eap_msg_alloc alloc出6个字节的response
    resp = eap_msg_alloc(EAP_VENDOR_IETF, EAP_TYPE_NAK,
                 sizeof(struct eap_hdr) + 1 + count + 1,
                 EAP_CODE_RESPONSE, id);
    if (resp == NULL)
        return NULL;

    start = wpabuf_put(resp, 0);
    // 校验config中的eap methods,并将其填充到resp中
    for (m = methods; m; m = m->next) {
        if (m->vendor == EAP_VENDOR_IETF && m->method == sm->reqMethod)
            continue; /* do not allow the current method again */
        if (eap_allowed_method(sm, m->vendor, m->method)) {
            if (m->vendor != EAP_VENDOR_IETF) {
                if (expanded_found)
                    continue;
                expanded_found = 1;
                wpabuf_put_u8(resp, EAP_TYPE_EXPANDED);
            } else
                // 将method这一个字节填入道resp中
                wpabuf_put_u8(resp, m->method);
            found++;
        }
    }
    ...
    return resp;

5. 总结

  经过Wi-Fi EAP网络验证过程与Android平台拓展实例(一)和本文的分析,我们已经大致了解了EAP网络的认证流程以及Android系统是如何处理EAP方法的,我们可以知道,造成Android和IOS在认证上的不同点是因为Android framework没有支持多种eap method,但是wpa_supplicant中已经支持。因此,不用修改Android中的eap验证协议,仅仅需要稍加修改framework和Settings的部分逻辑,就可以实现类似IOS那种简洁的登录方式。

你可能感兴趣的:(WiFi,wpa_supplicant)