mirror of
https://git.kernel.org/pub/scm/network/wireless/iwd.git
synced 2024-11-23 07:29:28 +01:00
542 lines
11 KiB
C
542 lines
11 KiB
C
/*
|
|
*
|
|
* 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 <config.h>
|
|
#endif
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
|
|
#include <ell/ell.h>
|
|
|
|
#include "src/missing.h"
|
|
#include "src/iwd.h"
|
|
#include "src/eap-private.h"
|
|
#include "src/erp.h"
|
|
#include "src/crypto.h"
|
|
#include "src/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;
|
|
void *user_data;
|
|
|
|
struct erp_cache_entry *cache;
|
|
|
|
uint8_t rmsk[64];
|
|
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 <key> 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,
|
|
void *user_data)
|
|
{
|
|
struct erp_state *erp;
|
|
|
|
if (!cache)
|
|
return NULL;
|
|
|
|
erp = l_new(struct erp_state, 1);
|
|
|
|
erp->tx_packet = tx_packet;
|
|
erp->user_data = user_data;
|
|
erp->cache = cache;
|
|
|
|
return erp;
|
|
}
|
|
|
|
void erp_free(struct erp_state *erp)
|
|
{
|
|
erp_cache_put(erp->cache);
|
|
|
|
explicit_bzero(erp->rmsk, sizeof(erp->rmsk));
|
|
explicit_bzero(erp->r_ik, sizeof(erp->r_ik));
|
|
explicit_bzero(erp->r_rk, sizeof(erp->r_rk));
|
|
|
|
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;
|
|
}
|
|
|
|
int 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];
|
|
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)
|
|
goto eap_failed;
|
|
|
|
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, erp->rmsk, erp->cache->emsk_len);
|
|
|
|
return 0;
|
|
|
|
eap_failed:
|
|
return -EINVAL;
|
|
}
|
|
|
|
const void *erp_get_rmsk(struct erp_state *erp, size_t *rmsk_len)
|
|
{
|
|
if (rmsk_len)
|
|
*rmsk_len = erp->cache->emsk_len;
|
|
|
|
return erp->rmsk;
|
|
}
|
|
|
|
static int erp_init(void)
|
|
{
|
|
key_cache = l_queue_new();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void erp_exit(void)
|
|
{
|
|
l_queue_destroy(key_cache, erp_cache_entry_destroy);
|
|
}
|
|
|
|
IWD_MODULE(erp, erp_init, erp_exit)
|