diff mbox series

[PATCHv3,1/2] SAE: add RADIUS Tunnel-Password support

Message ID 20210416111825.3895-2-michael-dev@fami-braun.de
State Changes Requested
Headers show
Series SAE support Tunnel-Password (RADIUS) | expand

Commit Message

michael-dev April 16, 2021, 11:18 a.m. UTC
From: Michael Braun <michael-dev@fami-braun.de>

If no matching sae_password is configured in hostapd, use the RADIUS
supplied Tunnel-Password as SAE password. Though, that is still subject to
the length constraint of WPA passphrase (8..63 chars) to avoid ambiguation.

The SAE password identifier can be optionally supplied using
Tunnel-Client-Auth-ID.

Signed-off-by: Michael Braun <michael-dev@fami-braun.de>

--
v2: fix duplicate passphrase cornercase
v3: fix build error without CONFIG_SAE
---
 hostapd/hostapd.conf     |   6 ++
 src/ap/ap_config.c       |   8 ++
 src/ap/ap_config.h       |   9 +-
 src/ap/ieee802_11.c      |  54 +++++++++++
 src/ap/ieee802_11_auth.c | 145 ++++++++++++++++++++++--------
 src/ap/wpa_auth_glue.c   |  28 ++++--
 src/radius/radius.c      | 188 +++++++++++++++++++++++++--------------
 src/radius/radius.h      |   7 +-
 8 files changed, 328 insertions(+), 117 deletions(-)

Comments

Jouni Malinen Aug. 19, 2021, 7:54 p.m. UTC | #1
On Fri, Apr 16, 2021 at 01:18:24PM +0200, michael-dev@fami-braun.de wrote:
> If no matching sae_password is configured in hostapd, use the RADIUS
> supplied Tunnel-Password as SAE password. Though, that is still subject to
> the length constraint of WPA passphrase (8..63 chars) to avoid ambiguation.
> 
> The SAE password identifier can be optionally supplied using
> Tunnel-Client-Auth-ID.

The cover letter in patch 0/2 noted about "while at it" change to
optimize iteration efficiency, but that is not mentioned here in patch
1/2 that is the only one that actually does changes.. Furthermore, this
patch seems to be combining a large number of independent changes into a
single large patch which makes this inconvenient to review. Ideally,
this would be split into separate patches for each such individual
change. For example, all the radius_attrs[] and debug printing changes
could be in their own patch and the optimizations that are not specific
to the new SAE related support in another one. This would hopefully
leave the actual addition of SAE support easier to understand in its own
patch.

As far as use of RADIUS with SAE is concerned, I guess there could be
uses for this type of mapping from a station to some external storage of
passwords, but I would expect it to be significantly more useful if the
mapping at the RADIUS authentication server could be done based on the
SAE password identifier provided by the station in the Authentication
frame instead of MAC address. That would allow large number of different
passwords (per-user password) to be used even in cases where random MAC
addresses are used. Have you happened to thought about that type of
approach with this or are you only interested in use cases where there
is a relatively small number of passwords in use?
diff mbox series

Patch

diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf
index 7932cb862..7ccad8912 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -1867,6 +1867,12 @@  own_ip_addr=127.0.0.1
 # WPA-PSK and both values are set, SAE uses the sae_password values and WPA-PSK
 # uses the wpa_passphrase value.
 #
+# If no sae_password matches, RADIUS supplied Tunnel-Password are used with
+# higher priority than wpa_passphrase if wpa_psk_radius > 0 except. Thought,
+# the WPA-PSK constraints (8..63 chars) apply here was well.
+# The corresponding SAE password identifier can optionally be supplied using
+# Tunnel-Client-Auth-ID.
+#
 # Each sae_password entry is added to a list of available passwords. This
 # corresponds to the dot11RSNAConfigPasswordValueEntry. sae_password value
 # starts with the password (dot11RSNAConfigPasswordCredential). That value can
diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c
index 7b6249bbe..5357f08d2 100644
--- a/src/ap/ap_config.c
+++ b/src/ap/ap_config.c
@@ -1588,6 +1588,11 @@  int hostapd_sae_pw_id_in_use(struct hostapd_bss_config *conf)
 	if (conf->ssid.wpa_passphrase)
 		without_id = 1;
 
+	if (conf->wpa_psk_radius != PSK_RADIUS_IGNORED) {
+		with_id = 1;
+		without_id = 1;
+	}
+
 	for (pw = conf->sae_passwords; pw; pw = pw->next) {
 		if (pw->identifier)
 			with_id = 1;
@@ -1627,6 +1632,9 @@  bool hostapd_sae_pk_exclusively(struct hostapd_bss_config *conf)
 	if (conf->ssid.wpa_passphrase)
 		return false;
 
+	if (conf->wpa_psk_radius != PSK_RADIUS_IGNORED)
+		return false;
+
 	for (pw = conf->sae_passwords; pw; pw = pw->next) {
 		if (!pw->pk)
 			return false;
diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h
index 95bd79873..4778b3937 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -148,9 +148,12 @@  struct hostapd_vlan {
 #define MAX_PASSPHRASE_LEN 63
 struct hostapd_sta_wpa_psk_short {
 	struct hostapd_sta_wpa_psk_short *next;
-	unsigned int is_passphrase:1;
-	u8 psk[PMK_LEN];
-	char passphrase[MAX_PASSPHRASE_LEN + 1];
+	u8 *psk;
+	char *passphrase;
+#ifdef CONFIG_SAE
+	char *sae_identifier;
+	struct sae_pt *sae_pt;
+#endif /* CONFIG_SAE */
 	int ref; /* (number of references held) - 1 */
 };
 
diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index b404e84af..1e96842bf 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -487,6 +487,56 @@  static void sae_set_state(struct sta_info *sta, enum sae_state state,
 }
 
 
+static void sae_derive_pt_from_psk(struct hostapd_data *hapd,
+				   struct hostapd_sta_wpa_psk_short *psk)
+{
+	struct hostapd_ssid *ssid;
+
+	if (!psk->passphrase)
+		return;
+	if (psk->sae_pt)
+		return;
+
+	ssid = &hapd->conf->ssid;
+
+	psk->sae_pt = sae_derive_pt(hapd->conf->sae_groups, ssid->ssid,
+				    ssid->ssid_len, (u8 *) psk->passphrase,
+				    os_strlen(psk->passphrase),
+				    psk->sae_identifier);
+
+}
+
+
+static const char * sae_get_password_from_psk(struct hostapd_data *hapd,
+					      struct sta_info *sta,
+					      const char *rx_id,
+					      struct sae_pt **s_pt)
+{
+	struct hostapd_sta_wpa_psk_short *pos;
+
+	for (pos = sta->psk; pos; pos = pos->next) {
+		if ((rx_id && !pos->sae_identifier) ||
+		    (!rx_id && pos->sae_identifier))
+			continue;
+		if (rx_id && pos->sae_identifier &&
+		    os_strcmp(rx_id, pos->sae_identifier) != 0)
+			continue;
+
+		sae_derive_pt_from_psk(hapd, pos);
+
+		if (!pos->sae_pt)
+			continue;
+
+		if (s_pt)
+			*s_pt = pos->sae_pt;
+
+		return pos->passphrase;
+	}
+
+	return NULL;
+}
+
+
 static const char * sae_get_password(struct hostapd_data *hapd,
 				     struct sta_info *sta,
 				     const char *rx_id,
@@ -514,6 +564,10 @@  static const char * sae_get_password(struct hostapd_data *hapd,
 			pk = pw->pk;
 		break;
 	}
+
+	if (!password)
+		password = sae_get_password_from_psk(hapd, sta, rx_id, &pt);
+
 	if (!password) {
 		password = hapd->conf->ssid.wpa_passphrase;
 		pt = hapd->conf->ssid.pt;
diff --git a/src/ap/ieee802_11_auth.c b/src/ap/ieee802_11_auth.c
index 783ee6dea..449fde303 100644
--- a/src/ap/ieee802_11_auth.c
+++ b/src/ap/ieee802_11_auth.c
@@ -23,6 +23,7 @@ 
 #include "ieee802_11.h"
 #include "ieee802_1x.h"
 #include "ieee802_11_auth.h"
+#include "common/sae.h"
 
 #define RADIUS_ACL_TIMEOUT 30
 
@@ -368,72 +369,133 @@  void hostapd_acl_expire(struct hostapd_data *hapd)
 	hostapd_acl_expire_queries(hapd, &now);
 }
 
+#ifdef CONFIG_SAE
+static int get_tunnel_sae_id(struct radius_msg *msg, u8 tag, u8** buf,
+			      size_t *len, size_t idx, size_t *next_idx)
+{
+	const u8 type = RADIUS_ATTR_TUNNEL_CLIENT_AUTH_ID;
 
-static void decode_tunnel_passwords(struct hostapd_data *hapd,
-				    const u8 *shared_secret,
-				    size_t shared_secret_len,
-				    struct radius_msg *msg,
-				    struct radius_msg *req,
-				    struct hostapd_cached_radius_acl *cache)
+	return radius_msg_get_attr_tag_ptr(msg, type, tag, buf, len, idx,
+					   next_idx);
+}
+#endif /* CONFIG_SAE */
+
+
+static void decode_tunnel_password(struct hostapd_data *hapd,
+				   const u8 *shared_secret,
+				   size_t shared_secret_len,
+				   struct radius_msg *msg,
+				   struct radius_msg *req,
+				   struct hostapd_cached_radius_acl *cache,
+				   size_t psk_idx, size_t *psk_next)
 {
 	int passphraselen;
 	char *passphrase;
-	size_t i;
 	struct hostapd_sta_wpa_psk_short *psk;
-
+	int has_sae_id = 0;
+	u8 tag;
+#ifdef CONFIG_SAE
+	u8 *sae_id;
+	size_t sae_id_len;
+	size_t sae_idx = 0, sae_idx_next;
+#endif /* CONFIG_SAE */
+
+	passphrase = radius_msg_get_tunnel_password(
+		msg, &passphraselen, shared_secret, shared_secret_len,
+		req, psk_idx, &tag, psk_next);
 	/*
-	 * Decode all tunnel passwords as PSK and save them into a linked list.
+	 * Passphrase is NULL iff there is no i-th Tunnel-Password
+	 * attribute in msg.
 	 */
-	for (i = 0; ; i++) {
-		passphrase = radius_msg_get_tunnel_password(
-			msg, &passphraselen, shared_secret, shared_secret_len,
-			req, i);
-		/*
-		 * Passphrase is NULL iff there is no i-th Tunnel-Password
-		 * attribute in msg.
-		 */
-		if (passphrase == NULL)
-			break;
+	if (passphrase == NULL) {
+		*psk_next = 0;
+		return;
+	}
 
-		/*
-		 * Passphase should be 8..63 chars (to be hashed with SSID)
-		 * or 64 chars hex string (no separate hashing with SSID).
-		 */
+	/*
+	 * Passphase should be 8..63 chars (to be hashed with SSID)
+	 * or 64 chars hex string (no separate hashing with SSID).
+	 */
 
-		if (passphraselen < MIN_PASSPHRASE_LEN ||
-		    passphraselen > MAX_PASSPHRASE_LEN + 1)
-			goto free_pass;
+	if (passphraselen < MIN_PASSPHRASE_LEN ||
+	    passphraselen > MAX_PASSPHRASE_LEN + 1)
+		goto free_pass;
 
+	do {
 		/*
 		 * passphrase does not contain the NULL termination.
 		 * Add it here as pbkdf2_sha1() requires it.
 		 */
 		psk = os_zalloc(sizeof(struct hostapd_sta_wpa_psk_short));
-		if (psk) {
-			if ((passphraselen == MAX_PASSPHRASE_LEN + 1) &&
-			    (hexstr2bin(passphrase, psk->psk, PMK_LEN) < 0)) {
+		if (!psk)
+			goto skip;
+		if (passphraselen == MAX_PASSPHRASE_LEN + 1) {
+			psk->psk = os_zalloc(PMK_LEN);
+			if (!psk->psk)
+				goto skip;
+			if (hexstr2bin(passphrase, psk->psk, PMK_LEN) < 0) {
 				hostapd_logger(hapd, cache->addr,
 					       HOSTAPD_MODULE_RADIUS,
 					       HOSTAPD_LEVEL_WARNING,
 					       "invalid hex string (%d chars) in Tunnel-Password",
 					       passphraselen);
 				goto skip;
-			} else if (passphraselen <= MAX_PASSPHRASE_LEN) {
-				os_memcpy(psk->passphrase, passphrase,
-					  passphraselen);
-				psk->is_passphrase = 1;
 			}
-			psk->next = cache->info.psk;
-			cache->info.psk = psk;
-			psk = NULL;
+		} else {
+			psk->passphrase = os_zalloc(passphraselen + 1);
+			os_memcpy(psk->passphrase, passphrase, passphraselen);
+#ifdef CONFIG_SAE
+			/*
+			 * Lookup Tunnel-Client-Auth-ID by tag
+			 */
+			has_sae_id = !get_tunnel_sae_id(msg, tag, &sae_id,
+							&sae_id_len, sae_idx,
+							&sae_idx_next);
+			if (has_sae_id) {
+				psk->sae_identifier = os_zalloc(sae_id_len+1);
+				if (!psk->sae_identifier)
+					goto skip;
+				os_memcpy(psk->sae_identifier, sae_id, sae_id_len);
+				sae_idx = sae_idx_next;
+			} else if (sae_idx > 0)
+				goto skip;
+#endif /* CONFIG_SAE */
 		}
+		psk->next = cache->info.psk;
+		cache->info.psk = psk;
+		psk = NULL;
+	} while (has_sae_id);
+
 skip:
+	if (psk) {
+		os_free(psk->psk);
+		os_free(psk->passphrase);
 		os_free(psk);
-free_pass:
-		os_free(passphrase);
 	}
+free_pass:
+	os_free(passphrase);
 }
 
+static void decode_tunnel_passwords(struct hostapd_data *hapd,
+				    const u8 *shared_secret,
+				    size_t shared_secret_len,
+				    struct radius_msg *msg,
+				    struct radius_msg *req,
+				    struct hostapd_cached_radius_acl *cache)
+{
+	size_t psk_idx = 0, psk_next;
+
+	/*
+	 * Decode all tunnel passwords as PSK and save them into a linked list.
+	 */
+	while (1) {
+		decode_tunnel_password(hapd, shared_secret, shared_secret_len,
+				       msg, req, cache, psk_idx, &psk_next);
+		if (psk_next == 0)
+			break;
+		psk_idx = psk_next;
+	}
+}
 
 /**
  * hostapd_acl_recv_radius - Process incoming RADIUS Authentication messages
@@ -646,6 +708,13 @@  void hostapd_free_psk_list(struct hostapd_sta_wpa_psk_short *psk)
 	while (psk) {
 		struct hostapd_sta_wpa_psk_short *prev = psk;
 		psk = psk->next;
+#ifdef CONFIG_SAE
+		if (prev->sae_pt)
+			sae_deinit_pt(prev->sae_pt);
+		os_free(prev->sae_identifier);
+		os_free(prev->psk);
+		os_free(prev->passphrase);
+#endif /* CONFIG_SAE */
 		os_free(prev);
 	}
 }
diff --git a/src/ap/wpa_auth_glue.c b/src/ap/wpa_auth_glue.c
index c3b2e81e2..c88552db0 100644
--- a/src/ap/wpa_auth_glue.c
+++ b/src/ap/wpa_auth_glue.c
@@ -381,23 +381,35 @@  static const u8 * hostapd_wpa_auth_get_psk(void *ctx, const u8 *addr,
 	 * logic list (all hostapd_get_psk; all sta->psk)
 	 */
 	if (sta && sta->psk && !psk) {
-		struct hostapd_sta_wpa_psk_short *pos;
+		struct hostapd_sta_wpa_psk_short *pos, *prev = NULL;
 
 		if (vlan_id)
 			*vlan_id = 0;
-		psk = sta->psk->psk;
+
 		for (pos = sta->psk; pos; pos = pos->next) {
-			if (pos->is_passphrase) {
+			if (pos->passphrase && !pos->psk) {
+				if (prev && prev->psk && prev->passphrase &&
+				    !os_strcmp(pos->passphrase, prev->passphrase))
+					continue;
+
+				pos->psk = os_zalloc(PMK_LEN);
+				if (!pos->psk)
+					continue;
+
 				pbkdf2_sha1(pos->passphrase,
 					    hapd->conf->ssid.ssid,
 					    hapd->conf->ssid.ssid_len, 4096,
 					    pos->psk, PMK_LEN);
-				pos->is_passphrase = 0;
-			}
-			if (pos->psk == prev_psk) {
-				psk = pos->next ? pos->next->psk : NULL;
-				break;
+			} else if (!pos->psk)
+				continue;
+
+			if (prev_psk && (!prev || prev->psk != prev_psk)) {
+				prev = pos;
+				continue;
 			}
+
+			psk = pos->psk;
+			break;
 		}
 	}
 	return psk;
diff --git a/src/radius/radius.c b/src/radius/radius.c
index be16e27b9..64396d319 100644
--- a/src/radius/radius.c
+++ b/src/radius/radius.c
@@ -165,101 +165,108 @@  struct radius_attr_type {
 		RADIUS_ATTR_UNDIST, RADIUS_ATTR_TEXT, RADIUS_ATTR_IP,
 		RADIUS_ATTR_HEXDUMP, RADIUS_ATTR_INT32, RADIUS_ATTR_IPV6
 	} data_type;
+	enum {
+		RADIUS_ATTR_F_TAGGED = BIT(0),
+		RADIUS_ATTR_F_TAGGED_LIM = BIT(1),
+	} flags;
 };
 
 static const struct radius_attr_type radius_attrs[] =
 {
-	{ RADIUS_ATTR_USER_NAME, "User-Name", RADIUS_ATTR_TEXT },
-	{ RADIUS_ATTR_USER_PASSWORD, "User-Password", RADIUS_ATTR_UNDIST },
-	{ RADIUS_ATTR_NAS_IP_ADDRESS, "NAS-IP-Address", RADIUS_ATTR_IP },
-	{ RADIUS_ATTR_NAS_PORT, "NAS-Port", RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_SERVICE_TYPE, "Service-Type", RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_FRAMED_IP_ADDRESS, "Framed-IP-Address", RADIUS_ATTR_IP },
-	{ RADIUS_ATTR_FRAMED_MTU, "Framed-MTU", RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_REPLY_MESSAGE, "Reply-Message", RADIUS_ATTR_TEXT },
-	{ RADIUS_ATTR_STATE, "State", RADIUS_ATTR_UNDIST },
-	{ RADIUS_ATTR_CLASS, "Class", RADIUS_ATTR_UNDIST },
-	{ RADIUS_ATTR_VENDOR_SPECIFIC, "Vendor-Specific", RADIUS_ATTR_UNDIST },
-	{ RADIUS_ATTR_SESSION_TIMEOUT, "Session-Timeout", RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_IDLE_TIMEOUT, "Idle-Timeout", RADIUS_ATTR_INT32 },
+	{ RADIUS_ATTR_USER_NAME, "User-Name", RADIUS_ATTR_TEXT, 0},
+	{ RADIUS_ATTR_USER_PASSWORD, "User-Password", RADIUS_ATTR_UNDIST, 0},
+	{ RADIUS_ATTR_NAS_IP_ADDRESS, "NAS-IP-Address", RADIUS_ATTR_IP, 0},
+	{ RADIUS_ATTR_NAS_PORT, "NAS-Port", RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_SERVICE_TYPE, "Service-Type", RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_FRAMED_IP_ADDRESS, "Framed-IP-Address", RADIUS_ATTR_IP, 0},
+	{ RADIUS_ATTR_FRAMED_MTU, "Framed-MTU", RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_REPLY_MESSAGE, "Reply-Message", RADIUS_ATTR_TEXT, 0},
+	{ RADIUS_ATTR_STATE, "State", RADIUS_ATTR_UNDIST, 0},
+	{ RADIUS_ATTR_CLASS, "Class", RADIUS_ATTR_UNDIST, 0},
+	{ RADIUS_ATTR_VENDOR_SPECIFIC, "Vendor-Specific", RADIUS_ATTR_UNDIST, 0},
+	{ RADIUS_ATTR_SESSION_TIMEOUT, "Session-Timeout", RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_IDLE_TIMEOUT, "Idle-Timeout", RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_TERMINATION_ACTION, "Termination-Action",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_CALLED_STATION_ID, "Called-Station-Id",
-	  RADIUS_ATTR_TEXT },
+	  RADIUS_ATTR_TEXT, 0},
 	{ RADIUS_ATTR_CALLING_STATION_ID, "Calling-Station-Id",
-	  RADIUS_ATTR_TEXT },
-	{ RADIUS_ATTR_NAS_IDENTIFIER, "NAS-Identifier", RADIUS_ATTR_TEXT },
-	{ RADIUS_ATTR_PROXY_STATE, "Proxy-State", RADIUS_ATTR_UNDIST },
+	  RADIUS_ATTR_TEXT, 0},
+	{ RADIUS_ATTR_NAS_IDENTIFIER, "NAS-Identifier", RADIUS_ATTR_TEXT, 0},
+	{ RADIUS_ATTR_PROXY_STATE, "Proxy-State", RADIUS_ATTR_UNDIST, 0},
 	{ RADIUS_ATTR_ACCT_STATUS_TYPE, "Acct-Status-Type",
-	  RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_ACCT_DELAY_TIME, "Acct-Delay-Time", RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_ACCT_DELAY_TIME, "Acct-Delay-Time", RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_INPUT_OCTETS, "Acct-Input-Octets",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_OUTPUT_OCTETS, "Acct-Output-Octets",
-	  RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_ACCT_SESSION_ID, "Acct-Session-Id", RADIUS_ATTR_TEXT },
-	{ RADIUS_ATTR_ACCT_AUTHENTIC, "Acct-Authentic", RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_ACCT_SESSION_ID, "Acct-Session-Id", RADIUS_ATTR_TEXT, 0},
+	{ RADIUS_ATTR_ACCT_AUTHENTIC, "Acct-Authentic", RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_SESSION_TIME, "Acct-Session-Time",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_INPUT_PACKETS, "Acct-Input-Packets",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_OUTPUT_PACKETS, "Acct-Output-Packets",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_TERMINATE_CAUSE, "Acct-Terminate-Cause",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_MULTI_SESSION_ID, "Acct-Multi-Session-Id",
-	  RADIUS_ATTR_TEXT },
-	{ RADIUS_ATTR_ACCT_LINK_COUNT, "Acct-Link-Count", RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_TEXT, 0},
+	{ RADIUS_ATTR_ACCT_LINK_COUNT, "Acct-Link-Count", RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_INPUT_GIGAWORDS, "Acct-Input-Gigawords",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_ACCT_OUTPUT_GIGAWORDS, "Acct-Output-Gigawords",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_EVENT_TIMESTAMP, "Event-Timestamp",
-	  RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_EGRESS_VLANID, "EGRESS-VLANID", RADIUS_ATTR_HEXDUMP },
-	{ RADIUS_ATTR_NAS_PORT_TYPE, "NAS-Port-Type", RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_TUNNEL_TYPE, "Tunnel-Type", RADIUS_ATTR_HEXDUMP },
+	  RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_EGRESS_VLANID, "EGRESS-VLANID", RADIUS_ATTR_HEXDUMP, 0},
+	{ RADIUS_ATTR_NAS_PORT_TYPE, "NAS-Port-Type", RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_TUNNEL_TYPE, "Tunnel-Type", RADIUS_ATTR_HEXDUMP,
+	  RADIUS_ATTR_F_TAGGED},
 	{ RADIUS_ATTR_TUNNEL_MEDIUM_TYPE, "Tunnel-Medium-Type",
-	  RADIUS_ATTR_HEXDUMP },
+	  RADIUS_ATTR_HEXDUMP, RADIUS_ATTR_F_TAGGED},
 	{ RADIUS_ATTR_TUNNEL_PASSWORD, "Tunnel-Password",
-	  RADIUS_ATTR_UNDIST },
-	{ RADIUS_ATTR_CONNECT_INFO, "Connect-Info", RADIUS_ATTR_TEXT },
-	{ RADIUS_ATTR_EAP_MESSAGE, "EAP-Message", RADIUS_ATTR_UNDIST },
+	  RADIUS_ATTR_UNDIST, RADIUS_ATTR_F_TAGGED},
+	{ RADIUS_ATTR_CONNECT_INFO, "Connect-Info", RADIUS_ATTR_TEXT, 0},
+	{ RADIUS_ATTR_EAP_MESSAGE, "EAP-Message", RADIUS_ATTR_UNDIST, 0},
 	{ RADIUS_ATTR_MESSAGE_AUTHENTICATOR, "Message-Authenticator",
-	  RADIUS_ATTR_UNDIST },
+	  RADIUS_ATTR_UNDIST, 0},
 	{ RADIUS_ATTR_TUNNEL_PRIVATE_GROUP_ID, "Tunnel-Private-Group-Id",
-	  RADIUS_ATTR_HEXDUMP },
+	  RADIUS_ATTR_HEXDUMP, RADIUS_ATTR_F_TAGGED_LIM },
 	{ RADIUS_ATTR_ACCT_INTERIM_INTERVAL, "Acct-Interim-Interval",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_CHARGEABLE_USER_IDENTITY, "Chargeable-User-Identity",
-	  RADIUS_ATTR_TEXT },
-	{ RADIUS_ATTR_NAS_IPV6_ADDRESS, "NAS-IPv6-Address", RADIUS_ATTR_IPV6 },
-	{ RADIUS_ATTR_ERROR_CAUSE, "Error-Cause", RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_EAP_KEY_NAME, "EAP-Key-Name", RADIUS_ATTR_HEXDUMP },
-	{ RADIUS_ATTR_OPERATOR_NAME, "Operator-Name", RADIUS_ATTR_TEXT },
+	  RADIUS_ATTR_TEXT, 0},
+	{ RADIUS_ATTR_TUNNEL_CLIENT_AUTH_ID, "Tunnel-Client-Auth-ID",
+	  RADIUS_ATTR_TEXT, RADIUS_ATTR_F_TAGGED},
+	{ RADIUS_ATTR_NAS_IPV6_ADDRESS, "NAS-IPv6-Address", RADIUS_ATTR_IPV6, 0},
+	{ RADIUS_ATTR_ERROR_CAUSE, "Error-Cause", RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_EAP_KEY_NAME, "EAP-Key-Name", RADIUS_ATTR_HEXDUMP, 0},
+	{ RADIUS_ATTR_OPERATOR_NAME, "Operator-Name", RADIUS_ATTR_TEXT, 0},
 	{ RADIUS_ATTR_LOCATION_INFO, "Location-Information",
-	  RADIUS_ATTR_HEXDUMP },
-	{ RADIUS_ATTR_LOCATION_DATA, "Location-Data", RADIUS_ATTR_HEXDUMP },
+	  RADIUS_ATTR_HEXDUMP, 0},
+	{ RADIUS_ATTR_LOCATION_DATA, "Location-Data", RADIUS_ATTR_HEXDUMP, 0},
 	{ RADIUS_ATTR_BASIC_LOCATION_POLICY_RULES,
-	  "Basic-Location-Policy-Rules", RADIUS_ATTR_HEXDUMP },
+	  "Basic-Location-Policy-Rules", RADIUS_ATTR_HEXDUMP, 0},
 	{ RADIUS_ATTR_EXTENDED_LOCATION_POLICY_RULES,
-	  "Extended-Location-Policy-Rules", RADIUS_ATTR_HEXDUMP },
-	{ RADIUS_ATTR_LOCATION_CAPABLE, "Location-Capable", RADIUS_ATTR_INT32 },
+	  "Extended-Location-Policy-Rules", RADIUS_ATTR_HEXDUMP, 0},
+	{ RADIUS_ATTR_LOCATION_CAPABLE, "Location-Capable", RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_REQUESTED_LOCATION_INFO, "Requested-Location-Info",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_MOBILITY_DOMAIN_ID, "Mobility-Domain-Id",
-	  RADIUS_ATTR_INT32 },
-	{ RADIUS_ATTR_WLAN_HESSID, "WLAN-HESSID", RADIUS_ATTR_TEXT },
+	  RADIUS_ATTR_INT32, 0},
+	{ RADIUS_ATTR_WLAN_HESSID, "WLAN-HESSID", RADIUS_ATTR_TEXT, 0},
 	{ RADIUS_ATTR_WLAN_REASON_CODE, "WLAN-Reason-Code",
-	  RADIUS_ATTR_INT32 },
+	  RADIUS_ATTR_INT32, 0},
 	{ RADIUS_ATTR_WLAN_PAIRWISE_CIPHER, "WLAN-Pairwise-Cipher",
-	  RADIUS_ATTR_HEXDUMP },
+	  RADIUS_ATTR_HEXDUMP, 0},
 	{ RADIUS_ATTR_WLAN_GROUP_CIPHER, "WLAN-Group-Cipher",
-	  RADIUS_ATTR_HEXDUMP },
+	  RADIUS_ATTR_HEXDUMP, 0},
 	{ RADIUS_ATTR_WLAN_AKM_SUITE, "WLAN-AKM-Suite",
-	  RADIUS_ATTR_HEXDUMP },
+	  RADIUS_ATTR_HEXDUMP, 0},
 	{ RADIUS_ATTR_WLAN_GROUP_MGMT_CIPHER, "WLAN-Group-Mgmt-Pairwise-Cipher",
-	  RADIUS_ATTR_HEXDUMP },
+	  RADIUS_ATTR_HEXDUMP, 0},
 };
 #define RADIUS_ATTRS ARRAY_SIZE(radius_attrs)
 
@@ -295,6 +302,18 @@  static void radius_msg_dump_attr(struct radius_attr_hdr *hdr)
 	len = hdr->length - sizeof(struct radius_attr_hdr);
 	pos = (unsigned char *) (hdr + 1);
 
+	if (len > 0 && (attr->flags & RADIUS_ATTR_F_TAGGED)) {
+		wpa_printf(MSG_INFO, "      Tag: %u", pos[0]);
+		pos++;
+		len--;
+	}
+	if (len > 0 && (attr->flags & RADIUS_ATTR_F_TAGGED_LIM) &&
+	    pos[0] <= 0x1F) {
+		wpa_printf(MSG_INFO, "      Tag: %u", pos[0]);
+		pos++;
+		len--;
+	}
+
 	switch (attr->data_type) {
 	case RADIUS_ATTR_TEXT:
 		printf_encode(buf, sizeof(buf), pos, len);
@@ -1379,6 +1398,39 @@  int radius_msg_get_attr(struct radius_msg *msg, u8 type, u8 *buf, size_t len)
 }
 
 
+int radius_msg_get_attr_tag_ptr(struct radius_msg *msg, u8 type, u8 tag,
+				u8 **buf, size_t *len, size_t idx,
+				size_t *next_idx)
+{
+	size_t i;
+	struct radius_attr_hdr *attr;
+
+	for (i = idx; i < msg->attr_used; i++) {
+		attr = radius_get_attr_hdr(msg, i);
+		if (attr->type != type ||
+		    attr->length <= sizeof(*attr))
+			continue;
+
+		*buf = (u8 *) (attr + 1);
+		*len = attr->length - sizeof(*attr) - 1;
+
+		if (*buf[0] != tag)
+			continue;
+
+		(*buf)++; // skip tag from output
+		if (next_idx)
+			*next_idx = i + 1;
+
+		return 0;
+	}
+
+	*buf = NULL;
+	*len = 0;
+
+	return -1;
+}
+
+
 int radius_msg_get_attr_ptr(struct radius_msg *msg, u8 type, u8 **buf,
 			    size_t *len, const u8 *start)
 {
@@ -1554,7 +1606,8 @@  int radius_msg_get_vlanid(struct radius_msg *msg, int *untagged, int numtagged,
  */
 char * radius_msg_get_tunnel_password(struct radius_msg *msg, int *keylen,
 				      const u8 *secret, size_t secret_len,
-				      struct radius_msg *sent_msg, size_t n)
+				      struct radius_msg *sent_msg, size_t idx,
+				      u8 *out_tag, size_t *next_idx)
 {
 	u8 *buf = NULL;
 	size_t buflen;
@@ -1564,7 +1617,7 @@  char * radius_msg_get_tunnel_password(struct radius_msg *msg, int *keylen,
 	size_t len[3];
 	u8 hash[16];
 	u8 *pos;
-	size_t i, j = 0;
+	size_t i;
 	struct radius_attr_hdr *attr;
 	const u8 *data;
 	size_t dlen;
@@ -1573,7 +1626,7 @@  char * radius_msg_get_tunnel_password(struct radius_msg *msg, int *keylen,
 	char *ret = NULL;
 
 	/* find n-th valid Tunnel-Password attribute */
-	for (i = 0; i < msg->attr_used; i++) {
+	for (i = idx; i < msg->attr_used; i++) {
 		attr = radius_get_attr_hdr(msg, i);
 		if (attr == NULL ||
 		    attr->type != RADIUS_ATTR_TUNNEL_PASSWORD) {
@@ -1585,9 +1638,8 @@  char * radius_msg_get_tunnel_password(struct radius_msg *msg, int *keylen,
 		dlen = attr->length - sizeof(*attr);
 		if (dlen <= 3 || dlen % 16 != 3)
 			continue;
-		j++;
-		if (j <= n)
-			continue;
+		if (next_idx)
+			*next_idx = i + 1;
 
 		fdata = data;
 		fdlen = dlen;
@@ -1603,6 +1655,8 @@  char * radius_msg_get_tunnel_password(struct radius_msg *msg, int *keylen,
 	buflen = fdlen;
 
 	/* init pointers */
+	if (out_tag)
+		*out_tag = *buf;
 	salt = buf + 1;
 	str = buf + 3;
 
diff --git a/src/radius/radius.h b/src/radius/radius.h
index fb8148180..f2b5e8540 100644
--- a/src/radius/radius.h
+++ b/src/radius/radius.h
@@ -92,6 +92,7 @@  enum { RADIUS_ATTR_USER_NAME = 1,
        RADIUS_ATTR_TUNNEL_PRIVATE_GROUP_ID = 81,
        RADIUS_ATTR_ACCT_INTERIM_INTERVAL = 85,
        RADIUS_ATTR_CHARGEABLE_USER_IDENTITY = 89,
+       RADIUS_ATTR_TUNNEL_CLIENT_AUTH_ID = 90,
        RADIUS_ATTR_NAS_IPV6_ADDRESS = 95,
        RADIUS_ATTR_ERROR_CAUSE = 101,
        RADIUS_ATTR_EAP_KEY_NAME = 102,
@@ -297,7 +298,8 @@  int radius_msg_get_vlanid(struct radius_msg *msg, int *untagged, int numtagged,
 			  int *tagged);
 char * radius_msg_get_tunnel_password(struct radius_msg *msg, int *keylen,
 				      const u8 *secret, size_t secret_len,
-				      struct radius_msg *sent_msg, size_t n);
+				      struct radius_msg *sent_msg, size_t idx,
+				      u8 *out_tag, size_t *next_idx);
 
 static inline int radius_msg_add_attr_int32(struct radius_msg *msg, u8 type,
 					    u32 value)
@@ -321,6 +323,9 @@  static inline int radius_msg_get_attr_int32(struct radius_msg *msg, u8 type,
 int radius_msg_get_attr_ptr(struct radius_msg *msg, u8 type, u8 **buf,
 			    size_t *len, const u8 *start);
 int radius_msg_count_attr(struct radius_msg *msg, u8 type, int min_len);
+int radius_msg_get_attr_tag_ptr(struct radius_msg *msg, u8 type, u8 tag,
+				u8 **buf, size_t *len, size_t idx,
+				size_t *next_idx);
 
 
 struct radius_attr_data {