summaryrefslogtreecommitdiffstats
path: root/src/modules/rlm_dpsk
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-08-26 10:41:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-08-26 10:41:52 +0000
commitde8bf9112695763664912e340b265fa898188460 (patch)
tree9bcd5f8d45fc3b81174d3de8abfd573b68e9d7f6 /src/modules/rlm_dpsk
parentAdding debian version 3.2.3+dfsg-2. (diff)
downloadfreeradius-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.mk10
-rw-r--r--src/modules/rlm_dpsk/rlm_dpsk.c940
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,
+ },
+};