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" 開始
        // 所以剩下的命令是"<network id> <name> <value>"
        // 在我們的這個例子中就是 "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: "<network id> <variable name> <value>" */
    // 取出<variable name>,注意這裏的"name"還不是真正的name,
    // 應該是除去<network id> 剩下的部分,具體可以研究下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));

    // 根據<network id>取出對應的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 <[email protected]> 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那種簡潔的登錄方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章