diff --git a/Makefile.am b/Makefile.am index b326ad66..4fd3f33c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -167,8 +167,7 @@ eap_sources = src/eap.c src/eap.h src/eap-private.h \ src/simauth.h src/simauth.c \ src/watchlist.h src/watchlist.c \ src/eap-tls-common.h src/eap-tls-common.c \ - src/mschaputil.h src/mschaputil.c \ - src/erpcache.h src/erpcache.c + src/mschaputil.h src/mschaputil.c if DAEMON libexec_PROGRAMS += src/iwd @@ -203,6 +202,7 @@ src_iwd_SOURCES = src/main.c linux/nl80211.h src/iwd.h src/missing.h \ src/owe.h src/owe.c \ src/blacklist.h src/blacklist.c \ src/manager.c \ + src/erp.h src/erp.c \ $(eap_sources) \ $(builtin_sources) src_iwd_LDADD = $(ell_ldadd) -ldl diff --git a/src/erp.c b/src/erp.c new file mode 100644 index 00000000..7cc21cc2 --- /dev/null +++ b/src/erp.c @@ -0,0 +1,527 @@ +/* + * + * 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 + +#include "eap-private.h" +#include "erp.h" +#include "crypto.h" +#include "util.h" + +#define ERP_DEFAULT_KEY_LIFETIME_US 86400000000 + +struct erp_cache_entry { + char *id; + void *emsk; + size_t emsk_len; + void *session_id; + size_t session_len; + char *ssid; + uint64_t expire_time; + uint32_t ref; + bool invalid : 1; +}; + +struct erp_state { + erp_tx_packet_func_t tx_packet; + erp_complete_func_t complete; + void *user_data; + + struct erp_cache_entry *cache; + + uint8_t r_rk[64]; + uint8_t r_ik[64]; + char keyname_nai[254]; + uint16_t seq; +}; + +enum eap_erp_type { + ERP_TYPE_REAUTH_START = 1, + ERP_TYPE_REAUTH = 2, +}; + +enum eap_erp_tlv { + ERP_TLV_KEYNAME_NAI = 1, + ERP_TV_RRK_LIFETIME = 2, + ERP_TV_RMSK_LIFETIME = 3, + ERP_TLV_DOMAIN_NAME = 4, + ERP_TLV_CRYPTOSUITES = 5, + ERP_TLV_AUTH_INDICATION = 6, + ERP_TLV_CALLED_STATION_ID = 128, + ERP_TLV_CALLING_STATION_ID = 129, + ERP_TLV_NAS_IDENTIFIER = 130, + ERP_TLV_NAS_IP_ADDRESS = 131, + ERP_TLV_NAS_IPV6_ADDRESS = 132, +}; + +enum eap_erp_cryptosuite { + ERP_CRYPTOSUITE_SHA256_64 = 1, + ERP_CRYPTOSUITE_SHA256_128 = 2, + ERP_CRYPTOSUITE_SHA256_256 = 3, +}; + +struct erp_tlv_iter { + unsigned int max; + unsigned int pos; + const unsigned char *tlv; + unsigned int tag; + unsigned int len; + const unsigned char *data; +}; + +static struct l_queue *key_cache; + +static void erp_tlv_iter_init(struct erp_tlv_iter *iter, + const unsigned char *tlv, unsigned int len) +{ + iter->tlv = tlv; + iter->max = len; + iter->pos = 0; +} + +static bool erp_tlv_iter_next(struct erp_tlv_iter *iter) +{ + const unsigned char *tlv = iter->tlv + iter->pos; + const unsigned char *end = iter->tlv + iter->max; + unsigned int tag; + unsigned int len; + + if (iter->pos + 2 >= iter->max) + return false; + + tag = *tlv++; + + /* + * These two tags are not actually TLVs (they are just type-value). Both + * are 32-bit integers. + */ + if (tag != ERP_TV_RMSK_LIFETIME && tag != ERP_TV_RRK_LIFETIME) + len = *tlv++; + else + len = 4; + + if (tlv + len > end) + return false; + + iter->tag = tag; + iter->len = len; + iter->data = tlv; + + iter->pos = tlv + len - iter->tlv; + + return true; +} + +static void erp_cache_entry_destroy(void *data) +{ + struct erp_cache_entry *entry = data; + + if (entry->ref) + l_error("ERP entry still has a reference on cleanup!"); + + l_free(entry->id); + l_free(entry->emsk); + l_free(entry->session_id); + l_free(entry->ssid); + + l_free(entry); +} + +void erp_cache_add(const char *id, const void *session_id, + size_t session_len, const void *emsk, size_t emsk_len, + const char *ssid) +{ + struct erp_cache_entry *entry; + + if (!unlikely(id || session_id || emsk)) + return; + + entry = l_new(struct erp_cache_entry, 1); + + entry->id = l_strdup(id); + entry->emsk = l_memdup(emsk, emsk_len); + entry->emsk_len = emsk_len; + entry->session_id = l_memdup(session_id, session_len); + entry->session_len = session_len; + entry->ssid = l_strdup(ssid); + entry->expire_time = l_time_offset(l_time_now(), + ERP_DEFAULT_KEY_LIFETIME_US); + + l_queue_push_head(key_cache, entry); +} + +static struct erp_cache_entry *find_keycache(const char *id, const char *ssid) +{ + const struct l_queue_entry *entry; + + if (!id && !ssid) + return NULL; + + for (entry = l_queue_get_entries(key_cache); entry; + entry = entry->next) { + struct erp_cache_entry *cache = entry->data; + + if (cache->invalid) + continue; + + if (l_time_after(l_time_now(), cache->expire_time)) { + if (!cache->ref) { + l_queue_remove(key_cache, cache); + erp_cache_entry_destroy(cache); + } else + cache->invalid = true; + + continue; + } + + if (id && !strcmp(cache->id, id)) + return cache; + + if (ssid && !strcmp(cache->ssid, ssid)) + return cache; + } + + return NULL; +} + +void erp_cache_remove(const char *id) +{ + struct erp_cache_entry *entry = find_keycache(id, NULL); + + if (!entry) + return; + + if (entry->ref) { + entry->invalid = true; + return; + } + + l_queue_remove(key_cache, entry); + + erp_cache_entry_destroy(entry); +} + +struct erp_cache_entry *erp_cache_get(const char *ssid) +{ + struct erp_cache_entry *cache = find_keycache(NULL, ssid); + + if (!cache) + return NULL; + + cache->ref++; + + return cache; +} + +void erp_cache_put(struct erp_cache_entry *cache) +{ + cache->ref--; + + if (cache->ref) + return; + + if (!cache->invalid) + return; + + /* + * Cache entry marked as invalid, either it expired or something + * attempted to remove it. Either way, it can now be removed. + */ + l_queue_remove(key_cache, cache); + erp_cache_entry_destroy(cache); +} + +const char *erp_cache_entry_get_identity(struct erp_cache_entry *cache) +{ + return cache->id; +} + +#define ERP_RRK_LABEL "EAP Re-authentication Root Key@ietf.org" +#define ERP_RIK_LABEL "Re-authentication Integrity Key@ietf.org" +#define ERP_RMSK_LABEL "Re-authentication Master Session Key@ietf.org" + +/* + * RFC 5295 - Section 3.2. EMSK and USRK Name Derivation + */ +static bool erp_derive_emsk_name(const uint8_t *session_id, size_t session_len, + char buf[static 17]) +{ + uint8_t hex[8]; + char info[7] = { 'E', 'M', 'S', 'K', '\0', 0x0, 0x8}; + char *ascii; + + if (!hkdf_expand(L_CHECKSUM_SHA256, session_id, session_len, info, + sizeof(info), hex, 8)) + return false; + + ascii = l_util_hexstring(hex, 8); + + strcpy(buf, ascii); + + l_free(ascii); + + return true; +} + +/* + * RFC 6696 - Section 4.1 and 4.3 - rRK and rIK derivation + * + * All reauth keys form a hiearchy, and all ultimately are derived from the + * EMSK. All keys follow the rule: + * + * "The length of the MUST be equal to the length of the parent key used + * to derive it." + * + * Therefore all keys derived are equal to the EMSK length. + */ +static bool erp_derive_reauth_keys(const uint8_t *emsk, size_t emsk_len, + void *r_rk, void *r_ik) +{ + char info[256]; + char *ptr;; + + ptr = info + l_strlcpy(info, ERP_RRK_LABEL, sizeof(info)) + 1; + + l_put_be16(emsk_len, ptr); + ptr += 2; + + if (!hkdf_expand(L_CHECKSUM_SHA256, emsk, emsk_len, (const char *)info, + ptr - info, r_rk, emsk_len)) + return false; + + ptr = info + l_strlcpy(info, ERP_RIK_LABEL, sizeof(info)) + 1; + + *ptr++ = ERP_CRYPTOSUITE_SHA256_128; + l_put_be16(emsk_len, ptr); + ptr += 2; + + if (!hkdf_expand(L_CHECKSUM_SHA256, r_rk, emsk_len, (const char *) info, + ptr - info, r_ik, emsk_len)) + return false; + + return true; +} + +struct erp_state *erp_new(struct erp_cache_entry *cache, + erp_tx_packet_func_t tx_packet, + erp_complete_func_t complete, void *user_data) +{ + struct erp_state *erp; + + if (!cache) + return NULL; + + erp = l_new(struct erp_state, 1); + + erp->tx_packet = tx_packet; + erp->complete = complete; + erp->user_data = user_data; + erp->cache = cache; + + return erp; +} + +void erp_free(struct erp_state *erp) +{ + erp_cache_put(erp->cache); + + l_free(erp); +} + +bool erp_start(struct erp_state *erp) +{ + uint8_t buf[512]; + uint8_t *ptr = buf; + char emsk_name[17]; + size_t nai_len; + + if (!erp_derive_emsk_name(erp->cache->session_id, + erp->cache->session_len, emsk_name)) + return false; + + if (!erp_derive_reauth_keys(erp->cache->emsk, erp->cache->emsk_len, + erp->r_rk, erp->r_ik)) + return false; + + nai_len = sprintf(erp->keyname_nai, "%s@%s", emsk_name, + util_get_domain(erp->cache->id)); + + *ptr++ = EAP_CODE_INITIATE; + *ptr++ = 0; + /* Header (8) + TL (2) + NAI (nai_len) + CS (1) + auth tag (16) */ + l_put_be16(27 + nai_len, ptr); + ptr += 2; + *ptr++ = ERP_TYPE_REAUTH; + *ptr++ = 0; + l_put_be16(erp->seq, ptr); + ptr += 2; + + /* keyName-NAI TLV */ + *ptr++ = ERP_TLV_KEYNAME_NAI; + *ptr++ = nai_len; + memcpy(ptr, erp->keyname_nai, nai_len); + ptr += nai_len; + + *ptr++ = ERP_CRYPTOSUITE_SHA256_128; + + hmac_sha256(erp->r_ik, erp->cache->emsk_len, buf, ptr - buf, ptr, 16); + ptr += 16; + + erp->tx_packet(buf, ptr - buf, erp->user_data); + + return true; +} + +void erp_rx_packet(struct erp_state *erp, const uint8_t *pkt, size_t len) +{ + struct erp_tlv_iter iter; + enum eap_erp_cryptosuite cs; + uint8_t hash[16]; + uint8_t rmsk[64]; + char info[256]; + char *ptr = info; + const uint8_t *nai = NULL; + uint8_t type; + uint16_t seq; + bool r; + + /* + * Not including the TLVs we have: + * header (8) + cryptosuite (1) + auth tag (16) = 25 bytes + */ + if (len < 25) + goto eap_failed; + + /* + * We can skip code/id/len, since that was already parsed. We just need + * the whole packet so we can verify the Auth tag. + */ + type = pkt[4]; + + if (type != ERP_TYPE_REAUTH) + return; + + r = util_is_bit_set(pkt[5], 0); + if (r) + goto eap_failed; + + /* + * TODO: Parse B and L bits. L bit indicates rRK lifetime, but our ERP + * cache does not yet support this. + */ + + seq = l_get_be16(pkt + 6); + + if (seq != erp->seq) + goto eap_failed; + + /* + * The Cryptosuite byte comes after the TLVs. Because of this we cannot + * parse the TLVs yet since we don't actually know where they end. There + * is really no good way to do this, but (at least for now) we can just + * require the 128 bit cryptosuite. If we limit to only this suite we + * can work backwards from the end (17 bytes) to get the cryptosuite. If + * it is not the 128 bit suite we just fail. If it is, we now know where + * the TLVs end; + */ + cs = *(pkt + len - 17); + + if (cs != ERP_CRYPTOSUITE_SHA256_128) + goto eap_failed; + + hmac_sha256(erp->r_ik, erp->cache->emsk_len, pkt, len - 16, hash, 16); + + if (memcmp(hash, pkt + len - 16, 16) != 0) { + l_debug("Authentication Tag did not verify"); + goto eap_failed; + } + + erp_tlv_iter_init(&iter, pkt + 8, len - 8 - 17); + + while (erp_tlv_iter_next(&iter)) { + switch (iter.tag) { + case ERP_TLV_KEYNAME_NAI: + if (nai) + goto eap_failed; + + nai = iter.data; + break; + default: + break; + } + } + + /* + * RFC 6696 Section 5.3.3 + * + * Exactly one instance of the keyName-NAI attribute SHALL be present + * in an EAP-Finish/Re-auth message + */ + if (!nai) { + l_error("AP did not include keyName-NAI in EAP-Finish"); + goto eap_failed; + } + + if (memcmp(nai, erp->keyname_nai, strlen(erp->keyname_nai))) { + l_error("keyName-NAI did not match"); + goto eap_failed; + } + + /* + * RFC 6696 Section 4.6 - rMSK Derivation + */ + strcpy(ptr, ERP_RMSK_LABEL); + ptr += strlen(ERP_RMSK_LABEL); + *ptr++ = '\0'; + l_put_be16(erp->seq, ptr); + ptr += 2; + l_put_be16(64, ptr); + ptr += 2; + + hkdf_expand(L_CHECKSUM_SHA256, erp->r_rk, erp->cache->emsk_len, + info, ptr - info, rmsk, erp->cache->emsk_len); + + erp->complete(ERP_RESULT_SUCCESS, rmsk, erp->cache->emsk_len, + erp->user_data); + + return; + +eap_failed: + erp->complete(ERP_RESULT_FAIL, NULL, 0, erp->user_data); +} + +void erp_init(void) +{ + key_cache = l_queue_new(); +} + +void erp_exit(void) +{ + l_queue_destroy(key_cache, erp_cache_entry_destroy); +} diff --git a/src/erpcache.h b/src/erp.h similarity index 50% rename from src/erpcache.h rename to src/erp.h index 0b05fbea..ebf6bc1c 100644 --- a/src/erpcache.h +++ b/src/erp.h @@ -20,19 +20,37 @@ * */ -void erp_add_key(const char *id, const void *session_id, size_t session_len, +struct erp_state; +struct erp_cache_entry; + +enum erp_result { + ERP_RESULT_SUCCESS, + ERP_RESULT_FAIL, +}; + +typedef void (*erp_tx_packet_func_t)(const uint8_t *erp_data, size_t len, + void *user_data); +typedef void (*erp_complete_func_t)(enum erp_result result, const void *rmsk, + size_t rmsk_len, void *user_data); + +struct erp_state *erp_new(struct erp_cache_entry *cache, + erp_tx_packet_func_t tx_packet, + erp_complete_func_t complete, void *user_data); +void erp_free(struct erp_state *erp); + +bool erp_start(struct erp_state *erp); +void erp_rx_packet(struct erp_state *erp, const uint8_t *erp_data, size_t len); + +void erp_cache_add(const char *id, const void *session_id, size_t session_len, const void *emsk, size_t emsk_len, - const char *ssid, const char *erp_domain); + const char *ssid); -void erp_remove_key(const char *id); +void erp_cache_remove(const char *id); -bool erp_find_key_by_identity(const char *id, void *session, - size_t *session_len, void *emsk, size_t *emsk_len, - const char **erp_domain); +struct erp_cache_entry *erp_cache_get(const char *ssid); +void erp_cache_put(struct erp_cache_entry *cache); -bool erp_has_key_for_ssid(const char *ssid); - -bool erp_has_key_for_identity(const char *id); +const char *erp_cache_entry_get_identity(struct erp_cache_entry *cache); void erp_init(void); void erp_exit(void); diff --git a/src/erpcache.c b/src/erpcache.c deleted file mode 100644 index 73aa8c5e..00000000 --- a/src/erpcache.c +++ /dev/null @@ -1,166 +0,0 @@ -/* - * - * 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 "erpcache.h" - -#define ERP_DEFAULT_KEY_LIFETIME 86400000000 - -struct erp_cache_entry { - char *id; - void *emsk; - size_t emsk_len; - void *session_id; - size_t session_len; - char *erp_domain; - char *ssid; - uint64_t expire_time; -}; - -static struct l_queue *key_cache; - -static void destroy_entry(void *data) -{ - struct erp_cache_entry *entry = data; - - l_free(entry->id); - l_free(entry->emsk); - l_free(entry->session_id); - l_free(entry->ssid); - - if (entry->erp_domain) - l_free(entry->erp_domain); - - l_free(entry); -} - -void erp_add_key(const char *id, const void *session_id, - size_t session_len, const void *emsk, size_t emsk_len, - const char *ssid, const char *erp_domain) -{ - struct erp_cache_entry *entry; - - if (!unlikely(id || session_id || emsk)) - return; - - entry = l_new(struct erp_cache_entry, 1); - - entry->id = l_strdup(id); - entry->emsk = l_memdup(emsk, emsk_len); - entry->emsk_len = emsk_len; - entry->session_id = l_memdup(session_id, session_len); - entry->session_len = session_len; - entry->ssid = l_strdup(ssid); - entry->expire_time = l_time_offset(l_time_now(), - ERP_DEFAULT_KEY_LIFETIME); - - if (erp_domain) - entry->erp_domain = l_strdup(erp_domain); - - l_queue_push_head(key_cache, entry); -} - -static struct erp_cache_entry *find_keycache(const char *id, const char *ssid) -{ - const struct l_queue_entry *entry; - - for (entry = l_queue_get_entries(key_cache); entry; - entry = entry->next) { - struct erp_cache_entry *cache = entry->data; - - if (l_time_after(l_time_now(), cache->expire_time)) { - l_queue_remove(key_cache, cache); - destroy_entry(cache); - continue; - } - - if (id) { - if (strcmp(cache->id, id)) - continue; - } else if (ssid) { - if (strcmp(cache->ssid, ssid)) - continue; - } else - return NULL; - - return cache; - } - - return NULL; -} - -void erp_remove_key(const char *id) -{ - struct erp_cache_entry *entry = find_keycache(id, NULL); - - if (!entry) - return; - - l_queue_remove(key_cache, entry); - - destroy_entry(entry); -} - -bool erp_find_key_by_identity(const char *id, void *session, - size_t *session_len, void *emsk, size_t *emsk_len, - const char **erp_domain) -{ - struct erp_cache_entry *cache = find_keycache(id, NULL); - - if (!cache) - return false; - - memcpy(emsk, cache->emsk, cache->emsk_len); - *emsk_len = cache->emsk_len; - memcpy(session, cache->session_id, cache->session_len); - *session_len = cache->session_len; - *erp_domain = cache->erp_domain; - - return true; -} - -bool erp_has_key_for_ssid(const char *ssid) -{ - return find_keycache(NULL, ssid) != NULL; -} - -bool erp_has_key_for_identity(const char *id) -{ - return find_keycache(id, NULL) != NULL; -} - -void erp_init(void) -{ - key_cache = l_queue_new(); -} - -void erp_exit(void) -{ - l_queue_destroy(key_cache, destroy_entry); -}