hostap/wpa_supplicant/interworking.c
Jouni Malinen 0b9d3b22c8 Interworking: Relax 3GPP info PLMN matching for MNC
3GPP TS 24.232 Annex A.3 allows network operator to advertise only two
digits of MNC even if MNC has three digits. Allow such matches in
network selection. In addition, allow three digit matches of MNC even if
MNC length was assumed to be two to avoid missing networks if MNC length
cannot be determined reliably. Remove the '-' separator from simulated
SIM/USIM cases to allow the new matching rules to work.

Fix the PLMN List information element parsing loop to use the length of
the PLMN List instead of the length of the full 3GPP Cellular Info to
avoid unexpected matches should a new element ever be added by 3GPP.

Finally, add more debug prints from PLMN matching to make the logs
easier to understand.

Signed-hostap: Jouni Malinen <jouni@qca.qualcomm.com>
2013-07-08 16:53:05 +03:00

2144 lines
52 KiB
C

/*
* Interworking (IEEE 802.11u)
* Copyright (c) 2011-2012, Qualcomm Atheros, Inc.
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*/
#include "includes.h"
#include "common.h"
#include "common/ieee802_11_defs.h"
#include "common/gas.h"
#include "common/wpa_ctrl.h"
#include "utils/pcsc_funcs.h"
#include "utils/eloop.h"
#include "drivers/driver.h"
#include "eap_common/eap_defs.h"
#include "eap_peer/eap.h"
#include "eap_peer/eap_methods.h"
#include "wpa_supplicant_i.h"
#include "config.h"
#include "config_ssid.h"
#include "bss.h"
#include "scan.h"
#include "notify.h"
#include "gas_query.h"
#include "hs20_supplicant.h"
#include "interworking.h"
#if defined(EAP_SIM) | defined(EAP_SIM_DYNAMIC)
#define INTERWORKING_3GPP
#else
#if defined(EAP_AKA) | defined(EAP_AKA_DYNAMIC)
#define INTERWORKING_3GPP
#else
#if defined(EAP_AKA_PRIME) | defined(EAP_AKA_PRIME_DYNAMIC)
#define INTERWORKING_3GPP
#endif
#endif
#endif
static void interworking_next_anqp_fetch(struct wpa_supplicant *wpa_s);
static struct wpa_cred * interworking_credentials_available_realm(
struct wpa_supplicant *wpa_s, struct wpa_bss *bss);
static struct wpa_cred * interworking_credentials_available_3gpp(
struct wpa_supplicant *wpa_s, struct wpa_bss *bss);
static void interworking_reconnect(struct wpa_supplicant *wpa_s)
{
if (wpa_s->wpa_state >= WPA_AUTHENTICATING) {
wpa_supplicant_cancel_sched_scan(wpa_s);
wpa_supplicant_deauthenticate(wpa_s,
WLAN_REASON_DEAUTH_LEAVING);
}
wpa_s->disconnected = 0;
wpa_s->reassociate = 1;
if (wpa_supplicant_fast_associate(wpa_s) >= 0)
return;
wpa_supplicant_req_scan(wpa_s, 0, 0);
}
static struct wpabuf * anqp_build_req(u16 info_ids[], size_t num_ids,
struct wpabuf *extra)
{
struct wpabuf *buf;
size_t i;
u8 *len_pos;
buf = gas_anqp_build_initial_req(0, 4 + num_ids * 2 +
(extra ? wpabuf_len(extra) : 0));
if (buf == NULL)
return NULL;
len_pos = gas_anqp_add_element(buf, ANQP_QUERY_LIST);
for (i = 0; i < num_ids; i++)
wpabuf_put_le16(buf, info_ids[i]);
gas_anqp_set_element_len(buf, len_pos);
if (extra)
wpabuf_put_buf(buf, extra);
gas_anqp_set_len(buf);
return buf;
}
static void interworking_anqp_resp_cb(void *ctx, const u8 *dst,
u8 dialog_token,
enum gas_query_result result,
const struct wpabuf *adv_proto,
const struct wpabuf *resp,
u16 status_code)
{
struct wpa_supplicant *wpa_s = ctx;
anqp_resp_cb(wpa_s, dst, dialog_token, result, adv_proto, resp,
status_code);
interworking_next_anqp_fetch(wpa_s);
}
static int cred_with_roaming_consortium(struct wpa_supplicant *wpa_s)
{
struct wpa_cred *cred;
for (cred = wpa_s->conf->cred; cred; cred = cred->next) {
if (cred->roaming_consortium_len)
return 1;
}
return 0;
}
static int cred_with_3gpp(struct wpa_supplicant *wpa_s)
{
struct wpa_cred *cred;
for (cred = wpa_s->conf->cred; cred; cred = cred->next) {
if (cred->pcsc || cred->imsi)
return 1;
}
return 0;
}
static int cred_with_nai_realm(struct wpa_supplicant *wpa_s)
{
struct wpa_cred *cred;
for (cred = wpa_s->conf->cred; cred; cred = cred->next) {
if (cred->pcsc || cred->imsi)
continue;
if (!cred->eap_method)
return 1;
if (cred->realm && cred->roaming_consortium_len == 0)
return 1;
}
return 0;
}
static int cred_with_domain(struct wpa_supplicant *wpa_s)
{
struct wpa_cred *cred;
for (cred = wpa_s->conf->cred; cred; cred = cred->next) {
if (cred->domain || cred->pcsc || cred->imsi)
return 1;
}
return 0;
}
static int additional_roaming_consortiums(struct wpa_bss *bss)
{
const u8 *ie;
ie = wpa_bss_get_ie(bss, WLAN_EID_ROAMING_CONSORTIUM);
if (ie == NULL || ie[1] == 0)
return 0;
return ie[2]; /* Number of ANQP OIs */
}
static void interworking_continue_anqp(void *eloop_ctx, void *sock_ctx)
{
struct wpa_supplicant *wpa_s = eloop_ctx;
interworking_next_anqp_fetch(wpa_s);
}
static int interworking_anqp_send_req(struct wpa_supplicant *wpa_s,
struct wpa_bss *bss)
{
struct wpabuf *buf;
int ret = 0;
int res;
u16 info_ids[8];
size_t num_info_ids = 0;
struct wpabuf *extra = NULL;
int all = wpa_s->fetch_all_anqp;
wpa_printf(MSG_DEBUG, "Interworking: ANQP Query Request to " MACSTR,
MAC2STR(bss->bssid));
wpa_s->interworking_gas_bss = bss;
info_ids[num_info_ids++] = ANQP_CAPABILITY_LIST;
if (all) {
info_ids[num_info_ids++] = ANQP_VENUE_NAME;
info_ids[num_info_ids++] = ANQP_NETWORK_AUTH_TYPE;
}
if (all || (cred_with_roaming_consortium(wpa_s) &&
additional_roaming_consortiums(bss)))
info_ids[num_info_ids++] = ANQP_ROAMING_CONSORTIUM;
if (all)
info_ids[num_info_ids++] = ANQP_IP_ADDR_TYPE_AVAILABILITY;
if (all || cred_with_nai_realm(wpa_s))
info_ids[num_info_ids++] = ANQP_NAI_REALM;
if (all || cred_with_3gpp(wpa_s))
info_ids[num_info_ids++] = ANQP_3GPP_CELLULAR_NETWORK;
if (all || cred_with_domain(wpa_s))
info_ids[num_info_ids++] = ANQP_DOMAIN_NAME;
wpa_hexdump(MSG_DEBUG, "Interworking: ANQP Query info",
(u8 *) info_ids, num_info_ids * 2);
#ifdef CONFIG_HS20
if (wpa_bss_get_vendor_ie(bss, HS20_IE_VENDOR_TYPE)) {
u8 *len_pos;
extra = wpabuf_alloc(100);
if (!extra)
return -1;
len_pos = gas_anqp_add_element(extra, ANQP_VENDOR_SPECIFIC);
wpabuf_put_be24(extra, OUI_WFA);
wpabuf_put_u8(extra, HS20_ANQP_OUI_TYPE);
wpabuf_put_u8(extra, HS20_STYPE_QUERY_LIST);
wpabuf_put_u8(extra, 0); /* Reserved */
wpabuf_put_u8(extra, HS20_STYPE_CAPABILITY_LIST);
if (all) {
wpabuf_put_u8(extra,
HS20_STYPE_OPERATOR_FRIENDLY_NAME);
wpabuf_put_u8(extra, HS20_STYPE_WAN_METRICS);
wpabuf_put_u8(extra, HS20_STYPE_CONNECTION_CAPABILITY);
wpabuf_put_u8(extra, HS20_STYPE_OPERATING_CLASS);
}
gas_anqp_set_element_len(extra, len_pos);
}
#endif /* CONFIG_HS20 */
buf = anqp_build_req(info_ids, num_info_ids, extra);
wpabuf_free(extra);
if (buf == NULL)
return -1;
res = gas_query_req(wpa_s->gas, bss->bssid, bss->freq, buf,
interworking_anqp_resp_cb, wpa_s);
if (res < 0) {
wpa_printf(MSG_DEBUG, "ANQP: Failed to send Query Request");
ret = -1;
eloop_register_timeout(0, 0, interworking_continue_anqp, wpa_s,
NULL);
} else
wpa_printf(MSG_DEBUG, "ANQP: Query started with dialog token "
"%u", res);
wpabuf_free(buf);
return ret;
}
struct nai_realm_eap {
u8 method;
u8 inner_method;
enum nai_realm_eap_auth_inner_non_eap inner_non_eap;
u8 cred_type;
u8 tunneled_cred_type;
};
struct nai_realm {
u8 encoding;
char *realm;
u8 eap_count;
struct nai_realm_eap *eap;
};
static void nai_realm_free(struct nai_realm *realms, u16 count)
{
u16 i;
if (realms == NULL)
return;
for (i = 0; i < count; i++) {
os_free(realms[i].eap);
os_free(realms[i].realm);
}
os_free(realms);
}
static const u8 * nai_realm_parse_eap(struct nai_realm_eap *e, const u8 *pos,
const u8 *end)
{
u8 elen, auth_count, a;
const u8 *e_end;
if (pos + 3 > end) {
wpa_printf(MSG_DEBUG, "No room for EAP Method fixed fields");
return NULL;
}
elen = *pos++;
if (pos + elen > end || elen < 2) {
wpa_printf(MSG_DEBUG, "No room for EAP Method subfield");
return NULL;
}
e_end = pos + elen;
e->method = *pos++;
auth_count = *pos++;
wpa_printf(MSG_DEBUG, "EAP Method: len=%u method=%u auth_count=%u",
elen, e->method, auth_count);
for (a = 0; a < auth_count; a++) {
u8 id, len;
if (pos + 2 > end || pos + 2 + pos[1] > end) {
wpa_printf(MSG_DEBUG, "No room for Authentication "
"Parameter subfield");
return NULL;
}
id = *pos++;
len = *pos++;
switch (id) {
case NAI_REALM_EAP_AUTH_NON_EAP_INNER_AUTH:
if (len < 1)
break;
e->inner_non_eap = *pos;
if (e->method != EAP_TYPE_TTLS)
break;
switch (*pos) {
case NAI_REALM_INNER_NON_EAP_PAP:
wpa_printf(MSG_DEBUG, "EAP-TTLS/PAP");
break;
case NAI_REALM_INNER_NON_EAP_CHAP:
wpa_printf(MSG_DEBUG, "EAP-TTLS/CHAP");
break;
case NAI_REALM_INNER_NON_EAP_MSCHAP:
wpa_printf(MSG_DEBUG, "EAP-TTLS/MSCHAP");
break;
case NAI_REALM_INNER_NON_EAP_MSCHAPV2:
wpa_printf(MSG_DEBUG, "EAP-TTLS/MSCHAPV2");
break;
}
break;
case NAI_REALM_EAP_AUTH_INNER_AUTH_EAP_METHOD:
if (len < 1)
break;
e->inner_method = *pos;
wpa_printf(MSG_DEBUG, "Inner EAP method: %u",
e->inner_method);
break;
case NAI_REALM_EAP_AUTH_CRED_TYPE:
if (len < 1)
break;
e->cred_type = *pos;
wpa_printf(MSG_DEBUG, "Credential Type: %u",
e->cred_type);
break;
case NAI_REALM_EAP_AUTH_TUNNELED_CRED_TYPE:
if (len < 1)
break;
e->tunneled_cred_type = *pos;
wpa_printf(MSG_DEBUG, "Tunneled EAP Method Credential "
"Type: %u", e->tunneled_cred_type);
break;
default:
wpa_printf(MSG_DEBUG, "Unsupported Authentication "
"Parameter: id=%u len=%u", id, len);
wpa_hexdump(MSG_DEBUG, "Authentication Parameter "
"Value", pos, len);
break;
}
pos += len;
}
return e_end;
}
static const u8 * nai_realm_parse_realm(struct nai_realm *r, const u8 *pos,
const u8 *end)
{
u16 len;
const u8 *f_end;
u8 realm_len, e;
if (end - pos < 4) {
wpa_printf(MSG_DEBUG, "No room for NAI Realm Data "
"fixed fields");
return NULL;
}
len = WPA_GET_LE16(pos); /* NAI Realm Data field Length */
pos += 2;
if (pos + len > end || len < 3) {
wpa_printf(MSG_DEBUG, "No room for NAI Realm Data "
"(len=%u; left=%u)",
len, (unsigned int) (end - pos));
return NULL;
}
f_end = pos + len;
r->encoding = *pos++;
realm_len = *pos++;
if (pos + realm_len > f_end) {
wpa_printf(MSG_DEBUG, "No room for NAI Realm "
"(len=%u; left=%u)",
realm_len, (unsigned int) (f_end - pos));
return NULL;
}
wpa_hexdump_ascii(MSG_DEBUG, "NAI Realm", pos, realm_len);
r->realm = dup_binstr(pos, realm_len);
if (r->realm == NULL)
return NULL;
pos += realm_len;
if (pos + 1 > f_end) {
wpa_printf(MSG_DEBUG, "No room for EAP Method Count");
return NULL;
}
r->eap_count = *pos++;
wpa_printf(MSG_DEBUG, "EAP Count: %u", r->eap_count);
if (pos + r->eap_count * 3 > f_end) {
wpa_printf(MSG_DEBUG, "No room for EAP Methods");
return NULL;
}
r->eap = os_calloc(r->eap_count, sizeof(struct nai_realm_eap));
if (r->eap == NULL)
return NULL;
for (e = 0; e < r->eap_count; e++) {
pos = nai_realm_parse_eap(&r->eap[e], pos, f_end);
if (pos == NULL)
return NULL;
}
return f_end;
}
static struct nai_realm * nai_realm_parse(struct wpabuf *anqp, u16 *count)
{
struct nai_realm *realm;
const u8 *pos, *end;
u16 i, num;
if (anqp == NULL || wpabuf_len(anqp) < 2)
return NULL;
pos = wpabuf_head_u8(anqp);
end = pos + wpabuf_len(anqp);
num = WPA_GET_LE16(pos);
wpa_printf(MSG_DEBUG, "NAI Realm Count: %u", num);
pos += 2;
if (num * 5 > end - pos) {
wpa_printf(MSG_DEBUG, "Invalid NAI Realm Count %u - not "
"enough data (%u octets) for that many realms",
num, (unsigned int) (end - pos));
return NULL;
}
realm = os_calloc(num, sizeof(struct nai_realm));
if (realm == NULL)
return NULL;
for (i = 0; i < num; i++) {
pos = nai_realm_parse_realm(&realm[i], pos, end);
if (pos == NULL) {
nai_realm_free(realm, num);
return NULL;
}
}
*count = num;
return realm;
}
static int nai_realm_match(struct nai_realm *realm, const char *home_realm)
{
char *tmp, *pos, *end;
int match = 0;
if (realm->realm == NULL || home_realm == NULL)
return 0;
if (os_strchr(realm->realm, ';') == NULL)
return os_strcasecmp(realm->realm, home_realm) == 0;
tmp = os_strdup(realm->realm);
if (tmp == NULL)
return 0;
pos = tmp;
while (*pos) {
end = os_strchr(pos, ';');
if (end)
*end = '\0';
if (os_strcasecmp(pos, home_realm) == 0) {
match = 1;
break;
}
if (end == NULL)
break;
pos = end + 1;
}
os_free(tmp);
return match;
}
static int nai_realm_cred_username(struct nai_realm_eap *eap)
{
if (eap_get_name(EAP_VENDOR_IETF, eap->method) == NULL)
return 0; /* method not supported */
if (eap->method != EAP_TYPE_TTLS && eap->method != EAP_TYPE_PEAP) {
/* Only tunneled methods with username/password supported */
return 0;
}
if (eap->method == EAP_TYPE_PEAP) {
if (eap->inner_method &&
eap_get_name(EAP_VENDOR_IETF, eap->inner_method) == NULL)
return 0;
if (!eap->inner_method &&
eap_get_name(EAP_VENDOR_IETF, EAP_TYPE_MSCHAPV2) == NULL)
return 0;
}
if (eap->method == EAP_TYPE_TTLS) {
if (eap->inner_method == 0 && eap->inner_non_eap == 0)
return 1; /* Assume TTLS/MSCHAPv2 is used */
if (eap->inner_method &&
eap_get_name(EAP_VENDOR_IETF, eap->inner_method) == NULL)
return 0;
if (eap->inner_non_eap &&
eap->inner_non_eap != NAI_REALM_INNER_NON_EAP_PAP &&
eap->inner_non_eap != NAI_REALM_INNER_NON_EAP_CHAP &&
eap->inner_non_eap != NAI_REALM_INNER_NON_EAP_MSCHAP &&
eap->inner_non_eap != NAI_REALM_INNER_NON_EAP_MSCHAPV2)
return 0;
}
if (eap->inner_method &&
eap->inner_method != EAP_TYPE_GTC &&
eap->inner_method != EAP_TYPE_MSCHAPV2)
return 0;
return 1;
}
static int nai_realm_cred_cert(struct nai_realm_eap *eap)
{
if (eap_get_name(EAP_VENDOR_IETF, eap->method) == NULL)
return 0; /* method not supported */
if (eap->method != EAP_TYPE_TLS) {
/* Only EAP-TLS supported for credential authentication */
return 0;
}
return 1;
}
static struct nai_realm_eap * nai_realm_find_eap(struct wpa_cred *cred,
struct nai_realm *realm)
{
u8 e;
if (cred == NULL ||
cred->username == NULL ||
cred->username[0] == '\0' ||
((cred->password == NULL ||
cred->password[0] == '\0') &&
(cred->private_key == NULL ||
cred->private_key[0] == '\0')))
return NULL;
for (e = 0; e < realm->eap_count; e++) {
struct nai_realm_eap *eap = &realm->eap[e];
if (cred->password && cred->password[0] &&
nai_realm_cred_username(eap))
return eap;
if (cred->private_key && cred->private_key[0] &&
nai_realm_cred_cert(eap))
return eap;
}
return NULL;
}
#ifdef INTERWORKING_3GPP
static int plmn_id_match(struct wpabuf *anqp, const char *imsi, int mnc_len)
{
u8 plmn[3], plmn2[3];
const u8 *pos, *end;
u8 udhl;
/*
* See Annex A of 3GPP TS 24.234 v8.1.0 for description. The network
* operator is allowed to include only two digits of the MNC, so allow
* matches based on both two and three digit MNC assumptions. Since some
* SIM/USIM cards may not expose MNC length conveniently, we may be
* provided the default MNC length 3 here and as such, checking with MNC
* length 2 is justifiable even though 3GPP TS 24.234 does not mention
* that case. Anyway, MCC/MNC pair where both 2 and 3 digit MNC is used
* with otherwise matching values would not be good idea in general, so
* this should not result in selecting incorrect networks.
*/
/* Match with 3 digit MNC */
plmn[0] = (imsi[0] - '0') | ((imsi[1] - '0') << 4);
plmn[1] = (imsi[2] - '0') | ((imsi[5] - '0') << 4);
plmn[2] = (imsi[3] - '0') | ((imsi[4] - '0') << 4);
/* Match with 2 digit MNC */
plmn2[0] = (imsi[0] - '0') | ((imsi[1] - '0') << 4);
plmn2[1] = (imsi[2] - '0') | 0xf0;
plmn2[2] = (imsi[3] - '0') | ((imsi[4] - '0') << 4);
if (anqp == NULL)
return 0;
pos = wpabuf_head_u8(anqp);
end = pos + wpabuf_len(anqp);
if (pos + 2 > end)
return 0;
if (*pos != 0) {
wpa_printf(MSG_DEBUG, "Unsupported GUD version 0x%x", *pos);
return 0;
}
pos++;
udhl = *pos++;
if (pos + udhl > end) {
wpa_printf(MSG_DEBUG, "Invalid UDHL");
return 0;
}
end = pos + udhl;
wpa_printf(MSG_DEBUG, "Interworking: Matching against MCC/MNC alternatives: %02x:%02x:%02x or %02x:%02x:%02x (IMSI %s, MNC length %d)",
plmn[0], plmn[1], plmn[2], plmn2[0], plmn2[1], plmn2[2],
imsi, mnc_len);
while (pos + 2 <= end) {
u8 iei, len;
const u8 *l_end;
iei = *pos++;
len = *pos++ & 0x7f;
if (pos + len > end)
break;
l_end = pos + len;
if (iei == 0 && len > 0) {
/* PLMN List */
u8 num, i;
wpa_hexdump(MSG_DEBUG, "Interworking: PLMN List information element",
pos, len);
num = *pos++;
for (i = 0; i < num; i++) {
if (pos + 3 > l_end)
break;
if (os_memcmp(pos, plmn, 3) == 0 ||
os_memcmp(pos, plmn2, 3) == 0)
return 1; /* Found matching PLMN */
pos += 3;
}
} else {
wpa_hexdump(MSG_DEBUG, "Interworking: Unrecognized 3GPP information element",
pos, len);
}
pos = l_end;
}
return 0;
}
static int build_root_nai(char *nai, size_t nai_len, const char *imsi,
size_t mnc_len, char prefix)
{
const char *sep, *msin;
char *end, *pos;
size_t msin_len, plmn_len;
/*
* TS 23.003, Clause 14 (3GPP to WLAN Interworking)
* Root NAI:
* <aka:0|sim:1><IMSI>@wlan.mnc<MNC>.mcc<MCC>.3gppnetwork.org
* <MNC> is zero-padded to three digits in case two-digit MNC is used
*/
if (imsi == NULL || os_strlen(imsi) > 16) {
wpa_printf(MSG_DEBUG, "No valid IMSI available");
return -1;
}
sep = os_strchr(imsi, '-');
if (sep) {
plmn_len = sep - imsi;
msin = sep + 1;
} else if (mnc_len && os_strlen(imsi) >= 3 + mnc_len) {
plmn_len = 3 + mnc_len;
msin = imsi + plmn_len;
} else
return -1;
if (plmn_len != 5 && plmn_len != 6)
return -1;
msin_len = os_strlen(msin);
pos = nai;
end = nai + nai_len;
if (prefix)
*pos++ = prefix;
os_memcpy(pos, imsi, plmn_len);
pos += plmn_len;
os_memcpy(pos, msin, msin_len);
pos += msin_len;
pos += os_snprintf(pos, end - pos, "@wlan.mnc");
if (plmn_len == 5) {
*pos++ = '0';
*pos++ = imsi[3];
*pos++ = imsi[4];
} else {
*pos++ = imsi[3];
*pos++ = imsi[4];
*pos++ = imsi[5];
}
pos += os_snprintf(pos, end - pos, ".mcc%c%c%c.3gppnetwork.org",
imsi[0], imsi[1], imsi[2]);
return 0;
}
static int set_root_nai(struct wpa_ssid *ssid, const char *imsi, char prefix)
{
char nai[100];
if (build_root_nai(nai, sizeof(nai), imsi, 0, prefix) < 0)
return -1;
return wpa_config_set_quoted(ssid, "identity", nai);
}
#endif /* INTERWORKING_3GPP */
static int interworking_set_hs20_params(struct wpa_supplicant *wpa_s,
struct wpa_ssid *ssid)
{
if (wpa_config_set(ssid, "key_mgmt",
wpa_s->conf->pmf != NO_MGMT_FRAME_PROTECTION ?
"WPA-EAP WPA-EAP-SHA256" : "WPA-EAP", 0) < 0)
return -1;
if (wpa_config_set(ssid, "proto", "RSN", 0) < 0)
return -1;
if (wpa_config_set(ssid, "pairwise", "CCMP", 0) < 0)
return -1;
return 0;
}
static int interworking_connect_3gpp(struct wpa_supplicant *wpa_s,
struct wpa_cred *cred,
struct wpa_bss *bss)
{
#ifdef INTERWORKING_3GPP
struct wpa_ssid *ssid;
const u8 *ie;
int eap_type;
int res;
char prefix;
if (bss->anqp == NULL || bss->anqp->anqp_3gpp == NULL)
return -1;
ie = wpa_bss_get_ie(bss, WLAN_EID_SSID);
if (ie == NULL)
return -1;
wpa_printf(MSG_DEBUG, "Interworking: Connect with " MACSTR " (3GPP)",
MAC2STR(bss->bssid));
ssid = wpa_config_add_network(wpa_s->conf);
if (ssid == NULL)
return -1;
ssid->parent_cred = cred;
wpas_notify_network_added(wpa_s, ssid);
wpa_config_set_network_defaults(ssid);
ssid->priority = cred->priority;
ssid->temporary = 1;
ssid->ssid = os_zalloc(ie[1] + 1);
if (ssid->ssid == NULL)
goto fail;
os_memcpy(ssid->ssid, ie + 2, ie[1]);
ssid->ssid_len = ie[1];
if (interworking_set_hs20_params(wpa_s, ssid) < 0)
goto fail;
eap_type = EAP_TYPE_SIM;
if (cred->pcsc && wpa_s->scard && scard_supports_umts(wpa_s->scard))
eap_type = EAP_TYPE_AKA;
if (cred->eap_method && cred->eap_method[0].vendor == EAP_VENDOR_IETF) {
if (cred->eap_method[0].method == EAP_TYPE_SIM ||
cred->eap_method[0].method == EAP_TYPE_AKA ||
cred->eap_method[0].method == EAP_TYPE_AKA_PRIME)
eap_type = cred->eap_method[0].method;
}
switch (eap_type) {
case EAP_TYPE_SIM:
prefix = '1';
res = wpa_config_set(ssid, "eap", "SIM", 0);
break;
case EAP_TYPE_AKA:
prefix = '0';
res = wpa_config_set(ssid, "eap", "AKA", 0);
break;
case EAP_TYPE_AKA_PRIME:
prefix = '6';
res = wpa_config_set(ssid, "eap", "AKA'", 0);
break;
default:
res = -1;
break;
}
if (res < 0) {
wpa_printf(MSG_DEBUG, "Selected EAP method (%d) not supported",
eap_type);
goto fail;
}
if (!cred->pcsc && set_root_nai(ssid, cred->imsi, prefix) < 0) {
wpa_printf(MSG_DEBUG, "Failed to set Root NAI");
goto fail;
}
if (cred->milenage && cred->milenage[0]) {
if (wpa_config_set_quoted(ssid, "password",
cred->milenage) < 0)
goto fail;
} else if (cred->pcsc) {
if (wpa_config_set_quoted(ssid, "pcsc", "") < 0)
goto fail;
if (wpa_s->conf->pcsc_pin &&
wpa_config_set_quoted(ssid, "pin", wpa_s->conf->pcsc_pin)
< 0)
goto fail;
}
if (cred->password && cred->password[0] &&
wpa_config_set_quoted(ssid, "password", cred->password) < 0)
goto fail;
wpa_config_update_prio_list(wpa_s->conf);
interworking_reconnect(wpa_s);
return 0;
fail:
wpas_notify_network_removed(wpa_s, ssid);
wpa_config_remove_network(wpa_s->conf, ssid->id);
#endif /* INTERWORKING_3GPP */
return -1;
}
static int roaming_consortium_element_match(const u8 *ie, const u8 *rc_id,
size_t rc_len)
{
const u8 *pos, *end;
u8 lens;
if (ie == NULL)
return 0;
pos = ie + 2;
end = ie + 2 + ie[1];
/* Roaming Consortium element:
* Number of ANQP OIs
* OI #1 and #2 lengths
* OI #1, [OI #2], [OI #3]
*/
if (pos + 2 > end)
return 0;
pos++; /* skip Number of ANQP OIs */
lens = *pos++;
if (pos + (lens & 0x0f) + (lens >> 4) > end)
return 0;
if ((lens & 0x0f) == rc_len && os_memcmp(pos, rc_id, rc_len) == 0)
return 1;
pos += lens & 0x0f;
if ((lens >> 4) == rc_len && os_memcmp(pos, rc_id, rc_len) == 0)
return 1;
pos += lens >> 4;
if (pos < end && (size_t) (end - pos) == rc_len &&
os_memcmp(pos, rc_id, rc_len) == 0)
return 1;
return 0;
}
static int roaming_consortium_anqp_match(const struct wpabuf *anqp,
const u8 *rc_id, size_t rc_len)
{
const u8 *pos, *end;
u8 len;
if (anqp == NULL)
return 0;
pos = wpabuf_head(anqp);
end = pos + wpabuf_len(anqp);
/* Set of <OI Length, OI> duples */
while (pos < end) {
len = *pos++;
if (pos + len > end)
break;
if (len == rc_len && os_memcmp(pos, rc_id, rc_len) == 0)
return 1;
pos += len;
}
return 0;
}
static int roaming_consortium_match(const u8 *ie, const struct wpabuf *anqp,
const u8 *rc_id, size_t rc_len)
{
return roaming_consortium_element_match(ie, rc_id, rc_len) ||
roaming_consortium_anqp_match(anqp, rc_id, rc_len);
}
static int cred_excluded_ssid(struct wpa_cred *cred, struct wpa_bss *bss)
{
size_t i;
if (!cred->excluded_ssid)
return 0;
for (i = 0; i < cred->num_excluded_ssid; i++) {
struct excluded_ssid *e = &cred->excluded_ssid[i];
if (bss->ssid_len == e->ssid_len &&
os_memcmp(bss->ssid, e->ssid, e->ssid_len) == 0)
return 1;
}
return 0;
}
static struct wpa_cred * interworking_credentials_available_roaming_consortium(
struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
{
struct wpa_cred *cred, *selected = NULL;
const u8 *ie;
ie = wpa_bss_get_ie(bss, WLAN_EID_ROAMING_CONSORTIUM);
if (ie == NULL &&
(bss->anqp == NULL || bss->anqp->roaming_consortium == NULL))
return NULL;
if (wpa_s->conf->cred == NULL)
return NULL;
for (cred = wpa_s->conf->cred; cred; cred = cred->next) {
if (cred->roaming_consortium_len == 0)
continue;
if (!roaming_consortium_match(ie,
bss->anqp ?
bss->anqp->roaming_consortium :
NULL,
cred->roaming_consortium,
cred->roaming_consortium_len))
continue;
if (cred_excluded_ssid(cred, bss))
continue;
if (selected == NULL ||
selected->priority < cred->priority)
selected = cred;
}
return selected;
}
static int interworking_set_eap_params(struct wpa_ssid *ssid,
struct wpa_cred *cred, int ttls)
{
if (cred->eap_method) {
ttls = cred->eap_method->vendor == EAP_VENDOR_IETF &&
cred->eap_method->method == EAP_TYPE_TTLS;
os_free(ssid->eap.eap_methods);
ssid->eap.eap_methods =
os_malloc(sizeof(struct eap_method_type) * 2);
if (ssid->eap.eap_methods == NULL)
return -1;
os_memcpy(ssid->eap.eap_methods, cred->eap_method,
sizeof(*cred->eap_method));
ssid->eap.eap_methods[1].vendor = EAP_VENDOR_IETF;
ssid->eap.eap_methods[1].method = EAP_TYPE_NONE;
}
if (ttls && cred->username && cred->username[0]) {
const char *pos;
char *anon;
/* Use anonymous NAI in Phase 1 */
pos = os_strchr(cred->username, '@');
if (pos) {
size_t buflen = 9 + os_strlen(pos) + 1;
anon = os_malloc(buflen);
if (anon == NULL)
return -1;
os_snprintf(anon, buflen, "anonymous%s", pos);
} else if (cred->realm) {
size_t buflen = 10 + os_strlen(cred->realm) + 1;
anon = os_malloc(buflen);
if (anon == NULL)
return -1;
os_snprintf(anon, buflen, "anonymous@%s", cred->realm);
} else {
anon = os_strdup("anonymous");
if (anon == NULL)
return -1;
}
if (wpa_config_set_quoted(ssid, "anonymous_identity", anon) <
0) {
os_free(anon);
return -1;
}
os_free(anon);
}
if (cred->username && cred->username[0] &&
wpa_config_set_quoted(ssid, "identity", cred->username) < 0)
return -1;
if (cred->password && cred->password[0]) {
if (cred->ext_password &&
wpa_config_set(ssid, "password", cred->password, 0) < 0)
return -1;
if (!cred->ext_password &&
wpa_config_set_quoted(ssid, "password", cred->password) <
0)
return -1;
}
if (cred->client_cert && cred->client_cert[0] &&
wpa_config_set_quoted(ssid, "client_cert", cred->client_cert) < 0)
return -1;
#ifdef ANDROID
if (cred->private_key &&
os_strncmp(cred->private_key, "keystore://", 11) == 0) {
/* Use OpenSSL engine configuration for Android keystore */
if (wpa_config_set_quoted(ssid, "engine_id", "keystore") < 0 ||
wpa_config_set_quoted(ssid, "key_id",
cred->private_key + 11) < 0 ||
wpa_config_set(ssid, "engine", "1", 0) < 0)
return -1;
} else
#endif /* ANDROID */
if (cred->private_key && cred->private_key[0] &&
wpa_config_set_quoted(ssid, "private_key", cred->private_key) < 0)
return -1;
if (cred->private_key_passwd && cred->private_key_passwd[0] &&
wpa_config_set_quoted(ssid, "private_key_passwd",
cred->private_key_passwd) < 0)
return -1;
if (cred->phase1) {
os_free(ssid->eap.phase1);
ssid->eap.phase1 = os_strdup(cred->phase1);
}
if (cred->phase2) {
os_free(ssid->eap.phase2);
ssid->eap.phase2 = os_strdup(cred->phase2);
}
if (cred->ca_cert && cred->ca_cert[0] &&
wpa_config_set_quoted(ssid, "ca_cert", cred->ca_cert) < 0)
return -1;
return 0;
}
static int interworking_connect_roaming_consortium(
struct wpa_supplicant *wpa_s, struct wpa_cred *cred,
struct wpa_bss *bss, const u8 *ssid_ie)
{
struct wpa_ssid *ssid;
wpa_printf(MSG_DEBUG, "Interworking: Connect with " MACSTR " based on "
"roaming consortium match", MAC2STR(bss->bssid));
ssid = wpa_config_add_network(wpa_s->conf);
if (ssid == NULL)
return -1;
ssid->parent_cred = cred;
wpas_notify_network_added(wpa_s, ssid);
wpa_config_set_network_defaults(ssid);
ssid->priority = cred->priority;
ssid->temporary = 1;
ssid->ssid = os_zalloc(ssid_ie[1] + 1);
if (ssid->ssid == NULL)
goto fail;
os_memcpy(ssid->ssid, ssid_ie + 2, ssid_ie[1]);
ssid->ssid_len = ssid_ie[1];
if (interworking_set_hs20_params(wpa_s, ssid) < 0)
goto fail;
if (cred->eap_method == NULL) {
wpa_printf(MSG_DEBUG, "Interworking: No EAP method set for "
"credential using roaming consortium");
goto fail;
}
if (interworking_set_eap_params(
ssid, cred,
cred->eap_method->vendor == EAP_VENDOR_IETF &&
cred->eap_method->method == EAP_TYPE_TTLS) < 0)
goto fail;
wpa_config_update_prio_list(wpa_s->conf);
interworking_reconnect(wpa_s);
return 0;
fail:
wpas_notify_network_removed(wpa_s, ssid);
wpa_config_remove_network(wpa_s->conf, ssid->id);
return -1;
}
int interworking_connect(struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
{
struct wpa_cred *cred, *cred_rc, *cred_3gpp;
struct wpa_ssid *ssid;
struct nai_realm *realm;
struct nai_realm_eap *eap = NULL;
u16 count, i;
char buf[100];
const u8 *ie;
if (wpa_s->conf->cred == NULL || bss == NULL)
return -1;
ie = wpa_bss_get_ie(bss, WLAN_EID_SSID);
if (ie == NULL || ie[1] == 0) {
wpa_printf(MSG_DEBUG, "Interworking: No SSID known for "
MACSTR, MAC2STR(bss->bssid));
return -1;
}
if (!wpa_bss_get_ie(bss, WLAN_EID_RSN)) {
/*
* We currently support only HS 2.0 networks and those are
* required to use WPA2-Enterprise.
*/
wpa_printf(MSG_DEBUG, "Interworking: Network does not use "
"RSN");
return -1;
}
cred_rc = interworking_credentials_available_roaming_consortium(wpa_s,
bss);
if (cred_rc) {
wpa_printf(MSG_DEBUG, "Interworking: Highest roaming "
"consortium matching credential priority %d",
cred_rc->priority);
}
cred = interworking_credentials_available_realm(wpa_s, bss);
if (cred) {
wpa_printf(MSG_DEBUG, "Interworking: Highest NAI Realm list "
"matching credential priority %d",
cred->priority);
}
cred_3gpp = interworking_credentials_available_3gpp(wpa_s, bss);
if (cred_3gpp) {
wpa_printf(MSG_DEBUG, "Interworking: Highest 3GPP matching "
"credential priority %d", cred_3gpp->priority);
}
if (cred_rc &&
(cred == NULL || cred_rc->priority >= cred->priority) &&
(cred_3gpp == NULL || cred_rc->priority >= cred_3gpp->priority))
return interworking_connect_roaming_consortium(wpa_s, cred_rc,
bss, ie);
if (cred_3gpp &&
(cred == NULL || cred_3gpp->priority >= cred->priority)) {
return interworking_connect_3gpp(wpa_s, cred_3gpp, bss);
}
if (cred == NULL) {
wpa_printf(MSG_DEBUG, "Interworking: No matching credentials "
"found for " MACSTR, MAC2STR(bss->bssid));
return -1;
}
realm = nai_realm_parse(bss->anqp ? bss->anqp->nai_realm : NULL,
&count);
if (realm == NULL) {
wpa_printf(MSG_DEBUG, "Interworking: Could not parse NAI "
"Realm list from " MACSTR, MAC2STR(bss->bssid));
return -1;
}
for (i = 0; i < count; i++) {
if (!nai_realm_match(&realm[i], cred->realm))
continue;
eap = nai_realm_find_eap(cred, &realm[i]);
if (eap)
break;
}
if (!eap) {
wpa_printf(MSG_DEBUG, "Interworking: No matching credentials "
"and EAP method found for " MACSTR,
MAC2STR(bss->bssid));
nai_realm_free(realm, count);
return -1;
}
wpa_printf(MSG_DEBUG, "Interworking: Connect with " MACSTR,
MAC2STR(bss->bssid));
ssid = wpa_config_add_network(wpa_s->conf);
if (ssid == NULL) {
nai_realm_free(realm, count);
return -1;
}
ssid->parent_cred = cred;
wpas_notify_network_added(wpa_s, ssid);
wpa_config_set_network_defaults(ssid);
ssid->priority = cred->priority;
ssid->temporary = 1;
ssid->ssid = os_zalloc(ie[1] + 1);
if (ssid->ssid == NULL)
goto fail;
os_memcpy(ssid->ssid, ie + 2, ie[1]);
ssid->ssid_len = ie[1];
if (interworking_set_hs20_params(wpa_s, ssid) < 0)
goto fail;
if (wpa_config_set(ssid, "eap", eap_get_name(EAP_VENDOR_IETF,
eap->method), 0) < 0)
goto fail;
switch (eap->method) {
case EAP_TYPE_TTLS:
if (eap->inner_method) {
os_snprintf(buf, sizeof(buf), "\"autheap=%s\"",
eap_get_name(EAP_VENDOR_IETF,
eap->inner_method));
if (wpa_config_set(ssid, "phase2", buf, 0) < 0)
goto fail;
break;
}
switch (eap->inner_non_eap) {
case NAI_REALM_INNER_NON_EAP_PAP:
if (wpa_config_set(ssid, "phase2", "\"auth=PAP\"", 0) <
0)
goto fail;
break;
case NAI_REALM_INNER_NON_EAP_CHAP:
if (wpa_config_set(ssid, "phase2", "\"auth=CHAP\"", 0)
< 0)
goto fail;
break;
case NAI_REALM_INNER_NON_EAP_MSCHAP:
if (wpa_config_set(ssid, "phase2", "\"auth=MSCHAP\"",
0) < 0)
goto fail;
break;
case NAI_REALM_INNER_NON_EAP_MSCHAPV2:
if (wpa_config_set(ssid, "phase2", "\"auth=MSCHAPV2\"",
0) < 0)
goto fail;
break;
default:
/* EAP params were not set - assume TTLS/MSCHAPv2 */
if (wpa_config_set(ssid, "phase2", "\"auth=MSCHAPV2\"",
0) < 0)
goto fail;
break;
}
break;
case EAP_TYPE_PEAP:
os_snprintf(buf, sizeof(buf), "\"auth=%s\"",
eap_get_name(EAP_VENDOR_IETF,
eap->inner_method ?
eap->inner_method :
EAP_TYPE_MSCHAPV2));
if (wpa_config_set(ssid, "phase2", buf, 0) < 0)
goto fail;
break;
case EAP_TYPE_TLS:
break;
}
if (interworking_set_eap_params(ssid, cred,
eap->method == EAP_TYPE_TTLS) < 0)
goto fail;
nai_realm_free(realm, count);
wpa_config_update_prio_list(wpa_s->conf);
interworking_reconnect(wpa_s);
return 0;
fail:
wpas_notify_network_removed(wpa_s, ssid);
wpa_config_remove_network(wpa_s->conf, ssid->id);
nai_realm_free(realm, count);
return -1;
}
static struct wpa_cred * interworking_credentials_available_3gpp(
struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
{
struct wpa_cred *selected = NULL;
#ifdef INTERWORKING_3GPP
struct wpa_cred *cred;
int ret;
if (bss->anqp == NULL || bss->anqp->anqp_3gpp == NULL)
return NULL;
for (cred = wpa_s->conf->cred; cred; cred = cred->next) {
char *sep;
const char *imsi;
int mnc_len;
char imsi_buf[16];
size_t msin_len;
#ifdef PCSC_FUNCS
if (cred->pcsc && wpa_s->conf->pcsc_reader && wpa_s->scard &&
wpa_s->imsi[0]) {
imsi = wpa_s->imsi;
mnc_len = wpa_s->mnc_len;
goto compare;
}
#endif /* PCSC_FUNCS */
#ifdef CONFIG_EAP_PROXY
if (cred->pcsc && wpa_s->mnc_len > 0 && wpa_s->imsi[0]) {
imsi = wpa_s->imsi;
mnc_len = wpa_s->mnc_len;
goto compare;
}
#endif /* CONFIG_EAP_PROXY */
if (cred->imsi == NULL || !cred->imsi[0] ||
cred->milenage == NULL || !cred->milenage[0])
continue;
sep = os_strchr(cred->imsi, '-');
if (sep == NULL ||
(sep - cred->imsi != 5 && sep - cred->imsi != 6))
continue;
mnc_len = sep - cred->imsi - 3;
os_memcpy(imsi_buf, cred->imsi, 3 + mnc_len);
sep++;
msin_len = os_strlen(cred->imsi);
if (3 + mnc_len + msin_len >= sizeof(imsi_buf) - 1)
msin_len = sizeof(imsi_buf) - 3 - mnc_len - 1;
os_memcpy(&imsi_buf[3 + mnc_len], sep, msin_len);
imsi_buf[3 + mnc_len + msin_len] = '\0';
imsi = imsi_buf;
#if defined(PCSC_FUNCS) || defined(CONFIG_EAP_PROXY)
compare:
#endif /* PCSC_FUNCS || CONFIG_EAP_PROXY */
wpa_printf(MSG_DEBUG, "Interworking: Parsing 3GPP info from "
MACSTR, MAC2STR(bss->bssid));
ret = plmn_id_match(bss->anqp->anqp_3gpp, imsi, mnc_len);
wpa_printf(MSG_DEBUG, "PLMN match %sfound", ret ? "" : "not ");
if (ret) {
if (cred_excluded_ssid(cred, bss))
continue;
if (selected == NULL ||
selected->priority < cred->priority)
selected = cred;
}
}
#endif /* INTERWORKING_3GPP */
return selected;
}
static struct wpa_cred * interworking_credentials_available_realm(
struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
{
struct wpa_cred *cred, *selected = NULL;
struct nai_realm *realm;
u16 count, i;
if (bss->anqp == NULL || bss->anqp->nai_realm == NULL)
return NULL;
if (wpa_s->conf->cred == NULL)
return NULL;
wpa_printf(MSG_DEBUG, "Interworking: Parsing NAI Realm list from "
MACSTR, MAC2STR(bss->bssid));
realm = nai_realm_parse(bss->anqp->nai_realm, &count);
if (realm == NULL) {
wpa_printf(MSG_DEBUG, "Interworking: Could not parse NAI "
"Realm list from " MACSTR, MAC2STR(bss->bssid));
return NULL;
}
for (cred = wpa_s->conf->cred; cred; cred = cred->next) {
if (cred->realm == NULL)
continue;
for (i = 0; i < count; i++) {
if (!nai_realm_match(&realm[i], cred->realm))
continue;
if (nai_realm_find_eap(cred, &realm[i])) {
if (cred_excluded_ssid(cred, bss))
continue;
if (selected == NULL ||
selected->priority < cred->priority)
selected = cred;
break;
}
}
}
nai_realm_free(realm, count);
return selected;
}
static struct wpa_cred * interworking_credentials_available(
struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
{
struct wpa_cred *cred, *cred2;
cred = interworking_credentials_available_realm(wpa_s, bss);
cred2 = interworking_credentials_available_3gpp(wpa_s, bss);
if (cred && cred2 && cred2->priority >= cred->priority)
cred = cred2;
if (!cred)
cred = cred2;
cred2 = interworking_credentials_available_roaming_consortium(wpa_s,
bss);
if (cred && cred2 && cred2->priority >= cred->priority)
cred = cred2;
if (!cred)
cred = cred2;
return cred;
}
static int domain_name_list_contains(struct wpabuf *domain_names,
const char *domain)
{
const u8 *pos, *end;
size_t len;
len = os_strlen(domain);
pos = wpabuf_head(domain_names);
end = pos + wpabuf_len(domain_names);
while (pos + 1 < end) {
if (pos + 1 + pos[0] > end)
break;
wpa_hexdump_ascii(MSG_DEBUG, "Interworking: AP domain name",
pos + 1, pos[0]);
if (pos[0] == len &&
os_strncasecmp(domain, (const char *) (pos + 1), len) == 0)
return 1;
pos += 1 + pos[0];
}
return 0;
}
int interworking_home_sp_cred(struct wpa_supplicant *wpa_s,
struct wpa_cred *cred,
struct wpabuf *domain_names)
{
#ifdef INTERWORKING_3GPP
char nai[100], *realm;
char *imsi = NULL;
int mnc_len = 0;
if (cred->imsi)
imsi = cred->imsi;
#ifdef CONFIG_PCSC
else if (cred->pcsc && wpa_s->conf->pcsc_reader &&
wpa_s->scard && wpa_s->imsi[0]) {
imsi = wpa_s->imsi;
mnc_len = wpa_s->mnc_len;
}
#endif /* CONFIG_PCSC */
if (domain_names &&
imsi && build_root_nai(nai, sizeof(nai), imsi, mnc_len, 0) == 0) {
realm = os_strchr(nai, '@');
if (realm)
realm++;
wpa_printf(MSG_DEBUG, "Interworking: Search for match "
"with SIM/USIM domain %s", realm);
if (realm &&
domain_name_list_contains(domain_names, realm))
return 1;
}
#endif /* INTERWORKING_3GPP */
if (domain_names == NULL || cred->domain == NULL)
return 0;
wpa_printf(MSG_DEBUG, "Interworking: Search for match with "
"home SP FQDN %s", cred->domain);
if (domain_name_list_contains(domain_names, cred->domain))
return 1;
return 0;
}
static int interworking_home_sp(struct wpa_supplicant *wpa_s,
struct wpabuf *domain_names)
{
struct wpa_cred *cred;
if (domain_names == NULL || wpa_s->conf->cred == NULL)
return -1;
for (cred = wpa_s->conf->cred; cred; cred = cred->next) {
int res = interworking_home_sp_cred(wpa_s, cred, domain_names);
if (res)
return res;
}
return 0;
}
static int interworking_find_network_match(struct wpa_supplicant *wpa_s)
{
struct wpa_bss *bss;
struct wpa_ssid *ssid;
dl_list_for_each(bss, &wpa_s->bss, struct wpa_bss, list) {
for (ssid = wpa_s->conf->ssid; ssid; ssid = ssid->next) {
if (wpas_network_disabled(wpa_s, ssid) ||
ssid->mode != WPAS_MODE_INFRA)
continue;
if (ssid->ssid_len != bss->ssid_len ||
os_memcmp(ssid->ssid, bss->ssid, ssid->ssid_len) !=
0)
continue;
/*
* TODO: Consider more accurate matching of security
* configuration similarly to what is done in events.c
*/
return 1;
}
}
return 0;
}
static void interworking_select_network(struct wpa_supplicant *wpa_s)
{
struct wpa_bss *bss, *selected = NULL, *selected_home = NULL;
int selected_prio = -999999, selected_home_prio = -999999;
unsigned int count = 0;
const char *type;
int res;
struct wpa_cred *cred;
wpa_s->network_select = 0;
dl_list_for_each(bss, &wpa_s->bss, struct wpa_bss, list) {
cred = interworking_credentials_available(wpa_s, bss);
if (!cred)
continue;
if (!wpa_bss_get_ie(bss, WLAN_EID_RSN)) {
/*
* We currently support only HS 2.0 networks and those
* are required to use WPA2-Enterprise.
*/
wpa_printf(MSG_DEBUG, "Interworking: Credential match "
"with " MACSTR " but network does not use "
"RSN", MAC2STR(bss->bssid));
continue;
}
count++;
res = interworking_home_sp(wpa_s, bss->anqp ?
bss->anqp->domain_name : NULL);
if (res > 0)
type = "home";
else if (res == 0)
type = "roaming";
else
type = "unknown";
wpa_msg(wpa_s, MSG_INFO, INTERWORKING_AP MACSTR " type=%s",
MAC2STR(bss->bssid), type);
if (wpa_s->auto_select ||
(wpa_s->conf->auto_interworking &&
wpa_s->auto_network_select)) {
if (selected == NULL ||
cred->priority > selected_prio) {
selected = bss;
selected_prio = cred->priority;
}
if (res > 0 &&
(selected_home == NULL ||
cred->priority > selected_home_prio)) {
selected_home = bss;
selected_home_prio = cred->priority;
}
}
}
if (selected_home && selected_home != selected &&
selected_home_prio >= selected_prio) {
/* Prefer network operated by the Home SP */
selected = selected_home;
}
if (count == 0) {
/*
* No matching network was found based on configured
* credentials. Check whether any of the enabled network blocks
* have matching APs.
*/
if (interworking_find_network_match(wpa_s)) {
wpa_printf(MSG_DEBUG, "Interworking: Possible BSS "
"match for enabled network configurations");
if (wpa_s->auto_select)
interworking_reconnect(wpa_s);
return;
}
if (wpa_s->auto_network_select) {
wpa_printf(MSG_DEBUG, "Interworking: Continue "
"scanning after ANQP fetch");
wpa_supplicant_req_scan(wpa_s, wpa_s->scan_interval,
0);
return;
}
wpa_msg(wpa_s, MSG_INFO, INTERWORKING_NO_MATCH "No network "
"with matching credentials found");
}
if (selected)
interworking_connect(wpa_s, selected);
}
static struct wpa_bss_anqp *
interworking_match_anqp_info(struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
{
struct wpa_bss *other;
if (is_zero_ether_addr(bss->hessid))
return NULL; /* Cannot be in the same homegenous ESS */
dl_list_for_each(other, &wpa_s->bss, struct wpa_bss, list) {
if (other == bss)
continue;
if (other->anqp == NULL)
continue;
if (other->anqp->roaming_consortium == NULL &&
other->anqp->nai_realm == NULL &&
other->anqp->anqp_3gpp == NULL &&
other->anqp->domain_name == NULL)
continue;
if (!(other->flags & WPA_BSS_ANQP_FETCH_TRIED))
continue;
if (os_memcmp(bss->hessid, other->hessid, ETH_ALEN) != 0)
continue;
if (bss->ssid_len != other->ssid_len ||
os_memcmp(bss->ssid, other->ssid, bss->ssid_len) != 0)
continue;
wpa_printf(MSG_DEBUG, "Interworking: Share ANQP data with "
"already fetched BSSID " MACSTR " and " MACSTR,
MAC2STR(other->bssid), MAC2STR(bss->bssid));
other->anqp->users++;
return other->anqp;
}
return NULL;
}
static void interworking_next_anqp_fetch(struct wpa_supplicant *wpa_s)
{
struct wpa_bss *bss;
int found = 0;
const u8 *ie;
if (eloop_terminated() || !wpa_s->fetch_anqp_in_progress)
return;
dl_list_for_each(bss, &wpa_s->bss, struct wpa_bss, list) {
if (!(bss->caps & IEEE80211_CAP_ESS))
continue;
ie = wpa_bss_get_ie(bss, WLAN_EID_EXT_CAPAB);
if (ie == NULL || ie[1] < 4 || !(ie[5] & 0x80))
continue; /* AP does not support Interworking */
if (!(bss->flags & WPA_BSS_ANQP_FETCH_TRIED)) {
if (bss->anqp == NULL) {
bss->anqp = interworking_match_anqp_info(wpa_s,
bss);
if (bss->anqp) {
/* Shared data already fetched */
continue;
}
bss->anqp = wpa_bss_anqp_alloc();
if (bss->anqp == NULL)
break;
}
found++;
bss->flags |= WPA_BSS_ANQP_FETCH_TRIED;
wpa_msg(wpa_s, MSG_INFO, "Starting ANQP fetch for "
MACSTR, MAC2STR(bss->bssid));
interworking_anqp_send_req(wpa_s, bss);
break;
}
}
if (found == 0) {
wpa_msg(wpa_s, MSG_INFO, "ANQP fetch completed");
wpa_s->fetch_anqp_in_progress = 0;
if (wpa_s->network_select)
interworking_select_network(wpa_s);
}
}
void interworking_start_fetch_anqp(struct wpa_supplicant *wpa_s)
{
struct wpa_bss *bss;
dl_list_for_each(bss, &wpa_s->bss, struct wpa_bss, list)
bss->flags &= ~WPA_BSS_ANQP_FETCH_TRIED;
wpa_s->fetch_anqp_in_progress = 1;
interworking_next_anqp_fetch(wpa_s);
}
int interworking_fetch_anqp(struct wpa_supplicant *wpa_s)
{
if (wpa_s->fetch_anqp_in_progress || wpa_s->network_select)
return 0;
wpa_s->network_select = 0;
wpa_s->fetch_all_anqp = 1;
interworking_start_fetch_anqp(wpa_s);
return 0;
}
void interworking_stop_fetch_anqp(struct wpa_supplicant *wpa_s)
{
if (!wpa_s->fetch_anqp_in_progress)
return;
wpa_s->fetch_anqp_in_progress = 0;
}
int anqp_send_req(struct wpa_supplicant *wpa_s, const u8 *dst,
u16 info_ids[], size_t num_ids)
{
struct wpabuf *buf;
int ret = 0;
int freq;
struct wpa_bss *bss;
int res;
freq = wpa_s->assoc_freq;
bss = wpa_bss_get_bssid(wpa_s, dst);
if (bss) {
wpa_bss_anqp_unshare_alloc(bss);
freq = bss->freq;
}
if (freq <= 0)
return -1;
wpa_printf(MSG_DEBUG, "ANQP: Query Request to " MACSTR " for %u id(s)",
MAC2STR(dst), (unsigned int) num_ids);
buf = anqp_build_req(info_ids, num_ids, NULL);
if (buf == NULL)
return -1;
res = gas_query_req(wpa_s->gas, dst, freq, buf, anqp_resp_cb, wpa_s);
if (res < 0) {
wpa_printf(MSG_DEBUG, "ANQP: Failed to send Query Request");
ret = -1;
} else
wpa_printf(MSG_DEBUG, "ANQP: Query started with dialog token "
"%u", res);
wpabuf_free(buf);
return ret;
}
static void interworking_parse_rx_anqp_resp(struct wpa_supplicant *wpa_s,
struct wpa_bss *bss, const u8 *sa,
u16 info_id,
const u8 *data, size_t slen)
{
const u8 *pos = data;
struct wpa_bss_anqp *anqp = NULL;
#ifdef CONFIG_HS20
u8 type;
#endif /* CONFIG_HS20 */
if (bss)
anqp = bss->anqp;
switch (info_id) {
case ANQP_CAPABILITY_LIST:
wpa_msg(wpa_s, MSG_INFO, "RX-ANQP " MACSTR
" ANQP Capability list", MAC2STR(sa));
break;
case ANQP_VENUE_NAME:
wpa_msg(wpa_s, MSG_INFO, "RX-ANQP " MACSTR
" Venue Name", MAC2STR(sa));
wpa_hexdump_ascii(MSG_DEBUG, "ANQP: Venue Name", pos, slen);
if (anqp) {
wpabuf_free(anqp->venue_name);
anqp->venue_name = wpabuf_alloc_copy(pos, slen);
}
break;
case ANQP_NETWORK_AUTH_TYPE:
wpa_msg(wpa_s, MSG_INFO, "RX-ANQP " MACSTR
" Network Authentication Type information",
MAC2STR(sa));
wpa_hexdump_ascii(MSG_DEBUG, "ANQP: Network Authentication "
"Type", pos, slen);
if (anqp) {
wpabuf_free(anqp->network_auth_type);
anqp->network_auth_type = wpabuf_alloc_copy(pos, slen);
}
break;
case ANQP_ROAMING_CONSORTIUM:
wpa_msg(wpa_s, MSG_INFO, "RX-ANQP " MACSTR
" Roaming Consortium list", MAC2STR(sa));
wpa_hexdump_ascii(MSG_DEBUG, "ANQP: Roaming Consortium",
pos, slen);
if (anqp) {
wpabuf_free(anqp->roaming_consortium);
anqp->roaming_consortium = wpabuf_alloc_copy(pos, slen);
}
break;
case ANQP_IP_ADDR_TYPE_AVAILABILITY:
wpa_msg(wpa_s, MSG_INFO, "RX-ANQP " MACSTR
" IP Address Type Availability information",
MAC2STR(sa));
wpa_hexdump(MSG_MSGDUMP, "ANQP: IP Address Availability",
pos, slen);
if (anqp) {
wpabuf_free(anqp->ip_addr_type_availability);
anqp->ip_addr_type_availability =
wpabuf_alloc_copy(pos, slen);
}
break;
case ANQP_NAI_REALM:
wpa_msg(wpa_s, MSG_INFO, "RX-ANQP " MACSTR
" NAI Realm list", MAC2STR(sa));
wpa_hexdump_ascii(MSG_DEBUG, "ANQP: NAI Realm", pos, slen);
if (anqp) {
wpabuf_free(anqp->nai_realm);
anqp->nai_realm = wpabuf_alloc_copy(pos, slen);
}
break;
case ANQP_3GPP_CELLULAR_NETWORK:
wpa_msg(wpa_s, MSG_INFO, "RX-ANQP " MACSTR
" 3GPP Cellular Network information", MAC2STR(sa));
wpa_hexdump_ascii(MSG_DEBUG, "ANQP: 3GPP Cellular Network",
pos, slen);
if (anqp) {
wpabuf_free(anqp->anqp_3gpp);
anqp->anqp_3gpp = wpabuf_alloc_copy(pos, slen);
}
break;
case ANQP_DOMAIN_NAME:
wpa_msg(wpa_s, MSG_INFO, "RX-ANQP " MACSTR
" Domain Name list", MAC2STR(sa));
wpa_hexdump_ascii(MSG_MSGDUMP, "ANQP: Domain Name", pos, slen);
if (anqp) {
wpabuf_free(anqp->domain_name);
anqp->domain_name = wpabuf_alloc_copy(pos, slen);
}
break;
case ANQP_VENDOR_SPECIFIC:
if (slen < 3)
return;
switch (WPA_GET_BE24(pos)) {
#ifdef CONFIG_HS20
case OUI_WFA:
pos += 3;
slen -= 3;
if (slen < 1)
return;
type = *pos++;
slen--;
switch (type) {
case HS20_ANQP_OUI_TYPE:
hs20_parse_rx_hs20_anqp_resp(wpa_s, sa, pos,
slen);
break;
default:
wpa_printf(MSG_DEBUG, "HS20: Unsupported ANQP "
"vendor type %u", type);
break;
}
break;
#endif /* CONFIG_HS20 */
default:
wpa_printf(MSG_DEBUG, "Interworking: Unsupported "
"vendor-specific ANQP OUI %06x",
WPA_GET_BE24(pos));
return;
}
break;
default:
wpa_printf(MSG_DEBUG, "Interworking: Unsupported ANQP Info ID "
"%u", info_id);
break;
}
}
void anqp_resp_cb(void *ctx, const u8 *dst, u8 dialog_token,
enum gas_query_result result,
const struct wpabuf *adv_proto,
const struct wpabuf *resp, u16 status_code)
{
struct wpa_supplicant *wpa_s = ctx;
const u8 *pos;
const u8 *end;
u16 info_id;
u16 slen;
struct wpa_bss *bss = NULL, *tmp;
if (result != GAS_QUERY_SUCCESS)
return;
pos = wpabuf_head(adv_proto);
if (wpabuf_len(adv_proto) < 4 || pos[0] != WLAN_EID_ADV_PROTO ||
pos[1] < 2 || pos[3] != ACCESS_NETWORK_QUERY_PROTOCOL) {
wpa_printf(MSG_DEBUG, "ANQP: Unexpected Advertisement "
"Protocol in response");
return;
}
/*
* If possible, select the BSS entry based on which BSS entry was used
* for the request. This can help in cases where multiple BSS entries
* may exist for the same AP.
*/
dl_list_for_each_reverse(tmp, &wpa_s->bss, struct wpa_bss, list) {
if (tmp == wpa_s->interworking_gas_bss &&
os_memcmp(tmp->bssid, dst, ETH_ALEN) == 0) {
bss = tmp;
break;
}
}
if (bss == NULL)
bss = wpa_bss_get_bssid(wpa_s, dst);
pos = wpabuf_head(resp);
end = pos + wpabuf_len(resp);
while (pos < end) {
if (pos + 4 > end) {
wpa_printf(MSG_DEBUG, "ANQP: Invalid element");
break;
}
info_id = WPA_GET_LE16(pos);
pos += 2;
slen = WPA_GET_LE16(pos);
pos += 2;
if (pos + slen > end) {
wpa_printf(MSG_DEBUG, "ANQP: Invalid element length "
"for Info ID %u", info_id);
break;
}
interworking_parse_rx_anqp_resp(wpa_s, bss, dst, info_id, pos,
slen);
pos += slen;
}
}
static void interworking_scan_res_handler(struct wpa_supplicant *wpa_s,
struct wpa_scan_results *scan_res)
{
wpa_printf(MSG_DEBUG, "Interworking: Scan results available - start "
"ANQP fetch");
interworking_start_fetch_anqp(wpa_s);
}
int interworking_select(struct wpa_supplicant *wpa_s, int auto_select)
{
interworking_stop_fetch_anqp(wpa_s);
wpa_s->network_select = 1;
wpa_s->auto_network_select = 0;
wpa_s->auto_select = !!auto_select;
wpa_s->fetch_all_anqp = 0;
wpa_printf(MSG_DEBUG, "Interworking: Start scan for network "
"selection");
wpa_s->scan_res_handler = interworking_scan_res_handler;
wpa_s->scan_req = MANUAL_SCAN_REQ;
wpa_supplicant_req_scan(wpa_s, 0, 0);
return 0;
}
static void gas_resp_cb(void *ctx, const u8 *addr, u8 dialog_token,
enum gas_query_result result,
const struct wpabuf *adv_proto,
const struct wpabuf *resp, u16 status_code)
{
struct wpa_supplicant *wpa_s = ctx;
wpa_msg(wpa_s, MSG_INFO, GAS_RESPONSE_INFO "addr=" MACSTR
" dialog_token=%d status_code=%d resp_len=%d",
MAC2STR(addr), dialog_token, status_code,
resp ? (int) wpabuf_len(resp) : -1);
if (!resp)
return;
wpabuf_free(wpa_s->last_gas_resp);
wpa_s->last_gas_resp = wpabuf_dup(resp);
if (wpa_s->last_gas_resp == NULL)
return;
os_memcpy(wpa_s->last_gas_addr, addr, ETH_ALEN);
wpa_s->last_gas_dialog_token = dialog_token;
}
int gas_send_request(struct wpa_supplicant *wpa_s, const u8 *dst,
const struct wpabuf *adv_proto,
const struct wpabuf *query)
{
struct wpabuf *buf;
int ret = 0;
int freq;
struct wpa_bss *bss;
int res;
size_t len;
u8 query_resp_len_limit = 0, pame_bi = 0;
freq = wpa_s->assoc_freq;
bss = wpa_bss_get_bssid(wpa_s, dst);
if (bss)
freq = bss->freq;
if (freq <= 0)
return -1;
wpa_printf(MSG_DEBUG, "GAS request to " MACSTR " (freq %d MHz)",
MAC2STR(dst), freq);
wpa_hexdump_buf(MSG_DEBUG, "Advertisement Protocol ID", adv_proto);
wpa_hexdump_buf(MSG_DEBUG, "GAS Query", query);
len = 3 + wpabuf_len(adv_proto) + 2;
if (query)
len += wpabuf_len(query);
buf = gas_build_initial_req(0, len);
if (buf == NULL)
return -1;
/* Advertisement Protocol IE */
wpabuf_put_u8(buf, WLAN_EID_ADV_PROTO);
wpabuf_put_u8(buf, 1 + wpabuf_len(adv_proto)); /* Length */
wpabuf_put_u8(buf, (query_resp_len_limit & 0x7f) |
(pame_bi ? 0x80 : 0));
wpabuf_put_buf(buf, adv_proto);
/* GAS Query */
if (query) {
wpabuf_put_le16(buf, wpabuf_len(query));
wpabuf_put_buf(buf, query);
} else
wpabuf_put_le16(buf, 0);
res = gas_query_req(wpa_s->gas, dst, freq, buf, gas_resp_cb, wpa_s);
if (res < 0) {
wpa_printf(MSG_DEBUG, "GAS: Failed to send Query Request");
ret = -1;
} else
wpa_printf(MSG_DEBUG, "GAS: Query started with dialog token "
"%u", res);
wpabuf_free(buf);
return ret;
}