diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 10:41:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 10:41:52 +0000 |
commit | de8bf9112695763664912e340b265fa898188460 (patch) | |
tree | 9bcd5f8d45fc3b81174d3de8abfd573b68e9d7f6 /src/modules/rlm_dpsk | |
parent | Adding debian version 3.2.3+dfsg-2. (diff) | |
download | freeradius-de8bf9112695763664912e340b265fa898188460.tar.xz freeradius-de8bf9112695763664912e340b265fa898188460.zip |
Merging upstream version 3.2.5+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/modules/rlm_dpsk')
-rw-r--r-- | src/modules/rlm_dpsk/all.mk | 10 | ||||
-rw-r--r-- | src/modules/rlm_dpsk/rlm_dpsk.c | 940 |
2 files changed, 950 insertions, 0 deletions
diff --git a/src/modules/rlm_dpsk/all.mk b/src/modules/rlm_dpsk/all.mk new file mode 100644 index 0000000..8da2475 --- /dev/null +++ b/src/modules/rlm_dpsk/all.mk @@ -0,0 +1,10 @@ +TARGETNAME := rlm_dpsk + +ifneq "$(OPENSSL_LIBS)" "" +TARGET := $(TARGETNAME).a +endif + +SOURCES := $(TARGETNAME).c + +SRC_CFLAGS := +TGT_LDLIBS := diff --git a/src/modules/rlm_dpsk/rlm_dpsk.c b/src/modules/rlm_dpsk/rlm_dpsk.c new file mode 100644 index 0000000..6ca43ee --- /dev/null +++ b/src/modules/rlm_dpsk/rlm_dpsk.c @@ -0,0 +1,940 @@ +/* + * Copyright (C) 2023 Network RADIUS SARL (legal@networkradius.com) + * + * This software may not be redistributed in any form without the prior + * written consent of Network RADIUS. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/** + * $Id$ + * @file rlm_dpsk.c + * @brief Dynamic PSK for WiFi + * + * @copyright 2023 Network RADIUS SAS (legal@networkradius.com) + */ +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/dlist.h> +#include <freeradius-devel/rad_assert.h> + +#include <openssl/ssl.h> +#include <openssl/evp.h> +#include <openssl/hmac.h> + +#include <ctype.h> + +#define PW_FREERADIUS_8021X_ANONCE (1) +#define PW_FREERADIUS_8021X_EAPOL_KEY_MSG (2) + +#define VENDORPEC_FREERADIUS_EVS5 ((((uint32_t) 245) << 24) | VENDORPEC_FREERADIUS) + +#define VENDORPEC_RUCKUS (25053) +#define PW_RUCKUS_BSSID (14) +#define PW_RUCKUS_DPSK_PARAMS (152) + +//#define PW_RUCKUS_DPSK_CIPHER (PW_RUCKUS_DPSK_PARAMS | (2 << 8)) +#define PW_RUCKUS_DPSK_ANONCE (PW_RUCKUS_DPSK_PARAMS | (3 << 8)) +#define PW_RUCKUS_DPSK_EAPOL_KEY_FRAME (PW_RUCKUS_DPSK_PARAMS | (4 << 8)) + + +/* + Header: 02030075 + + descriptor 02 + information 010a + length 0010 + replay counter 000000000000001 + snonce c3bb319516614aacfb44e933bf1671131fb1856e5b2721952d414ce3f5aa312b + IV 0000000000000000000000000000000 + rsc 0000000000000000 + reserved 0000000000000000 + mic 35cddcedad0dfb6a12a2eca55c17c323 + data length 0016 + data 30140100000fac040100000fac040100000fac028c00 + + 30 + 14 length of data + 01 ... +*/ + +typedef struct eapol_key_frame_t { + uint8_t descriptor; // message number 2 + uint16_t information; // + uint16_t length; // always 0010, for 16 octers + uint8_t replay_counter[8]; // usually "1" + uint8_t nonce[32]; // random token + uint8_t iv[16]; // zeroes + uint8_t rsc[8]; // zeros + uint8_t reserved[8]; // zeroes + uint8_t mic[16]; // calculated data + uint16_t data_len; // various other things we don't need. +// uint8_t data[]; +} CC_HINT(__packed__) eapol_key_frame_t; + +typedef struct eapol_attr_t { + uint8_t header[4]; // 02030075 + eapol_key_frame_t frame; +} CC_HINT(__packed__) eapol_attr_t; + +#ifdef HAVE_PTHREAD_H +#define PTHREAD_MUTEX_LOCK pthread_mutex_lock +#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock +#else +#define PTHREAD_MUTEX_LOCK(_x) +#define PTHREAD_MUTEX_UNLOCK(_x) +#endif + +typedef struct rlm_dpsk_s rlm_dpsk_t; + +typedef struct { + uint8_t mac[6]; + uint8_t pmk[32]; + + uint8_t *ssid; + size_t ssid_len; + + char *identity; + size_t identity_len; + + uint8_t *psk; + size_t psk_len; + time_t expires; + + fr_dlist_t dlist; + rlm_dpsk_t *inst; +} rlm_dpsk_cache_t; + +struct rlm_dpsk_s { + char const *xlat_name; + bool ruckus; + + rbtree_t *cache; + + uint32_t cache_size; + uint32_t cache_lifetime; + + char const *filename; + +#ifdef HAVE_PTHREAD_H + pthread_mutex_t mutex; +#endif + fr_dlist_t head; + + DICT_ATTR const *ssid; + DICT_ATTR const *anonce; + DICT_ATTR const *frame; +}; + +static const CONF_PARSER module_config[] = { + { "ruckus", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_dpsk_t, ruckus), "no" }, + + { "cache_size", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_dpsk_t, cache_size), "0" }, + { "cache_lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_dpsk_t, cache_lifetime), "0" }, + + { "filename", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_dpsk_t, filename), NULL }, + + CONF_PARSER_TERMINATOR +}; + + +static inline CC_HINT(nonnull) rlm_dpsk_cache_t *fr_dlist_head(fr_dlist_t const *head) +{ + if (head->prev == head) return NULL; + + return (rlm_dpsk_cache_t *) (((uintptr_t) head->next) - offsetof(rlm_dpsk_cache_t, dlist)); +} + +static void rdebug_hex(REQUEST *request, char const *prefix, uint8_t const *data, int len) +{ + int i; + char buffer[2048]; /* large enough for largest len */ + + /* + * Leave a trailing space, we don't really care about that. + */ + for (i = 0; i < len; i++) { + snprintf(buffer + i * 2, sizeof(buffer) - i * 2, "%02x", data[i]); + } + + RDEBUG("%s %s", prefix, buffer); +} +#define RDEBUG_HEX if (rad_debug_lvl >= 3) rdebug_hex + +#if 0 +/* + * Find the Ruckus attributes, and convert to FreeRADIUS ones. + * + * Also check the WPA2 cipher. We need AES + HMAC-SHA1. + */ +static bool normalize(rlm_dpsk_t *inst, REQUEST *request) +{ + VALUE_PAIR *bssid, *cipher, *anonce, *key_msg, *vp; + + if (!inst->ruckus) return false; + + bssid = fr_pair_find_by_num(request->packet->vps, PW_RUCKUS_BSSID, VENDORPEC_RUCKUS, TAG_ANY); + if (!bssid) return false; + + cipher = fr_pair_find_by_num(request->packet->vps, PW_RUCKUS_DPSK_CIPHER, VENDORPEC_RUCKUS, TAG_ANY); + if (!cipher) return false; + + if (cipher->vp_byte != 4) { + RDEBUG("Found Ruckus-DPSK-Cipher != 4, which means that we cannot do DPSK"); + return false; + } + + anonce = fr_pair_find_by_num(request->packet->vps, PW_RUCKUS_DPSK_ANONCE, VENDORPEC_RUCKUS, TAG_ANY); + if (!anonce) return false; + + key_msg = fr_pair_find_by_num(request->packet->vps, PW_RUCKUS_DPSK_EAPOL_KEY_FRAME, VENDORPEC_RUCKUS, TAG_ANY); + if (!key_msg) return false; + + MEM(vp = fr_pair_afrom_da(request->packet, anonce->da)); + fr_pair_value_memcpy(vp, anonce->vp_octets, anonce->vp_length); + fr_pair_add(&request->packet->vps, vp); + + MEM(vp = fr_pair_afrom_da(request->packet, key_msg->da)); + fr_pair_value_memcpy(vp, key_msg->vp_octets, key_msg->vp_length); + fr_pair_add(&request->packet->vps, vp); + + return false; +} +#endif + +/* + * mod_authorize() - authorize user if we can authenticate + * it later. Add Auth-Type attribute if present in module + * configuration (usually Auth-Type must be "DPSK") + */ +static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void * instance, REQUEST *request) +{ + rlm_dpsk_t *inst = instance; + + if (!fr_pair_find_by_da(request->packet->vps, inst->anonce, TAG_ANY) && + !fr_pair_find_by_da(request->packet->vps, inst->frame, TAG_ANY)) { + return RLM_MODULE_NOOP; + } + + if (fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY)) { + RWDEBUG2("Auth-Type already set. Not setting to %s", inst->xlat_name); + return RLM_MODULE_NOOP; + } + + RDEBUG2("Found %s. Setting 'Auth-Type = %s'", inst->frame->name, inst->xlat_name); + + /* + * Set Auth-Type to MS-CHAP. The authentication code + * will take care of turning cleartext passwords into + * NT/LM passwords. + */ + if (!pair_make_config("Auth-Type", inst->xlat_name, T_OP_EQ)) { + return RLM_MODULE_FAIL; + } + + return RLM_MODULE_OK; +} + +static rlm_dpsk_cache_t *dpsk_cache_find(REQUEST *request, rlm_dpsk_t const *inst, uint8_t *buffer, size_t buflen, VALUE_PAIR *ssid, uint8_t const *mac) +{ + rlm_dpsk_cache_t *entry, my_entry; + + memcpy(my_entry.mac, mac, sizeof(my_entry.mac)); + memcpy(&my_entry.ssid, &ssid->vp_octets, sizeof(my_entry.ssid)); /* const issues */ + my_entry.ssid_len = ssid->vp_length; + + entry = rbtree_finddata(inst->cache, &my_entry); + if (entry) { + if (entry->expires > request->timestamp) { + RDEBUG3("Cache entry found"); + memcpy(buffer, entry->pmk, buflen); + return entry; + } + + RDEBUG3("Cache entry has expired"); + rbtree_deletebydata(inst->cache, entry); + } + + return NULL; +} + + +static int generate_pmk(REQUEST *request, rlm_dpsk_t const *inst, uint8_t *buffer, size_t buflen, VALUE_PAIR *ssid, uint8_t const *mac, char const *psk, size_t psk_len) +{ + VALUE_PAIR *vp; + + fr_assert(buflen == 32); + + if (!ssid) { + ssid = fr_pair_find_by_da(request->packet->vps, inst->ssid, TAG_ANY); + if (!ssid) { + RDEBUG("No %s in the request", inst->ssid->name); + return 0; + } + } + + /* + * No provided PSK. Try to look it up in the cache. If + * it isn't there, find it in the config items. + */ + if (!psk) { + if (inst->cache && mac) { + rlm_dpsk_cache_t *entry; + + entry = dpsk_cache_find(request, inst, buffer, buflen, ssid, mac); + if (entry) { + memcpy(buffer, entry->pmk, buflen); + return 1; + } + RDEBUG3("Cache entry not found"); + } /* else no caching */ + + vp = fr_pair_find_by_num(request->config, PW_PRE_SHARED_KEY, 0, TAG_ANY); + if (!vp) { + RDEBUG("No &config:Pre-Shared-Key"); + return 0; + } + + psk = vp->vp_strvalue; + psk_len = vp->vp_length; + } + + if (PKCS5_PBKDF2_HMAC_SHA1((const char *) psk, psk_len, (const unsigned char *) ssid->vp_strvalue, ssid->vp_length, 4096, buflen, buffer) == 0) { + RDEBUG("Failed calling OpenSSL to calculate the PMK"); + return 0; + } + + return 1; +} + +/* + * Verify the DPSK information. + */ +static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request) +{ + rlm_dpsk_t *inst = instance; + VALUE_PAIR *anonce, *key_msg, *ssid, *vp; + rlm_dpsk_cache_t *entry; + int lineno = 0; + size_t len, psk_len; + unsigned int digest_len, mic_len; + eapol_attr_t const *eapol; + eapol_attr_t *zeroed; + FILE *fp = NULL; + char const *psk_identity = NULL, *psk; + uint8_t *p; + uint8_t const *snonce, *ap_mac; + uint8_t const *min_mac, *max_mac; + uint8_t const *min_nonce, *max_nonce; + uint8_t pmk[32]; + uint8_t s_mac[6], message[sizeof("Pairwise key expansion") + 6 + 6 + 32 + 32 + 1], frame[128]; + uint8_t digest[EVP_MAX_MD_SIZE], mic[EVP_MAX_MD_SIZE]; + char token_identity[256]; + + /* + * Search for the information in a bunch of attributes. + */ + anonce = fr_pair_find_by_da(request->packet->vps, inst->anonce, TAG_ANY); + if (!anonce) { + RDEBUG("No FreeRADIUS-802.1X-Anonce in the request"); + return RLM_MODULE_NOOP; + } + + if (anonce->vp_length != 32) { + RDEBUG("%s has incorrect length (%zu, not 32)", inst->anonce->name, anonce->vp_length); + return RLM_MODULE_NOOP; + } + + key_msg = fr_pair_find_by_da(request->packet->vps, inst->frame, TAG_ANY); + if (!key_msg) { + RDEBUG("No %s in the request", inst->frame->name); + return RLM_MODULE_NOOP; + } + + if (key_msg->vp_length < sizeof(*eapol)) { + RDEBUG("%s has incorrect length (%zu < %zu)", inst->frame->name, key_msg->vp_length, sizeof(*eapol)); + return RLM_MODULE_NOOP; + } + + if (key_msg->vp_length > sizeof(frame)) { + RDEBUG("%s has incorrect length (%zu > %zu)", inst->frame->name, key_msg->vp_length, sizeof(frame)); + return RLM_MODULE_NOOP; + } + + ssid = fr_pair_find_by_da(request->packet->vps, inst->ssid, TAG_ANY); + if (!ssid) { + RDEBUG("No %s in the request", inst->ssid->name); + return 0; + } + + /* + * Get supplicant MAC address. + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + if (!vp) { + RDEBUG("No &User-Name"); + return RLM_MODULE_NOOP; + } + + len = fr_hex2bin(s_mac, sizeof(s_mac), vp->vp_strvalue, vp->vp_length); + if (len != 6) { + RDEBUG("&User-Name is not a recognizable hex MAC address"); + return RLM_MODULE_NOOP; + } + + /* + * In case we're not reading from a file. + */ + vp = fr_pair_find_by_num(request->config, PW_PSK_IDENTITY, 0, TAG_ANY); + if (vp) psk_identity = vp->vp_strvalue; + + vp = fr_pair_find_by_num(request->config, PW_PRE_SHARED_KEY, 0, TAG_ANY); + if (vp) { + psk = vp->vp_strvalue; + psk_len = vp->vp_length; + } else { + psk = NULL; + psk_len = 0; + } + + /* + * Get the AP MAC address. + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_CALLED_STATION_MAC, 0, TAG_ANY); + if (!vp) { + RDEBUG("No &Called-Station-MAC"); + return RLM_MODULE_NOOP; + } + + if (vp->length != 6) { + RDEBUG("&Called-Station-MAC is not a recognizable MAC address"); + return RLM_MODULE_NOOP; + } + + ap_mac = vp->vp_octets; + + /* + * Sort the MACs + */ + if (memcmp(s_mac, ap_mac, 6) <= 0) { + min_mac = s_mac; + max_mac = ap_mac; + } else { + min_mac = ap_mac; + max_mac = s_mac; + } + + eapol = (eapol_attr_t const *) key_msg->vp_octets; + + /* + * Get supplicant nonce and AP nonce. + * + * Then sort the nonces. + */ + snonce = key_msg->vp_octets + 17; + if (memcmp(snonce, anonce->vp_octets, 32) <= 0) { + min_nonce = snonce; + max_nonce = anonce->vp_octets; + } else { + min_nonce = anonce->vp_octets; + max_nonce = snonce; + } + + /* + * Create the base message which we will hash. + */ + memcpy(message, "Pairwise key expansion", sizeof("Pairwise key expansion")); /* including trailing NUL */ + p = &message[sizeof("Pairwise key expansion")]; + + memcpy(p, min_mac, 6); + memcpy(p + 6, max_mac, 6); + p += 12; + + memcpy(p, min_nonce, 32); + memcpy(p + 32, max_nonce, 32); + p += 64; + *p = '\0'; + fr_assert(sizeof(message) == (p + 1 - message)); + + if (inst->filename && !psk) { + FR_TOKEN token; + char const *q; + char token_psk[256]; + char token_mac[256]; + char buffer[1024]; + + /* + * If there's a cached entry, we don't read the file. + */ + entry = dpsk_cache_find(request, inst, pmk, sizeof(pmk), ssid, s_mac); + if (entry) { + psk_identity = entry->identity; + goto make_digest; + } + + RDEBUG3("Looking for PSK in file %s", inst->filename); + + fp = fopen(inst->filename, "r"); + if (!fp) { + REDEBUG("Failed opening %s - %s", inst->filename, fr_syserror(errno)); + return RLM_MODULE_FAIL; + } + +get_next_psk: + q = fgets(buffer, sizeof(buffer), fp); + if (!q) { + RDEBUG("Failed to find matching key in %s", inst->filename); + fail: + fclose(fp); + return RLM_MODULE_FAIL; + } + + /* + * Split the line on commas, paying attention to double quotes. + */ + token = getstring(&q, token_identity, sizeof(token_identity), true); + if (token == T_INVALID) { + RDEBUG("%s[%d] Failed parsing identity", inst->filename, lineno); + goto fail; + } + + if (*q != ',') { + RDEBUG("%s[%d] Failed to find ',' after identity", inst->filename, lineno); + goto fail; + } + q++; + + token = getstring(&q, token_psk, sizeof(token_psk), true); + if (token == T_INVALID) { + RDEBUG("%s[%d] Failed parsing PSK", inst->filename, lineno); + goto fail; + } + + if (*q == ',') { + q++; + + token = getstring(&q, token_mac, sizeof(token_mac), true); + if (token == T_INVALID) { + RDEBUG("%s[%d] Failed parsing MAC", inst->filename, lineno); + goto fail; + } + + /* + * See if the MAC matches. If not, skip + * this entry. That's a basic negative cache. + */ + if ((strlen(token_mac) != 12) || + (fr_hex2bin((uint8_t *) token_mac, 6, token_mac, 12) != 12)) { + RDEBUG("%s[%d] Failed parsing MAC", inst->filename, lineno); + goto fail; + } + + if (memcmp(s_mac, token_mac, 6) != 0) { + psk_identity = NULL; + goto get_next_psk; + } + + /* + * Close the file so that we don't check any other entries. + */ + MEM(vp = fr_pair_afrom_num(request, PW_PRE_SHARED_KEY, 0)); + fr_pair_value_bstrncpy(vp, token_psk, strlen(token_psk)); + + fr_pair_add(&request->config, vp); + fclose(fp); + fp = NULL; + + RDEBUG3("Found matching MAC"); + } + + /* + * Generate the PMK using the SSID, this MAC, and the PSK we just read. + */ + RDEBUG3("%s[%d] Trying PSK %s", inst->filename, lineno, token_psk); + if (generate_pmk(request, inst, pmk, sizeof(pmk), ssid, s_mac, token_psk, strlen(token_psk)) == 0) { + RDEBUG("No &config:Pairwise-Master-Key or &config:Pre-Shared-Key found"); + return RLM_MODULE_NOOP; + } + + /* + * Remember which identity we had + */ + psk_identity = token_identity; + goto make_digest; + } + + /* + * Use the PMK if it already exists. Otherwise calculate it from the PSK. + */ + vp = fr_pair_find_by_num(request->config, PW_PAIRWISE_MASTER_KEY, 0, TAG_ANY); + if (!vp) { + if (generate_pmk(request, inst, pmk, sizeof(pmk), ssid, s_mac, psk, psk_len) == 0) { + RDEBUG("No &config:Pairwise-Master-Key or &config:Pre-Shared-Key found"); + fr_assert(!fp); + return RLM_MODULE_NOOP; + } + + } else if (vp->vp_length != sizeof(pmk)) { + RDEBUG("Pairwise-Master-Key has incorrect length (%zu != %zu)", vp->vp_length, sizeof(pmk)); + fr_assert(!fp); + return RLM_MODULE_NOOP; + + } else { + memcpy(pmk, vp->vp_octets, sizeof(pmk)); + } + + /* + * HMAC = HMAC_SHA1(pmk, message); + * + * We need the first 16 octets of this. + */ +make_digest: + digest_len = sizeof(digest); + HMAC(EVP_sha1(), pmk, sizeof(pmk), message, sizeof(message), digest, &digest_len); + + RDEBUG_HEX(request, "message:", message, sizeof(message)); + RDEBUG_HEX(request, "pmk :", pmk, sizeof(pmk)); + RDEBUG_HEX(request, "kck :", digest, 16); + + /* + * Create the frame with the middle field zero, and hash it with the KCK digest we calculated from the key expansion. + */ + memcpy(frame, key_msg->vp_octets, key_msg->vp_length); + zeroed = (eapol_attr_t *) &frame[0]; + memset(&zeroed->frame.mic[0], 0, 16); + + RDEBUG_HEX(request, "zeroed:", frame, key_msg->vp_length); + + mic_len = sizeof(mic); + HMAC(EVP_sha1(), digest, 16, frame, key_msg->vp_length, mic, &mic_len); + + /* + * Do the MICs match? + */ + if (memcmp(&eapol->frame.mic[0], mic, 16) != 0) { + if (fp) { + psk_identity = NULL; + goto get_next_psk; + } + + RDEBUG_HEX(request, "calculated mic:", mic, 16); + RDEBUG_HEX(request, "packet mic :", &eapol->frame.mic[0], 16); + return RLM_MODULE_FAIL; + } + + /* + * It matches. Close the input file if necessary. + */ + if (fp) fclose(fp); + + /* + * Extend the lifetime of the cache entry, or add the + * cache entry if necessary. + */ + if (inst->cache) { + rlm_dpsk_cache_t my_entry; + + /* + * Find the entry (again), and update the expiry time. + * + * Create the entry if neessary. + */ + memcpy(my_entry.mac, s_mac, sizeof(my_entry.mac)); + + vp = fr_pair_find_by_da(request->packet->vps, inst->ssid, TAG_ANY); + if (!vp) goto save_psk; /* should never really happen, but just to be safe */ + + memcpy(&my_entry.ssid, &vp->vp_octets, sizeof(my_entry.ssid)); /* const issues */ + my_entry.ssid_len = vp->vp_length; + + entry = rbtree_finddata(inst->cache, &my_entry); + if (!entry) { + /* + * Too many entries in the cache. Delete the oldest one. + */ + if (rbtree_num_elements(inst->cache) > inst->cache_size) { + PTHREAD_MUTEX_LOCK(&inst->mutex); + entry = fr_dlist_head(&inst->head); + PTHREAD_MUTEX_UNLOCK(&inst->mutex); + + rbtree_deletebydata(inst->cache, entry); + } + + MEM(entry = talloc_zero(NULL, rlm_dpsk_cache_t)); + + memcpy(entry->mac, s_mac, sizeof(entry->mac)); + memcpy(entry->pmk, pmk, sizeof(entry->pmk)); + + fr_dlist_entry_init(&entry->dlist); + entry->inst = inst; + + /* + * Save the variable-length SSID. + */ + MEM(entry->ssid = talloc_memdup(entry, vp->vp_octets, vp->vp_length)); + entry->ssid_len = vp->vp_length; + + /* + * Save the PSK. If we just have the + * PMK, then we can still cache that. + */ + vp = fr_pair_find_by_num(request->config, PW_PRE_SHARED_KEY, 0, TAG_ANY); + if (vp) { + MEM(entry->psk = talloc_memdup(entry, vp->vp_octets, vp->vp_length)); + entry->psk_len = vp->vp_length; + } + + /* + * Save the identity. + */ + if (psk_identity) { + MEM(entry->identity = talloc_memdup(entry, psk_identity, strlen(psk_identity))); + entry->identity_len = strlen(psk_identity); + } + + /* + * Cache it. + */ + if (!rbtree_insert(inst->cache, entry)) { + talloc_free(entry); + goto save_found_psk; + } + RDEBUG3("Cache entry saved"); + } + entry->expires = request->timestamp + inst->cache_lifetime; + + PTHREAD_MUTEX_LOCK(&inst->mutex); + fr_dlist_entry_unlink(&entry->dlist); + fr_dlist_insert_tail(&inst->head, &entry->dlist); + PTHREAD_MUTEX_UNLOCK(&inst->mutex); + + /* + * Add the PSK to the reply items, if it was cached. + */ + if (entry->psk) { + MEM(vp = fr_pair_afrom_num(request->reply, PW_PRE_SHARED_KEY, 0)); + fr_pair_value_bstrncpy(vp, entry->psk, entry->psk_len); + + fr_pair_add(&request->reply->vps, vp); + } + + goto save_psk_identity; + } + + /* + * Save a copy of the found PSK in the reply; + */ +save_psk: + vp = fr_pair_find_by_num(request->config, PW_PRE_SHARED_KEY, 0, TAG_ANY); + +save_found_psk: + if (!vp) return RLM_MODULE_OK; + + fr_pair_add(&request->reply->vps, fr_pair_copy(request->reply, vp)); + +save_psk_identity: + /* + * Save which identity matched. + */ + if (psk_identity) { + MEM(vp = fr_pair_afrom_num(request->reply, PW_PSK_IDENTITY, 0)); + fr_pair_value_bstrncpy(vp, psk_identity, strlen(psk_identity)); + + fr_pair_add(&request->reply->vps, vp); + } + + return RLM_MODULE_OK; +} + +/* + * Generate the PMK from SSID and Pre-Shared-Key + */ +static ssize_t dpsk_xlat(void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + rlm_dpsk_t *inst = instance; + char const *p, *ssid, *psk; + size_t ssid_len, psk_len; + uint8_t buffer[32]; + + /* + * Prefer xlat arguments. But if they don't exist, use the attributes. + */ + p = fmt; + while (isspace((uint8_t) *p)) p++; + + if (!*p) { + if (generate_pmk(request, inst, buffer, sizeof(buffer), NULL, NULL, NULL, 0) == 0) { + RDEBUG("No &request:Called-Station-SSID or &config:Pre-Shared-Key found"); + return 0; + } + } else { + ssid = p; + + while (*p && !isspace((uint8_t) *p)) p++; + + ssid_len = p - ssid; + + if (!*p) { + REDEBUG("Found SSID, but no PSK"); + return 0; + } + + psk = p; + + while (*p && !isspace((uint8_t) *p)) p++; + + psk_len = p - psk; + + if (PKCS5_PBKDF2_HMAC_SHA1(psk, psk_len, (const unsigned char *) ssid, ssid_len, 4096, sizeof(buffer), buffer) == 0) { + RDEBUG("Failed calling OpenSSL to calculate the PMK"); + return 0; + } + } + + if (outlen < sizeof(buffer) * 2 + 1) { + REDEBUG("Output buffer is too small for PMK"); + return 0; + } + + return fr_bin2hex(out, buffer, 32); +} + +static int mod_bootstrap(CONF_SECTION *conf, void *instance) +{ + char const *name; + rlm_dpsk_t *inst = instance; + + /* + * Create the dynamic translation. + */ + name = cf_section_name2(conf); + if (!name) name = cf_section_name1(conf); + inst->xlat_name = name; + xlat_register(inst->xlat_name, dpsk_xlat, NULL, inst); + + if (inst->ruckus) { + inst->ssid = dict_attrbyvalue(PW_RUCKUS_BSSID, VENDORPEC_RUCKUS); + inst->anonce = dict_attrbyvalue(PW_RUCKUS_DPSK_ANONCE, VENDORPEC_RUCKUS); + inst->frame = dict_attrbyvalue(PW_RUCKUS_DPSK_EAPOL_KEY_FRAME, VENDORPEC_RUCKUS); + } else { + inst->ssid = dict_attrbyvalue(PW_CALLED_STATION_SSID, 0); + inst->anonce = dict_attrbyvalue(PW_FREERADIUS_8021X_ANONCE, VENDORPEC_FREERADIUS_EVS5); + inst->frame = dict_attrbyvalue(PW_FREERADIUS_8021X_EAPOL_KEY_MSG, VENDORPEC_FREERADIUS_EVS5); + } + + if (!inst->ssid || !inst->anonce || !inst->frame) { + cf_log_err_cs(conf, "Failed to find attributes in the dictionary. Please do not edit the default dictionaries!"); + return -1; + } + + return 0; +} + +static int cmp_cache_entry(void const *one, void const *two) +{ + rlm_dpsk_cache_t const *a = (rlm_dpsk_cache_t const *) one; + rlm_dpsk_cache_t const *b = (rlm_dpsk_cache_t const *) two; + int rcode; + + rcode = memcmp(a->mac, b->mac, sizeof(a->mac)); + if (rcode != 0) return rcode; + + if (a->ssid_len < b->ssid_len) return -1; + if (a->ssid_len > b->ssid_len) return +1; + + return memcmp(a->ssid, b->ssid, a->ssid_len); +} + +static void free_cache_entry(void *data) +{ + rlm_dpsk_cache_t *entry = (rlm_dpsk_cache_t *) data; + + PTHREAD_MUTEX_LOCK(&entry->inst->mutex); + fr_dlist_entry_unlink(&entry->dlist); + PTHREAD_MUTEX_UNLOCK(&entry->inst->mutex); + + talloc_free(entry); +} + +static int mod_instantiate(CONF_SECTION *conf, void *instance) +{ + rlm_dpsk_t *inst = instance; + + if (!inst->cache_size) return 0; + + FR_INTEGER_BOUND_CHECK("cache_size", inst->cache_size, <=, ((uint32_t) 1) << 16); + + if (!inst->cache_size) return 0; + + FR_INTEGER_BOUND_CHECK("cache_lifetime", inst->cache_lifetime, <=, (7 * 86400)); + FR_INTEGER_BOUND_CHECK("cache_lifetime", inst->cache_lifetime, >=, 3600); + + inst->cache = rbtree_create(inst, cmp_cache_entry, free_cache_entry, RBTREE_FLAG_LOCK); + if (!inst->cache) { + cf_log_err_cs(conf, "Failed creating internal cache"); + return -1; + } + + fr_dlist_entry_init(&inst->head); +#ifdef HAVE_PTHREAD_H + if (pthread_mutex_init(&inst->mutex, NULL) < 0) { + cf_log_err_cs(conf, "Failed creating mutex"); + return -1; + } +#endif + + return 0; +} + +#ifdef HAVE_PTHREAD_H +static int mod_detach(void *instance) +{ + rlm_dpsk_t *inst = instance; + + if (!inst->cache_size) return 0; + + pthread_mutex_destroy(&inst->mutex); + return 0; +} +#endif + +/* + * The module name should be the only globally exported symbol. + * That is, everything else should be 'static'. + * + * If the module needs to temporarily modify it's instantiation + * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE. + * The server will then take care of ensuring that the module + * is single-threaded. + */ +extern module_t rlm_dpsk; +module_t rlm_dpsk = { + .magic = RLM_MODULE_INIT, + .name = "dpsk", + .type = RLM_TYPE_THREAD_SAFE, + .inst_size = sizeof(rlm_dpsk_t), + .config = module_config, + .bootstrap = mod_bootstrap, + .instantiate = mod_instantiate, +#ifdef HAVE_PTHREAD_H + .detach = mod_detach, +#endif + .methods = { + [MOD_AUTHORIZE] = mod_authorize, + [MOD_AUTHENTICATE] = mod_authenticate, + }, +}; |