diff --git a/Makefile.am b/Makefile.am index a2d1a55f..7d9ef085 100644 --- a/Makefile.am +++ b/Makefile.am @@ -205,6 +205,7 @@ src_iwd_SOURCES = src/main.c linux/nl80211.h src/iwd.h src/missing.h \ src/fils.h src/fils.c \ src/rtnlutil.h src/rtnlutil.c \ src/auth-proto.h \ + src/anqp.h src/anqp.c \ $(eap_sources) \ $(builtin_sources) src_iwd_LDADD = $(ell_ldadd) -ldl diff --git a/src/anqp.c b/src/anqp.c new file mode 100644 index 00000000..b058b046 --- /dev/null +++ b/src/anqp.c @@ -0,0 +1,361 @@ +/* + * + * Wireless daemon for Linux + * + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "src/anqp.h" +#include "src/util.h" +#include "src/eap-private.h" + +static const uint8_t wifi_alliance_oui[3] = { 0x50, 0x6f, 0x9a }; + +void anqp_iter_init(struct anqp_iter *iter, const unsigned char *anqp, + unsigned int len) +{ + iter->anqp = anqp; + iter->max = len; + iter->pos = 0; +} + +bool anqp_iter_next(struct anqp_iter *iter, uint16_t *id, uint16_t *len, + const void **data) +{ + const unsigned char *anqp = iter->anqp + iter->pos; + const unsigned char *end = iter->anqp + iter->max; + + if (iter->pos + 4 >= iter->max) + return false; + + if (anqp + l_get_le16(anqp + 2) > end) + return false; + + *id = l_get_le16(anqp); + anqp += 2; + + *len = l_get_le16(anqp); + anqp += 2; + + *data = anqp; + + iter->id = *id; + iter->len = *len; + iter->data = *data; + + iter->pos = anqp + *len - iter->anqp; + + return true; +} + +bool anqp_hs20_parse_osu_provider_nai(const unsigned char *anqp, + unsigned int len, const char **nai_out) +{ + uint8_t nai_len; + static char nai[256] = { 0 }; + + if (len < 1) + return false; + + nai_len = *anqp++; + len--; + + if (len < nai_len) + return false; + + memcpy(nai, anqp, nai_len); + + *nai_out = nai; + + return true; +} + +bool anqp_iter_is_hs20(const struct anqp_iter *iter, uint8_t *stype, + unsigned int *len, const unsigned char **data) +{ + const unsigned char *anqp = iter->data; + unsigned int anqp_len = iter->len; + uint8_t type; + + if (iter->len < 6) + return false; + + if (memcmp(anqp, wifi_alliance_oui, 3)) + return false; + + anqp += 3; + anqp_len -= 3; + + type = *anqp++; + anqp_len--; + + if (type != 0x11) + return false; + + *stype = *anqp++; + anqp_len--; + + /* reserved byte */ + anqp++; + anqp_len--; + + *data = anqp; + *len = anqp_len; + + return true; +} + +static bool parse_eap_params(const unsigned char *anqp, unsigned int len, + uint8_t *method, uint8_t *non_eap_inner, + uint8_t *eap_inner, uint8_t *credential, + uint8_t *tunneled_credential) +{ + uint8_t param_count; + + if (len < 2) + return false; + + *method = *anqp++; + param_count = *anqp++; + + len -= 2; + + *non_eap_inner = 0; + *eap_inner = 0; + *credential = 0; + *tunneled_credential = 0; + + while (param_count--) { + uint8_t ap_id; + uint8_t ap_len; + + if (len < 2) + return false; + + ap_id = *anqp++; + ap_len = *anqp++; + len -= 2; + + if (len < ap_len) + return false; + + switch (ap_id) { + case ANQP_AP_NON_INNER_AUTH_EAP: + *non_eap_inner = *anqp; + break; + case ANQP_AP_INNER_AUTH_EAP: + *eap_inner = *anqp; + break; + case ANQP_AP_CREDENTIAL: + *credential = *anqp; + break; + case ANQP_AP_TUNNELED_EAP_CREDENTIAL: + *tunneled_credential = *anqp; + break; + case ANQP_AP_EXPANDED_EAP_METHOD: + case ANQP_AP_EXPANDED_INNER_EAP_METHOD: + case ANQP_AP_VENDOR_SPECIFIC: + break; + } + + anqp += ap_len; + len -= ap_len; + } + + return true; +} + +/* + * Parses an EAP ANQP list. + */ +static bool parse_eap(const unsigned char *anqp, unsigned int len, + const char *nai, bool hs20, + struct anqp_eap_method *method_out) +{ + uint8_t eap_count; + + if (len < 1) + return false; + + eap_count = *anqp++; + len--; + + while (eap_count--) { + uint8_t eap_len; + uint8_t method; + uint8_t non_eap_inner; + uint8_t eap_inner; + uint8_t credential; + uint8_t tunneled_credential; + + if (len < 1) + return false; + + eap_len = *anqp++; + len--; + + if (!parse_eap_params(anqp, eap_len, + &method, &non_eap_inner, + &eap_inner, &credential, + &tunneled_credential)) + return false; + + if (hs20) { + /* + * TODO: Support EAP-SIM/AKA/AKA' with Hotspot + */ + if (method != EAP_TYPE_TTLS) { + l_debug("EAP method %u not supported", method); + goto next; + } + + /* MSCHAPv2 */ + if (non_eap_inner != 4) { + l_debug("Non-EAP inner %u not supported", + non_eap_inner); + goto next; + } + + /* username/password */ + if (credential != 7) { + l_debug("credential type %u not supported", + credential); + goto next; + } + } else { + /* can't use methods without user/password */ + if (credential != 7 && tunneled_credential != 7) + goto next; + } + + method_out->method = method; + /* nai is guarenteed to NULL terminate and be < 256 bytes */ + l_strlcpy(method_out->realm, nai, + sizeof(method_out->realm) - 1); + method_out->non_eap_inner = non_eap_inner; + method_out->eap_inner = eap_inner; + method_out->credential = credential; + method_out->tunneled_credential = tunneled_credential; + + return true; + +next: + if (len < eap_len) + return false; + + anqp += eap_len; + len -= eap_len; + } + + return false; +} + +/* + * Parses NAI Realm ANQP-element. The code here parses the NAI Realm until an + * acceptable EAP method is found. Once a method is found it is returned via + * method_out. The structure of NAI realm is such that it does not allow for a + * convenient static structure (several nested lists). Since we can only handle + * EAP methods with user/password credentials anyways it makes sense to just + * return the first EAP method found that meets our criteria. In addition, this + * is only being used for Hotspot 2.0, which mandates EAP-TLS/TTLS/SIM/AKA, + * meaning TTLS is the only contender for this parsing. + * + * @param hs20 true if this parsing is for a Hotspot 2.0 network. This will + * restrict what EAP method info is chosen as to comply with the + * Hotspot 2.0 spec (i.e. EAP-TTLS w/ MSCHAPv2 or SIM/AKA/AKA'). + */ +bool anqp_parse_nai_realm(const unsigned char *anqp, unsigned int len, + bool hs20, struct anqp_eap_method *method_out) +{ + uint16_t count; + + if (len < 2) + return false; + + count = l_get_le16(anqp); + + anqp += 2; + len -= 2; + + while (count--) { + uint16_t realm_len; + uint8_t encoding; + uint8_t nai_len; + char nai_realm[256] = { 0 }; + + /* + * The method list is a variable field, so the only way to + * reliably increment anqp is by realm_len at the very end since + * we dont know how many bytes parse_eap advanced (it does + * internal length checking so it should not overflow). We + * cant incrementally advance anqp/len, hence the hardcoded + * length and pointer adjustments. + */ + + if (len < 4) + return false; + + realm_len = l_get_le16(anqp); + anqp += 2; + len -= 2; + + encoding = anqp[0]; + + nai_len = anqp[1]; + + if (len - 2 < nai_len) + return false; + + memcpy(nai_realm, anqp + 2, nai_len); + + /* + * TODO: Verify NAI encoding in accordance with RFC 4282 ? + * + * The encoding in RFC 4282 seems to only limit which characters + * can be used in an NAI. Since these come in from public + * action frames it could have been spoofed, but ultimately if + * its bogus the AP won't allow us to connect. + */ + if (!util_is_bit_set(encoding, 0)) + l_warn("Not verifying NAI encoding"); + else if (!l_utf8_validate(nai_realm, nai_len, NULL)) { + l_warn("NAI is not UTF-8"); + return false; + } + + if (parse_eap(anqp + 2 + nai_len, realm_len - 2 - nai_len, + nai_realm, hs20, method_out)) + return true; + + if (len < realm_len) + return false; + + anqp += realm_len; + len -= realm_len; + } + + return false; +} diff --git a/src/anqp.h b/src/anqp.h new file mode 100644 index 00000000..f70c62f0 --- /dev/null +++ b/src/anqp.h @@ -0,0 +1,117 @@ +/* + * + * Wireless daemon for Linux + * + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include + +/* IEEE 802.11-2016 Section 9.4.5 ANQP elements */ +enum anqp_element { + /* 0-255 reserved */ + ANQP_QUERY_LIST = 256, + ANQP_CAPABILITY_LIST = 257, + ANQP_VENUE_NAME = 258, + ANQP_EMERGENCY_CALL_NUMBER = 259, + ANQP_NETWORK_AUTH_TYPE = 260, + ANQP_ROAMING_CONSORTIUM = 261, + ANQP_IP_ADDRESS_TYPE_AVAILABILITY = 262, + ANQP_NAI_REALM = 263, + ANQP_3GPP_CELLULAR_NETWORK = 264, + ANQP_AP_GEOSPATIAL_LOCATION = 265, + ANQP_AP_CIVIC_LOCATION = 266, + ANQP_AP_LOCATION_PUBLIC_ID = 267, + ANQP_DOMAIN_NAME = 268, + ANQP_EMERGENCY_ALERT_ID_URI = 269, + ANQP_TDLS_CAPABILITY = 270, + ANQP_EMERGENCY_NAI = 271, + ANQP_NEIGHBOR_REPORT = 272, + /* 273-276 reserved */ + ANQP_VENUE_URI = 277, + ANQP_ADVICE_OF_CHARGE = 278, + ANQP_LOCAL_CONTENT = 279, + ANQP_NETWORK_AUTH_TYPE_WITH_TIMESTAMP = 280, + /* 281-56796 reserved */ + ANQP_VENDOR_SPECIFIC = 56797, + /* 56798-65535 reserved */ +}; + +/* WiFi Alliance Hotspot 2.0 Spec - Section 4 Hotspot 2.0 ANQP-elements */ +enum anqp_hs20_element { + ANQP_HS20_QUERY_LIST = 1, + ANQP_HS20_CAPABILITY_LIST = 2, + ANQP_HS20_OPERATOR_FRIENDLY_NAME = 3, + ANQP_HS20_WLAN_METRICS = 4, + ANQP_HS20_CONNECTION_CAPABILITY = 5, + ANQP_HS20_NAI_HOME_REALM_QUERY = 6, + ANQP_HS20_OPERATING_CLASS_INDICATION = 7, + ANQP_HS20_OSU_PROVIDERS_LIST = 8, + /* 9 reserved */ + ANQP_HS20_ICON_REQUST = 10, + ANQP_HS20_ICON_BINARY_FILE = 11, + ANQP_HS20_OPERATOR_ICON_METADATA = 12, + ANQP_HS20_OSU_PROVIDERS_NAI_LIST = 13, + /* 14 - 255 reserved */ +}; + +/* IEEE 802.11-2016 Table 9-275 Authentication Parameter types */ +enum anqp_auth_parameter_type { + ANQP_AP_EXPANDED_EAP_METHOD = 1, + ANQP_AP_NON_INNER_AUTH_EAP = 2, + ANQP_AP_INNER_AUTH_EAP = 3, + ANQP_AP_EXPANDED_INNER_EAP_METHOD = 4, + ANQP_AP_CREDENTIAL = 5, + ANQP_AP_TUNNELED_EAP_CREDENTIAL = 6, + ANQP_AP_VENDOR_SPECIFIC = 221, +}; + +struct anqp_iter { + unsigned int max; + unsigned int pos; + const unsigned char *anqp; + + unsigned int id; + unsigned int len; + const unsigned char *data; +}; + +/* + * TODO: Support expanded EAP types + */ +struct anqp_eap_method { + char realm[256]; + uint8_t method; + uint8_t non_eap_inner; + uint8_t eap_inner; + uint8_t credential; + uint8_t tunneled_credential; +}; + +void anqp_iter_init(struct anqp_iter *iter, const unsigned char *anqp, + unsigned int len); +bool anqp_iter_next(struct anqp_iter *iter, uint16_t *id, uint16_t *len, + const void **data); +bool anqp_iter_is_hs20(const struct anqp_iter *iter, uint8_t *stype, + unsigned int *len, const unsigned char **data); +bool anqp_hs20_parse_osu_provider_nai(const unsigned char *anqp, + unsigned int len, const char **nai_out); +bool anqp_parse_nai_realm(const unsigned char *anqp, unsigned int len, + bool hs20, struct anqp_eap_method *method);