Blob Blame History Raw
Date: Tue, 11 Jan 2022
Subject: [PATCH] Backport OpenSSL3 fixes from 3.0.x

Backport TLS and OpenSSL3 fixes from the 3.0.x branch as of May 24th, 2022.

Related: rhbz#1978216
Related: rhbz#2083699
Signed-off-by: Antonio Torres <antorres@redhat.com>

[antorres@redhat.com]: these changes include the macro WITH_FIPS, which allows FreeRADIUS
to work on top of OpenSSL 3.0 when the system is in FIPS mode. We enable this macro on the specfile.
---
 share/dictionary.freeradius.internal               |   54 +-
 src/include/build.h                                |   25 +-
 src/include/libradius.h                            |   23 +-
 src/include/listen.h                               |   26 +-
 src/include/md4.h                                  |   50 +-
 src/include/md5.h                                  |   33 +-
 src/include/openssl3.h                             |  109 ++
 src/include/tls-h                                  |   32 +-
 src/include/token.h                                |    7 +-
 src/lib/dict.c                                     |  150 +-
 src/lib/hmacmd5.c                                  |    6 +-
 src/lib/hmacsha1.c                                 |    5 +-
 src/lib/md4.c                                      |    1 +
 src/lib/md5.c                                      |    1 +
 src/lib/pair.c                                     |   97 +-
 src/lib/print.c                                    |   19 +-
 src/lib/radius.c                                   |  300 ++-
 src/lib/token.c                                    |   24 +-
 src/main/cb.c                                      |  121 +-
 src/main/map.c                                     |   54 +-
 src/main/radclient.c                               |   77 +-
 src/main/tls.c                                     | 1912 ++++++++++++++++----
 src/main/tls_listen.c                              |  177 +-
 src/modules/proto_dhcp/rlm_dhcp.c                  |    2 +-
 src/modules/rlm_eap/libeap/eap_tls.c               |  178 +-
 src/modules/rlm_eap/libeap/eap_tls.h               |   10 +-
 src/modules/rlm_eap/libeap/mppe_keys.c             |  211 ++-
 src/modules/rlm_eap/radeapclient.c                 |    8 +
 src/modules/rlm_eap/types/rlm_eap_fast/eap_fast.c  |   51 +-
 .../rlm_eap/types/rlm_eap_fast/rlm_eap_fast.c      |   64 +-
 src/modules/rlm_eap/types/rlm_eap_peap/peap.c      |   22 +-
 .../rlm_eap/types/rlm_eap_peap/rlm_eap_peap.c      |   21 +-
 src/modules/rlm_eap/types/rlm_eap_pwd/const_time.h |  190 ++
 src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.c    |  779 +++++---
 src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.h    |   16 +-
 .../rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.c        |  508 +++++-
 .../rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.h        |    2 +
 .../rlm_eap/types/rlm_eap_tls/rlm_eap_tls.c        |   52 +-
 .../rlm_eap/types/rlm_eap_tls/rlm_eap_tls.h        |    5 +
 .../rlm_eap/types/rlm_eap_ttls/rlm_eap_ttls.c      |   20 +-
 src/modules/rlm_eap/types/rlm_eap_ttls/ttls.c      |   25 +-
 src/modules/rlm_exec/rlm_exec.c                    |    4 +-
 src/modules/rlm_expr/rlm_expr.c                    |  115 ++
 src/modules/rlm_files/rlm_files.c                  |    2 +-
 src/modules/rlm_ldap/ldap.h                        |    4 +
 src/modules/rlm_mschap/rlm_mschap.c                |   99 +-
 src/modules/rlm_otp/otp_mppe.c                     |   16 +-
 src/modules/rlm_otp/otp_pwe.c                      |    8 -
 src/modules/rlm_otp/otp_radstate.c                 |    9 +-
 src/modules/rlm_rest/rest.c                        |  107 +-
 src/modules/rlm_rest/rest.h                        |   18 +
 src/modules/rlm_rest/rlm_rest.c                    |   12 +
 src/modules/rlm_wimax/milenage.c                   |  642 +++++++
 src/modules/rlm_wimax/milenage.h                   |  128 ++
 src/modules/rlm_wimax/rlm_wimax.c                  |  429 ++++-
 src/tests/keywords/md4                             |   58 +
 56 files changed, 5913 insertions(+), 1205 deletions(-)

diff --git a/share/dictionary.freeradius.internal b/share/dictionary.freeradius.internal
index 724e1f7ff6..347e3e59f3 100644
--- a/share/dictionary.freeradius.internal
+++ b/share/dictionary.freeradius.internal
@@ -148,7 +148,7 @@ VALUE	EAP-IKEv2-IDType		DER_ASN1_GN		10
 VALUE	EAP-IKEv2-IDType		KEY_ID			11
 
 ATTRIBUTE	EAP-IKEv2-ID				1104	string
-ATTRIBUTE	EAP-IKEv2-Secret			1105	string
+ATTRIBUTE	EAP-IKEv2-Secret			1105	string	secret
 ATTRIBUTE	EAP-IKEv2-AuthType			1106	integer
 
 VALUE	EAP-IKEv2-AuthType		none			0
@@ -196,7 +196,7 @@ ATTRIBUTE	FreeRADIUS-Client-Require-MA		1122	integer
 VALUE	FreeRADIUS-Client-Require-MA	no			0
 VALUE	FreeRADIUS-Client-Require-MA	yes			1
 
-ATTRIBUTE	FreeRADIUS-Client-Secret		1123	string
+ATTRIBUTE	FreeRADIUS-Client-Secret		1123	string secret
 ATTRIBUTE	FreeRADIUS-Client-Shortname		1124	string
 ATTRIBUTE	FreeRADIUS-Client-NAS-Type		1125	string
 ATTRIBUTE	FreeRADIUS-Client-Virtual-Server	1126	string
@@ -254,6 +254,7 @@ ATTRIBUTE	FreeRADIUS-Response-Delay-USec		1155	integer
 
 ATTRIBUTE	REST-HTTP-Header			1160	string
 ATTRIBUTE	REST-HTTP-Body				1161	string
+ATTRIBUTE	REST-HTTP-Status-Code			1162	integer
 
 ATTRIBUTE	Cache-Expires				1170	date
 ATTRIBUTE	Cache-Created				1171	date
@@ -277,7 +278,24 @@ ATTRIBUTE	SSHA2-256-Password			1178	octets
 ATTRIBUTE	SSHA2-384-Password			1179	octets
 ATTRIBUTE	SSHA2-512-Password			1180	octets
 
+ATTRIBUTE	PBKDF2-Password				1181	octets
+ATTRIBUTE	SSHA3-224-Password			1182	octets
+ATTRIBUTE	SSHA3-256-Password			1183	octets
+ATTRIBUTE	SSHA3-384-Password			1184	octets
+ATTRIBUTE	SSHA3-512-Password			1185	octets
+
 ATTRIBUTE	MS-CHAP-Peer-Challenge			1192	octets
+ATTRIBUTE	Home-Server-Name			1193	string
+ATTRIBUTE	Originating-Realm-Key			1194	string
+ATTRIBUTE	Proxy-To-Originating-Realm		1195	string
+
+ATTRIBUTE	TOTP-Secret				1194	string # base32 encoded
+ATTRIBUTE	TOTP-Key				1195	octets # raw key
+ATTRIBUTE	TOTP-Password				1196	string
+
+ATTRIBUTE	Proxy-Tunneled-Request-As-EAP		1197	integer
+VALUE	Proxy-Tunneled-Request-As-EAP	No			0
+VALUE	Proxy-Tunneled-Request-As-EAP	Yes			1
 
 #
 #	Range:	1200-1279
@@ -318,6 +336,10 @@ ATTRIBUTE	EAP-Sim-Algo-Version			1216	integer
 ATTRIBUTE	Outer-Realm-Name			1218	string
 ATTRIBUTE	Inner-Realm-Name			1219	string
 
+ATTRIBUTE	EAP-Pwd-Password-Hash			1220	octets
+ATTRIBUTE	EAP-Pwd-Password-Salt			1221	octets
+ATTRIBUTE	EAP-Pwd-Password-Prep			1222	byte
+
 #
 #	Range:	1280 - 1535
 #		EAP-type specific attributes
@@ -516,6 +538,11 @@ ATTRIBUTE	Tmp-Cast-IPv4Prefix			1870	ipv4prefix
 #	these attributes.
 #
 ATTRIBUTE	WiMAX-MN-NAI				1900	string
+ATTRIBUTE	WiMAX-SIM-Ki				1901	octets
+ATTRIBUTE	WiMAX-SIM-OPc				1902	octets
+ATTRIBUTE	WiMAX-SIM-AMF				1903	octets
+ATTRIBUTE	WiMAX-SIM-SQN				1904	octets
+ATTRIBUTE	WiMAX-SIM-RAND				1905	octets
 
 ATTRIBUTE	TLS-Cert-Serial				1910	string
 ATTRIBUTE	TLS-Cert-Expiration			1911	string
@@ -526,7 +553,7 @@ ATTRIBUTE	TLS-Cert-Subject-Alt-Name-Email		1915	string
 ATTRIBUTE	TLS-Cert-Subject-Alt-Name-Dns		1916	string
 ATTRIBUTE	TLS-Cert-Subject-Alt-Name-Upn		1917	string
 ATTRIBUTE	TLS-Cert-Valid-Since			1918	string
-# 1919: reserved for future cert attribute
+ATTRIBUTE	TLS-Session-Information			1919	string
 ATTRIBUTE	TLS-Client-Cert-Serial			1920	string
 ATTRIBUTE	TLS-Client-Cert-Expiration		1921	string
 ATTRIBUTE	TLS-Client-Cert-Issuer			1922	string
@@ -543,10 +570,19 @@ ATTRIBUTE	TLS-Client-Cert-Subject-Alt-Name-Upn	1932	string
 ATTRIBUTE	TLS-PSK-Identity			1933	string
 ATTRIBUTE	TLS-Client-Cert-X509v3-Extended-Key-Usage-OID 1936	string
 ATTRIBUTE	TLS-Client-Cert-Valid-Since		1937	string
+ATTRIBUTE	TLS-Cache-Method			1938	integer
+VALUE	TLS-Cache-Method		save			1
+VALUE	TLS-Cache-Method		load			2
+VALUE	TLS-Cache-Method		clear			3
+VALUE	TLS-Cache-Method		refresh			4
 
-# 1938 - 1939: reserved for future cert attributes
 
-# 1940 - 1949: reserved for TLS session caching, mostly in 4.0
+# 1939: reserved for future cert attributes
+
+# 1940 - 1959: reserved for TLS session caching, mostly in 4.0
+
+ATTRIBUTE	TLS-Session-ID				1940	octets
+ATTRIBUTE	TLS-Session-Data			1942	octets
 
 # Set by EAP-TLS code
 ATTRIBUTE	TLS-OCSP-Cert-Valid			1943	integer
@@ -560,8 +596,13 @@ ATTRIBUTE	TLS-Cache-Filename			1946	string
 ATTRIBUTE	TLS-Session-Version			1947	string
 ATTRIBUTE	TLS-Session-Cipher-Suite		1948	string
 
+ATTRIBUTE	TLS-Session-Cert-File			1949	string
+ATTRIBUTE	TLS-Session-Cert-Private-Key-File	1950	string
+
+ATTRIBUTE	TLS-Server-Name-Indication		1951	string
+
 #
-#	Range:	1950-2099
+#	Range:	1960-2099
 #		Free
 #
 #	Range:	2100-2199
@@ -650,6 +691,7 @@ VALUE	Session-Type			Local			1
 VALUE	Post-Auth-Type			Local			1
 VALUE	Post-Auth-Type			Reject			2
 VALUE	Post-Auth-Type			Challenge		3
+VALUE	Post-Auth-Type			Client-Lost		4
 
 #
 #	And Post-Proxy
diff --git a/src/include/build.h b/src/include/build.h
index 5da940c2b7..e1c2a1c79b 100644
--- a/src/include/build.h
+++ b/src/include/build.h
@@ -46,9 +46,13 @@ extern "C" {
  *	compiler.
  */
 #ifdef __GNUC__
-#  define CC_HINT(_x) __attribute__ ((_x))
+#  define CC_HINT(...)  __attribute__ ((__VA_ARGS__))
+#  define likely(_x)	__builtin_expect((_x), 1)
+#  define unlikely(_x)	__builtin_expect((_x), 0)
 #else
-#  define CC_HINT(_x)
+#  define CC_HINT(...)
+#  define likely(_x)	_x
+#  define unlikely(_x)	_x
 #endif
 
 #ifdef HAVE_ATTRIBUTE_BOUNDED
@@ -57,6 +61,18 @@ extern "C" {
 #  define CC_BOUNDED(...)
 #endif
 
+/*
+ *      GCC uses __SANITIZE_ADDRESS__, clang uses __has_feature, which
+ *      GCC complains about.
+ */
+#ifndef __SANITIZE_ADDRESS__
+#ifdef __has_feature
+#if __has_feature(address_sanitizer)
+#define __SANITIZE_ADDRESS__ (1)
+#endif
+#endif
+#endif
+
 /*
  *	Macros to add pragmas
  */
@@ -137,6 +153,11 @@ extern "C" {
 #  endif
 #endif
 
+#define PRINTF_LIKE(n)		CC_HINT(format(printf, n, n+1))
+#define NEVER_RETURNS		CC_HINT(noreturn)
+#define UNUSED			CC_HINT(unused)
+#define BLANK_FORMAT		" "	/* GCC_LINT whines about empty formats */
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/include/libradius.h b/src/include/libradius.h
index ce2f713de1..757828f070 100644
--- a/src/include/libradius.h
+++ b/src/include/libradius.h
@@ -57,7 +57,13 @@ RCSIDH(libradius_h, "$Id$")
  *  Talloc memory allocation is used in preference to malloc throughout
  *  the libraries and server.
  */
+#ifdef HAVE_WDOCUMENTATION
+DIAG_OFF(documentation)
+#endif
 #include <talloc.h>
+#ifdef HAVE_WDOCUMENTATION
+DIAG_ON(documentation)
+#endif
 
 /*
  *  Defines signatures for any missing functions.
@@ -162,11 +168,6 @@ typedef void (*sig_t)(int);
 
 #define PAD(_x, _y)		(_y - ((_x) % _y))
 
-#define PRINTF_LIKE(n)		CC_HINT(format(printf, n, n+1))
-#define NEVER_RETURNS		CC_HINT(noreturn)
-#define UNUSED			CC_HINT(unused)
-#define BLANK_FORMAT		" "	/* GCC_LINT whines about empty formats */
-
 typedef struct attr_flags {
 	unsigned int 	is_unknown : 1;				//!< Attribute number or vendor is unknown.
 	unsigned int	is_tlv : 1;				//!< Is a sub attribute.
@@ -189,6 +190,10 @@ typedef struct attr_flags {
 
 	unsigned int	compare : 1;				//!< has a paircompare registered
 
+	unsigned int	is_dup : 1;				//!< is a duplicate of another attribute
+
+	unsigned int	secret : 1;				//!< is a secret thingy
+
 	uint8_t		encrypt;      				//!< Ecryption method.
 	uint8_t		length;
 } ATTR_FLAGS;
@@ -443,7 +448,7 @@ size_t   	vp_prints_value(char *out, size_t outlen, VALUE_PAIR const *vp, char q
 
 char     	*vp_aprints_value(TALLOC_CTX *ctx, VALUE_PAIR const *vp, char quote);
 
-size_t    	vp_prints_value_json(char *out, size_t outlen, VALUE_PAIR const *vp);
+size_t    	vp_prints_value_json(char *out, size_t outlen, VALUE_PAIR const *vp, bool raw_value);
 size_t		vp_prints(char *out, size_t outlen, VALUE_PAIR const *vp);
 void		vp_print(FILE *, VALUE_PAIR const *);
 void		vp_printlist(FILE *, VALUE_PAIR const *);
@@ -472,6 +477,8 @@ int		dict_addvalue(char const *namestr, char const *attrstr, int value);
 int		dict_init(char const *dir, char const *fn);
 void		dict_free(void);
 int		dict_read(char const *dir, char const *filename);
+size_t		dict_print_oid(char *buffer, size_t buflen, DICT_ATTR const *da);
+int		dict_walk(fr_hash_table_walk_t callback, void *context);
 
 void 		dict_attr_free(DICT_ATTR const **da);
 int		dict_unknown_from_fields(DICT_ATTR *da, unsigned int attr, unsigned int vendor);
@@ -586,6 +593,7 @@ int		rad_vp2attr(RADIUS_PACKET const *packet,
 			    VALUE_PAIR const **pvp, uint8_t *ptr, size_t room);
 
 /* pair.c */
+VALUE_PAIR	*fr_pair_alloc(TALLOC_CTX *ctx);
 VALUE_PAIR	*fr_pair_afrom_da(TALLOC_CTX *ctx, DICT_ATTR const *da);
 VALUE_PAIR	*fr_pair_afrom_num(TALLOC_CTX *ctx, unsigned int attr, unsigned int vendor);
 int		fr_pair_to_unknown(VALUE_PAIR *vp);
@@ -611,6 +619,7 @@ VALUE_PAIR	*fr_cursor_remove(vp_cursor_t *cursor);
 VALUE_PAIR	*fr_cursor_replace(vp_cursor_t *cursor, VALUE_PAIR *new);
 void		fr_pair_delete_by_num(VALUE_PAIR **, unsigned int attr, unsigned int vendor, int8_t tag);
 void		fr_pair_add(VALUE_PAIR **, VALUE_PAIR *);
+void		fr_pair_prepend(VALUE_PAIR **, VALUE_PAIR *);
 void		fr_pair_replace(VALUE_PAIR **first, VALUE_PAIR *add);
 int		fr_pair_cmp(VALUE_PAIR *a, VALUE_PAIR *b);
 int		fr_pair_list_cmp(VALUE_PAIR *a, VALUE_PAIR *b);
@@ -632,7 +641,7 @@ void		fr_pair_value_strsteal(VALUE_PAIR *vp, char const *src);
 void		fr_pair_value_strcpy(VALUE_PAIR *vp, char const * src);
 void		fr_pair_value_bstrncpy(VALUE_PAIR *vp, void const * src, size_t len);
 void		fr_pair_value_sprintf(VALUE_PAIR *vp, char const * fmt, ...) CC_HINT(format (printf, 2, 3));
-void		fr_pair_list_move(TALLOC_CTX *ctx, VALUE_PAIR **to, VALUE_PAIR **from);
+void		fr_pair_list_move(TALLOC_CTX *ctx, VALUE_PAIR **to, VALUE_PAIR **from, FR_TOKEN op);
 void		fr_pair_list_move_by_num(TALLOC_CTX *ctx, VALUE_PAIR **to, VALUE_PAIR **from,
 					 unsigned int attr, unsigned int vendor, int8_t tag);
 void		fr_pair_list_mcopy_by_num(TALLOC_CTX *ctx, VALUE_PAIR **to, VALUE_PAIR **from,
diff --git a/src/include/listen.h b/src/include/listen.h
index 4f50bbf808..e8222a3f02 100644
--- a/src/include/listen.h
+++ b/src/include/listen.h
@@ -45,6 +45,8 @@ typedef enum RAD_LISTEN_TYPE {
 typedef enum RAD_LISTEN_STATUS {
 	RAD_LISTEN_STATUS_INIT = 0,
 	RAD_LISTEN_STATUS_KNOWN,
+	RAD_LISTEN_STATUS_PAUSE,
+	RAD_LISTEN_STATUS_RESUME,
 	RAD_LISTEN_STATUS_FROZEN,
 	RAD_LISTEN_STATUS_EOL,
 	RAD_LISTEN_STATUS_REMOVE_NOW
@@ -68,11 +70,12 @@ struct rad_listen {
 	int		fd;
 	char const	*server;
 	int		status;
-#ifdef WITH_TCP
 	int		count;
-	bool		dual;
+#ifdef WITH_TCP
 	rbtree_t	*children;
 	rad_listen_t	*parent;
+
+	bool		dual;
 #endif
 	bool		nodup;
 	bool		synchronous;
@@ -80,12 +83,25 @@ struct rad_listen {
 
 #ifdef WITH_TLS
 	fr_tls_server_conf_t *tls;
+	bool		check_client_connections;
 #endif
 
 	rad_listen_recv_t recv;
 	rad_listen_send_t send;
+
+	/*
+	 *	We don't need a proxy_recv, because the main loop in
+	 *	process.c calls listener->recv(), and we don't know
+	 *	what kind of packet we're receiving until we receive
+	 *	it.
+	 */
+	rad_listen_send_t proxy_send;
+
+
 	rad_listen_encode_t encode;
 	rad_listen_decode_t decode;
+	rad_listen_encode_t proxy_encode;
+	rad_listen_decode_t proxy_decode;
 	rad_listen_print_t print;
 
 	CONF_SECTION const *cs;
@@ -146,6 +162,12 @@ typedef struct listen_socket_t {
 	pthread_mutex_t mutex;
 	uint8_t		*data;
 	size_t		partial;
+	enum {
+		LISTEN_TLS_INIT = 0,
+		LISTEN_TLS_CHECKING,
+		LISTEN_TLS_SETUP,
+		LISTEN_TLS_RUNNING,
+	} state;
 #endif
 
 	RADCLIENT_LIST	*clients;
diff --git a/src/include/md4.h b/src/include/md4.h
index b7bdd6a15e..1492bd4a5c 100644
--- a/src/include/md4.h
+++ b/src/include/md4.h
@@ -26,6 +26,10 @@ RCSIDH(md4_h, "$Id$")
 
 #include <string.h>
 
+#ifdef WITH_FIPS
+#undef HAVE_OPENSSL_MD4_H
+#endif
+
 #ifdef HAVE_OPENSSL_MD4_H
 #  include <openssl/md4.h>
 #endif
@@ -71,14 +75,58 @@ void	fr_md4_final(uint8_t out[MD4_DIGEST_LENGTH], FR_MD4_CTX *ctx)
 void	fr_md4_transform(uint32_t buf[4], uint8_t const inc[MD4_BLOCK_LENGTH])
 	CC_BOUNDED(__size__, 1, 4, 4)
 	CC_BOUNDED(__minbytes__, 2, MD4_BLOCK_LENGTH);
+#  define fr_md4_destroy(_x)
 #else  /* HAVE_OPENSSL_MD4_H */
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
 USES_APPLE_DEPRECATED_API
 #  define FR_MD4_CTX		MD4_CTX
 #  define fr_md4_init		MD4_Init
 #  define fr_md4_update		MD4_Update
 #  define fr_md4_final		MD4_Final
 #  define fr_md4_transform	MD4_Transform
-#endif
+#  define fr_md4_destroy(_x)
+#else
+#include <openssl/evp.h>
+
+/*
+ *	Wrappers for OpenSSL3, so we don't have to butcher the rest of
+ *	the code too much.
+ */
+typedef struct FR_MD4_CTX {
+	EVP_MD_CTX	*ctx;
+	EVP_MD const   	*md;
+	unsigned int	len;
+} FR_MD4_CTX;
+
+static inline void fr_md4_init(FR_MD4_CTX *ctx)
+{
+	ctx->ctx = EVP_MD_CTX_new();
+//	ctx->md = EVP_MD_fetch(NULL, "MD4", "provider=legacy");
+	ctx->md = EVP_md4();
+	ctx->len = MD4_DIGEST_LENGTH;
+
+	EVP_MD_CTX_set_flags(ctx->ctx, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
+	EVP_DigestInit_ex(ctx->ctx, ctx->md, NULL);
+}
+
+static inline void fr_md4_update(FR_MD4_CTX *ctx, uint8_t const *in, size_t inlen)
+{
+        EVP_DigestUpdate(ctx->ctx, in, inlen);
+}
+
+static inline void fr_md4_final(uint8_t out[MD4_DIGEST_LENGTH], FR_MD4_CTX *ctx)
+{
+	EVP_DigestFinal_ex(ctx->ctx, out, &(ctx->len));
+}
+
+static inline void fr_md4_destroy(FR_MD4_CTX *ctx)
+{
+	EVP_MD_CTX_destroy(ctx->ctx);
+//	EVP_MD_free(ctx->md);
+}
+
+#endif	/* OPENSSL3 */
+#endif	/* HAVE_OPENSSL_MD4_H */
 
 /* md4.c */
 void fr_md4_calc(uint8_t out[MD4_DIGEST_LENGTH], uint8_t const *in, size_t inlen);
diff --git a/src/include/md5.h b/src/include/md5.h
index a44584564f..b7d571ac7b 100644
--- a/src/include/md5.h
+++ b/src/include/md5.h
@@ -26,6 +26,10 @@ RCSIDH(md5_h, "$Id$")
 
 #  include <string.h>
 
+#ifdef WITH_FIPS
+#undef HAVE_OPENSSL_MD5_H
+#endif
+
 #ifdef HAVE_OPENSSL_MD5_H
 #  include <openssl/md5.h>
 #endif
@@ -68,14 +72,41 @@ void	fr_md5_final(uint8_t out[MD5_DIGEST_LENGTH], FR_MD5_CTX *ctx)
 void	fr_md5_transform(uint32_t state[4], uint8_t const block[MD5_BLOCK_LENGTH])
 	CC_BOUNDED(__size__, 1, 4, 4)
 	CC_BOUNDED(__minbytes__, 2, MD5_BLOCK_LENGTH);
+#  define fr_md5_destroy(_x)
+#  define fr_md5_copy(_dst, _src) _dst = _src
 #else  /* HAVE_OPENSSL_MD5_H */
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
 USES_APPLE_DEPRECATED_API
 #  define FR_MD5_CTX		MD5_CTX
 #  define fr_md5_init		MD5_Init
 #  define fr_md5_update		MD5_Update
 #  define fr_md5_final		MD5_Final
 #  define fr_md5_transform	MD5_Transform
-#endif
+#  define fr_md5_copy(_dst, _src) _dst = _src
+#  define fr_md5_destroy(_x)
+#else
+#include <openssl/evp.h>
+
+/*
+ *	Wrappers for OpenSSL3, so we don't have to butcher the rest of
+ *	the code too much.
+ */
+typedef EVP_MD_CTX* FR_MD5_CTX;
+
+#  define fr_md5_init(_ctx) \
+	do { \
+		*_ctx = EVP_MD_CTX_new(); \
+		EVP_MD_CTX_set_flags(*_ctx, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW); \
+		EVP_DigestInit_ex(*_ctx, EVP_md5(), NULL); \
+	} while (0)
+#  define fr_md5_update(_ctx, _str, _len) \
+        EVP_DigestUpdate(*_ctx, _str, _len)
+#  define fr_md5_final(_out, _ctx) \
+	EVP_DigestFinal_ex(*_ctx, _out, NULL)
+#  define fr_md5_destroy(_ctx)	EVP_MD_CTX_destroy(*_ctx)
+#  define fr_md5_copy(_dst, _src) EVP_MD_CTX_copy_ex(_dst, _src)
+#endif	/* OPENSSL3 */
+#endif	/* HAVE_OPENSSL_MD5_H */
 
 /* hmac.c */
 void	fr_hmac_md5(uint8_t digest[MD5_DIGEST_LENGTH], uint8_t const *text, size_t text_len,
diff --git a/src/include/openssl3.h b/src/include/openssl3.h
new file mode 100644
index 0000000000..4423ee538a
--- /dev/null
+++ b/src/include/openssl3.h
@@ -0,0 +1,109 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program 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 General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+#ifndef FR_OPENSSL3_H
+#define FR_OPENSSL3_H
+/**
+ * $Id$
+ *
+ * @file openssl3.h
+ * @brief Wrappers to shut up OpenSSL3
+ *
+ * @copyright 2021 Network RADIUS SAS (legal@networkradius.com)
+ */
+
+RCSIDH(openssl3_h, "$Id$")
+
+/*
+ *	The HMAC APIs are deprecated in OpenSSL3.  We don't want to
+ *	fill the code with ifdef's, so we define some horrific
+ *	wrappers here.
+ *
+ *	This file should be included AFTER all OpenSSL header files.
+ */
+#ifdef HAVE_OPENSSL_SSL_H
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+#include <string.h>
+#include <openssl/evp.h>
+#include <openssl/core_names.h>
+
+typedef struct {
+	EVP_MAC		*mac;
+	EVP_MAC_CTX	*ctx;
+} HMAC3_CTX;
+#define HMAC_CTX HMAC3_CTX
+
+#define HMAC_CTX_new HMAC3_CTX_new
+static inline HMAC3_CTX *HMAC3_CTX_new(void)
+{
+	HMAC3_CTX *h = calloc(1, sizeof(*h));
+
+	return h;
+}
+
+#define HMAC_Init_ex(_ctx, _key, _keylen, _md, _engine) HMAC3_Init_ex(_ctx, _key, _keylen, _md, _engine)
+static inline int HMAC3_Init_ex(HMAC3_CTX *ctx, const unsigned char *key, unsigned int keylen, const EVP_MD *md, UNUSED void *engine)
+{
+	OSSL_PARAM params[2], *p = params;
+	char const *name;
+	char *unconst;
+
+	ctx->mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
+	if (!ctx->mac) return 0;
+
+	ctx->ctx = EVP_MAC_CTX_new(ctx->mac);
+	if (!ctx->ctx) return 0;
+
+	name = EVP_MD_get0_name(md);
+	memcpy(&unconst, &name, sizeof(name)); /* const issues */
+
+	p[0] = OSSL_PARAM_construct_utf8_string(OSSL_ALG_PARAM_DIGEST, unconst, 0);
+	p[1] = OSSL_PARAM_construct_end();
+
+	return EVP_MAC_init(ctx->ctx, key, keylen, params);
+}
+
+#define HMAC_Update HMAC3_Update
+static inline int HMAC3_Update(HMAC3_CTX *ctx, const unsigned char *data, unsigned int datalen)
+{
+	return EVP_MAC_update(ctx->ctx, data, datalen);
+}
+
+#define HMAC_Final HMAC3_Final
+static inline int HMAC3_Final(HMAC3_CTX *ctx, unsigned char *out, unsigned int *len)
+{
+	size_t mylen = *len;
+
+	if (!EVP_MAC_final(ctx->ctx, out, &mylen, mylen)) return 0;
+
+	*len = mylen;
+	return 1;
+}
+
+#define HMAC_CTX_free HMAC3_CTX_free
+static inline void HMAC3_CTX_free(HMAC3_CTX *ctx)
+{
+	if (!ctx) return;
+
+	EVP_MAC_free(ctx->mac);
+	EVP_MAC_CTX_free(ctx->ctx);
+	free(ctx);
+}
+
+#define HMAC_CTX_set_flags(_ctx, _flags)
+
+#endif	/* OPENSSL_VERSION_NUMBER */
+#endif
+#endif /* FR_OPENSSL3_H */
diff --git a/src/include/tls-h b/src/include/tls-h
index 62f57c4715..206f55db79 100644
--- a/src/include/tls-h
+++ b/src/include/tls-h
@@ -94,7 +94,7 @@ typedef struct _record_t {
 } record_t;
 
 typedef struct _tls_info_t {
-	int		origin;
+	int		origin;		// 0 - received (from peer), 1 - sending (to peer)
 	int		content_type;
 	uint8_t		handshake_type;
 	uint8_t		alert_level;
@@ -103,7 +103,6 @@ typedef struct _tls_info_t {
 
 	char 		info_description[256];
 	size_t		record_len;
-	int		version;
 } tls_info_t;
 
 #if OPENSSL_VERSION_NUMBER < 0x10001000L
@@ -139,6 +138,9 @@ typedef struct _tls_session_t {
 	bool		invalid_hb_used;		//!< Whether heartbleed attack was detected.
 	bool		connected;			//!< whether the outgoing socket is connected
 	bool		is_init_finished;		//!< whether or not init is finished
+	bool		client_cert_ok;			//!< whether or not we validated the client certificate
+	bool		authentication_success;		//!< whether or not the user was authenticated (cert or PW)
+	bool		quick_session_tickets;		//!< for EAP-TLS.
 
 	/*
 	 *	Framed-MTU attribute in RADIUS, if present, can also be used to set this
@@ -160,8 +162,11 @@ typedef struct _tls_session_t {
 	void 		*opaque;
 	void 		(*free_opaque)(void *opaque);
 
-	char const	*prf_label;
+	char const	*label;
 	bool		allow_session_resumption;	//!< Whether session resumption is allowed.
+	bool		session_not_resumed;		//!< Whether our session was not resumed.
+
+	fr_tls_server_conf_t const *conf;		//! for better complaints
 } tls_session_t;
 
 /*
@@ -309,16 +314,17 @@ int		tls_error_io_log(REQUEST *request, tls_session_t *session, int ret, char co
 				 CC_HINT(format (printf, 4, 5));
 
 void		tls_global_cleanup(void);
-tls_session_t	*tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQUEST *request, bool client_cert);
+tls_session_t	*tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQUEST *request, bool client_cert, bool allow_tls13);
 tls_session_t	*tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, int fd, VALUE_PAIR **certs);
 fr_tls_server_conf_t *tls_server_conf_parse(CONF_SECTION *cs);
 fr_tls_server_conf_t *tls_client_conf_parse(CONF_SECTION *cs);
 fr_tls_server_conf_t *tls_server_conf_alloc(TALLOC_CTX *ctx);
-SSL_CTX		*tls_init_ctx(fr_tls_server_conf_t *conf, int client);
+SSL_CTX		*tls_init_ctx(fr_tls_server_conf_t *conf, int client, char const *chain_file, char const *private_key_file);
 int 		tls_handshake_recv(REQUEST *, tls_session_t *ssn);
 int 		tls_handshake_send(REQUEST *, tls_session_t *ssn);
 void 		tls_session_information(tls_session_t *ssn);
 void		tls_session_id(SSL_SESSION *ssn, char *buffer, size_t bufsize);
+X509_STORE	*fr_init_x509_store(fr_tls_server_conf_t *conf);
 
 /*
  *	Low-level TLS stuff
@@ -360,6 +366,10 @@ struct fr_tls_server_conf_t {
 	bool		disable_tlsv1;
 	bool		disable_tlsv1_1;
 	bool		disable_tlsv1_2;
+	bool		disallow_untrusted;		//!< allow untrusted CAs to issue client certificates
+
+	int		min_version;
+	int		max_version;
 
 	char const	*tls_min_version;
 	char const	*tls_max_version;
@@ -371,16 +381,20 @@ struct fr_tls_server_conf_t {
 	bool		check_crl;
 	bool		check_all_crl;
 	bool		allow_expired_crl;
+	uint32_t	ca_path_reload_interval;
+	uint32_t	ca_path_last_reload;
+	X509_STORE	*old_x509_store;
 	char const	*check_cert_cn;
 	char const	*cipher_list;
 	bool		cipher_server_preference;
 	char const	*check_cert_issuer;
 
 	bool     	session_cache_enable;
-	uint32_t     	session_timeout;
+	uint32_t     	session_lifetime;
 	uint32_t     	session_cache_size;
 	char const	*session_id_name;
 	char const	*session_cache_path;
+	char const	*session_cache_server;
 	fr_hash_table_t *cache_ht;
 	char		session_context_id[SSL_MAX_SSL_SESSION_ID_LENGTH];
 
@@ -389,6 +403,8 @@ struct fr_tls_server_conf_t {
 	char const	*verify_client_cert_cmd;
 	bool		require_client_cert;
 
+	pthread_mutex_t	mutex;
+
 #ifdef HAVE_OPENSSL_OCSP_H
 	/*
 	 * OCSP Configuration
@@ -414,6 +430,10 @@ struct fr_tls_server_conf_t {
 	char const	*psk_query;
 #endif
 
+	char const	*realm_dir;
+	fr_hash_table_t	*realms;
+
+	char const	*client_hostname;
 };
 
 #ifdef __cplusplus
diff --git a/src/include/token.h b/src/include/token.h
index 6cbd05217a..c8bb748702 100644
--- a/src/include/token.h
+++ b/src/include/token.h
@@ -56,16 +56,17 @@ typedef enum fr_token_t {
 	T_OP_CMP_TRUE,			/* =* 		20 */
 	T_OP_CMP_FALSE,			/* !* */
 	T_OP_CMP_EQ,			/* == */
+	T_OP_PREPEND,			/* ^= */
 	T_HASH,				/* # */
-	T_BARE_WORD,			/* bare word */
-	T_DOUBLE_QUOTED_STRING,		/* "foo" 	25 */
+	T_BARE_WORD,			/* bare word    25 */
+	T_DOUBLE_QUOTED_STRING,		/* "foo" */
 	T_SINGLE_QUOTED_STRING,		/* 'foo' */
 	T_BACK_QUOTED_STRING,		/* `foo` */
 	T_TOKEN_LAST
 } FR_TOKEN;
 
 #define T_EQSTART	T_OP_ADD
-#define	T_EQEND		(T_OP_CMP_EQ + 1)
+#define	T_EQEND		(T_OP_PREPEND + 1)
 
 typedef struct FR_NAME_NUMBER {
 	char const	*name;
diff --git a/src/lib/dict.c b/src/lib/dict.c
index 96e06b5287..479bf1104e 100644
--- a/src/lib/dict.c
+++ b/src/lib/dict.c
@@ -725,7 +725,8 @@ int dict_addattr(char const *name, int attr, unsigned int vendor, PW_TYPE type,
 {
 	size_t namelen;
 	DICT_ATTR const	*parent;
-	DICT_ATTR *n;
+	DICT_ATTR	*n;
+	DICT_ATTR const *old;
 	static int      max_attr = 0;
 
 	namelen = strlen(name);
@@ -882,6 +883,8 @@ int dict_addattr(char const *name, int attr, unsigned int vendor, PW_TYPE type,
 		return -1;
 	}
 
+	if (flags.encrypt) flags.secret = 1;
+
 	if (flags.length && (type != PW_TYPE_OCTETS)) {
 		fr_strerror_printf("The \"length\" flag can only be set for attributes of type \"octets\"");
 		return -1;
@@ -1080,6 +1083,18 @@ int dict_addattr(char const *name, int attr, unsigned int vendor, PW_TYPE type,
 	n->type = type;
 	n->flags = flags;
 
+	/*
+	 *	Allow old-style names, but they always end up as
+	 *	new-style names.
+	 */
+	old = dict_attrbyvalue(n->attr, n->vendor);
+	if (old && (old->type == n->type)) {
+		DICT_ATTR *mutable;
+
+		memcpy(&mutable, &old, sizeof(old)); /* const issues */
+		mutable->flags.is_dup = true;
+	}
+
 	/*
 	 *	Insert the attribute, only if it's not a duplicate.
 	 */
@@ -1258,6 +1273,8 @@ int dict_addvalue(char const *namestr, char const *attrstr, int value)
 				   fr_int2str(dict_attr_types, da->type, "?Unknown?"));
 			return -1;
 		}
+		/* in v4 this is done with the UNCONST #define */
+		((DICT_ATTR *)((uintptr_t)(da)))->flags.has_value = 1;
 	} else {
 		value_fixup_t *fixup;
 
@@ -1742,6 +1759,10 @@ static int process_attribute(char const* fn, int const line,
 							   "\"encrypt=3\" flag set", fn, line);
 					return -1;
 				}
+				flags.secret = 1;
+
+			} else if (strncmp(key, "secret", 6) == 0) {
+				flags.secret = 1;
 
 			} else if (strncmp(key, "array", 6) == 0) {
 				flags.array = 1;
@@ -2022,7 +2043,7 @@ static int process_value_alias(char const* fn, int const line, char **argv,
 }
 
 
-static int parse_format(char const *fn, int line, char const *format, int *pvalue, int *ptype, int *plength, bool *pcontinuation)
+static int parse_format(char const *fn, int line, char const *format, int *ptype, int *plength, bool *pcontinuation)
 {
 	char const *p;
 	int type, length;
@@ -2075,9 +2096,8 @@ static int parse_format(char const *fn, int line, char const *format, int *pvalu
 		}
 		continuation = true;
 
-		if ((*pvalue != VENDORPEC_WIMAX) ||
-		    (type != 1) || (length != 1)) {
-			fr_strerror_printf("dict_init: %s[%d]: Only WiMAX VSAs can have continuations",
+		if ((type != 1) || (length != 1)) {
+			fr_strerror_printf("dict_init: %s[%d]: Only 'format=1,1' VSAs can have continuations",
 					   fn, line);
 			return -1;
 		}
@@ -2132,7 +2152,7 @@ static int process_vendor(char const* fn, int const line, char **argv,
 	 *	Look for a format statement.  Allow it to over-ride the hard-coded formats below.
 	 */
 	if (argc == 3) {
-		if (parse_format(fn, line, argv[2], &value, &type, &length, &continuation) < 0) {
+		if (parse_format(fn, line, argv[2], &type, &length, &continuation) < 0) {
 			return -1;
 		}
 
@@ -2409,7 +2429,8 @@ static int my_dict_init(char const *parent, char const *filename,
 		/*
 		 *	Optionally include a dictionary
 		 */
-		if (strcasecmp(argv[0], "$INCLUDE-") == 0) {
+		if ((strcasecmp(argv[0], "$INCLUDE-") == 0) ||
+		    (strcasecmp(argv[0], "$-INCLUDE") == 0)) {
 			int rcode = my_dict_init(dir, argv[1], fn, line);
 
 			if (rcode == -2) continue;
@@ -2783,45 +2804,72 @@ int dict_init(char const *dir, char const *fn)
 	return 0;
 }
 
-static size_t print_attr_oid(char *buffer, size_t size, unsigned int attr,
-			     int dv_type)
+static size_t print_attr_oid(char *buffer, size_t bufsize, unsigned int attr, unsigned int vendor)
 {
-	int nest;
-	size_t outlen;
+	int nest, dv_type = 1;
 	size_t len;
+	char *p = buffer;
+
+	if (vendor > FR_MAX_VENDOR) {
+		len = snprintf(p, bufsize, "%u.", vendor / FR_MAX_VENDOR);
+		p += len;
+		bufsize -= len;
+		vendor &= (FR_MAX_VENDOR) - 1;
+	}
+
+	if (vendor) {
+		DICT_VENDOR *dv;
+
+		/*
+		 *	dv_type is the length of the vendor's type field
+		 *	RFC 2865 never defined a mandatory length, so
+		 *	different vendors have different length type fields.
+		 */
+		dv = dict_vendorbyvalue(vendor);
+		if (dv) dv_type = dv->type;
+
+		len = snprintf(p, bufsize, "26.%u.", vendor);
+
+		p += len;
+		bufsize -= len;
+	}
+
 
 	switch (dv_type) {
 	default:
 	case 1:
-		len = snprintf(buffer, size, "%u", attr & 0xff);
+		len = snprintf(p, bufsize, "%u", attr & 0xff);
+		p += len;
+		bufsize -= len;
+		if ((attr >> 8) == 0) return p - buffer;
 		break;
 
-	case 4:
-		return snprintf(buffer, size, "%u", attr);
-
 	case 2:
-		return snprintf(buffer, size, "%u", attr & 0xffff);
-
-	}
+		len = snprintf(p, bufsize, "%u", attr & 0xffff);
+		p += len;
+		return p - buffer;
 
-	if ((attr >> 8) == 0) return len;
+	case 4:
+		len = snprintf(p, bufsize, "%u", attr);
+		p += len;
+		return p - buffer;
 
-	outlen = len;
-	buffer += len;
-	size -= len;
+	}
 
+	/*
+	 *	"attr" is a sequence of packed numbers.  Unpack them.
+	 */
 	for (nest = 1; nest <= fr_attr_max_tlv; nest++) {
 		if (((attr >> fr_attr_shift[nest]) & fr_attr_mask[nest]) == 0) break;
 
-		len = snprintf(buffer, size, ".%u",
+		len = snprintf(p, bufsize, ".%u",
 			       (attr >> fr_attr_shift[nest]) & fr_attr_mask[nest]);
 
-		outlen = len;
-		buffer += len;
-		size -= len;
+		p += len;
+		bufsize -= len;
 	}
 
-	return outlen;
+	return p - buffer;
 }
 
 /** Free dynamically allocated (unknown attributes)
@@ -2862,7 +2910,6 @@ void dict_attr_free(DICT_ATTR const **da)
 int dict_unknown_from_fields(DICT_ATTR *da, unsigned int attr, unsigned int vendor)
 {
 	char *p;
-	int dv_type = 1;
 	size_t len = 0;
 	size_t bufsize = DICT_ATTR_MAX_NAME_LEN;
 
@@ -2888,32 +2935,7 @@ int dict_unknown_from_fields(DICT_ATTR *da, unsigned int attr, unsigned int vend
 	p += len;
 	bufsize -= len;
 
-	if (vendor > FR_MAX_VENDOR) {
-		len = snprintf(p, bufsize, "%u.", vendor / FR_MAX_VENDOR);
-		p += len;
-		bufsize -= len;
-		vendor &= (FR_MAX_VENDOR) - 1;
-	}
-
-	if (vendor) {
-		DICT_VENDOR *dv;
-
-		/*
-		 *	dv_type is the length of the vendor's type field
-		 *	RFC 2865 never defined a mandatory length, so
-		 *	different vendors have different length type fields.
-		 */
-		dv = dict_vendorbyvalue(vendor);
-		if (dv) {
-			dv_type = dv->type;
-		}
-		len = snprintf(p, bufsize, "26.%u.", vendor);
-
-		p += len;
-		bufsize -= len;
-	}
-
-	print_attr_oid(p, bufsize , attr, dv_type);
+	print_attr_oid(p, bufsize , attr, vendor);
 
 	return 0;
 }
@@ -3270,7 +3292,15 @@ DICT_ATTR const *dict_attrbyname(char const *name)
 	da = (DICT_ATTR *) buffer;
 	strlcpy(da->name, name, DICT_ATTR_MAX_NAME_LEN + 1);
 
-	return fr_hash_table_finddata(attributes_byname, da);
+	da = fr_hash_table_finddata(attributes_byname, da);
+	if (!da) return NULL;
+
+	if (!da->flags.is_dup) return da;
+
+	/*
+	 *	This MUST exist if the dup flag is set.
+	 */
+	return dict_attrbyvalue(da->attr, da->vendor);
 }
 
 /** Look up a dictionary attribute by name embedded in another string
@@ -3464,3 +3494,13 @@ DICT_ATTR const *dict_unknown_add(DICT_ATTR const *old)
 	da = dict_attrbyvalue(old->attr, old->vendor);
 	return da;
 }
+
+size_t dict_print_oid(char *buffer, size_t buflen, DICT_ATTR const *da)
+{
+	return print_attr_oid(buffer, buflen, da->attr, da->vendor);
+}
+
+int dict_walk(fr_hash_table_walk_t callback, void *context)
+{
+	return fr_hash_table_walk(attributes_byname, callback, context);
+}
diff --git a/src/lib/hmacmd5.c b/src/lib/hmacmd5.c
index 1cca00fa2a..2aad490e30 100644
--- a/src/lib/hmacmd5.c
+++ b/src/lib/hmacmd5.c
@@ -34,8 +34,9 @@ RCSID("$Id$")
 
 #include <freeradius-devel/libradius.h>
 #include <freeradius-devel/md5.h>
+#include <freeradius-devel/openssl3.h>
 
-#ifdef HAVE_OPENSSL_EVP_H
+#if defined(HAVE_OPENSSL_EVP_H) && !defined(WITH_FIPS)
 /** Calculate HMAC using OpenSSL's MD5 implementation
  *
  * @param digest Caller digest to be filled in.
@@ -49,6 +50,7 @@ void fr_hmac_md5(uint8_t digest[MD5_DIGEST_LENGTH], uint8_t const *text, size_t
 		 uint8_t const *key, size_t key_len)
 {
 	HMAC_CTX *ctx  = HMAC_CTX_new();
+	unsigned int len = MD5_DIGEST_LENGTH;
 
 #ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW
 	/* Since MD5 is not allowed by FIPS, explicitly allow it. */
@@ -57,7 +59,7 @@ void fr_hmac_md5(uint8_t digest[MD5_DIGEST_LENGTH], uint8_t const *text, size_t
 
 	HMAC_Init_ex(ctx, key, key_len, EVP_md5(), NULL);
 	HMAC_Update(ctx, text, text_len);
-	HMAC_Final(ctx, digest, NULL);
+	HMAC_Final(ctx, digest, &len);
 	HMAC_CTX_free(ctx);
 }
 #else
diff --git a/src/lib/hmacsha1.c b/src/lib/hmacsha1.c
index 211470ea35..8711f983b7 100644
--- a/src/lib/hmacsha1.c
+++ b/src/lib/hmacsha1.c
@@ -13,6 +13,7 @@ RCSID("$Id$")
 #ifdef HAVE_OPENSSL_EVP_H
 #include <openssl/hmac.h>
 #include <openssl/evp.h>
+#include <freeradius-devel/openssl3.h>
 #endif
 
 #include <freeradius-devel/libradius.h>
@@ -35,9 +36,11 @@ void fr_hmac_sha1(uint8_t digest[SHA1_DIGEST_LENGTH], uint8_t const *text, size_
 		  uint8_t const *key, size_t key_len)
 {
 	HMAC_CTX *ctx  = HMAC_CTX_new();
+	unsigned int len = SHA1_DIGEST_LENGTH;
+
 	HMAC_Init_ex(ctx, key, key_len, EVP_sha1(), NULL);
 	HMAC_Update(ctx, text, text_len);
-	HMAC_Final(ctx, digest, NULL);
+	HMAC_Final(ctx, digest, &len);
 	HMAC_CTX_free(ctx);
 }
 
diff --git a/src/lib/md4.c b/src/lib/md4.c
index 0515fca1ae..7169000381 100644
--- a/src/lib/md4.c
+++ b/src/lib/md4.c
@@ -28,6 +28,7 @@ void fr_md4_calc(uint8_t out[MD4_DIGEST_LENGTH], uint8_t const *in, size_t inlen
 	fr_md4_init(&ctx);
 	fr_md4_update(&ctx, in, inlen);
 	fr_md4_final(out, &ctx);
+	fr_md4_destroy(&ctx);
 }
 
 #ifndef HAVE_OPENSSL_MD4_H
diff --git a/src/lib/md5.c b/src/lib/md5.c
index 9858175bd4..b5c1729148 100644
--- a/src/lib/md5.c
+++ b/src/lib/md5.c
@@ -30,6 +30,7 @@ void fr_md5_calc(uint8_t *out, uint8_t const *in, size_t inlen)
 	fr_md5_init(&ctx);
 	fr_md5_update(&ctx, in, inlen);
 	fr_md5_final(out, &ctx);
+	fr_md5_destroy(&ctx);
 }
 
 #ifndef HAVE_OPENSSL_MD5_H
diff --git a/src/lib/pair.c b/src/lib/pair.c
index d711f90c5d..146c82f95b 100644
--- a/src/lib/pair.c
+++ b/src/lib/pair.c
@@ -45,7 +45,7 @@ static int _fr_pair_free(VALUE_PAIR *vp) {
 	return 0;
 }
 
-static VALUE_PAIR *fr_pair_alloc(TALLOC_CTX *ctx)
+VALUE_PAIR *fr_pair_alloc(TALLOC_CTX *ctx)
 {
 	VALUE_PAIR *vp;
 
@@ -121,24 +121,7 @@ VALUE_PAIR *fr_pair_afrom_num(TALLOC_CTX *ctx, unsigned int attr, unsigned int v
 	DICT_ATTR const *da;
 
 	da = dict_attrbyvalue(attr, vendor);
-	if (!da) {
-		VALUE_PAIR *vp;
-
-		vp = fr_pair_alloc(ctx);
-		if (!vp) return NULL;
-
-		/*
-		 *	Ensure that the DA is parented by the VP.
-		 */
-		da = dict_unknown_afrom_fields(vp, attr, vendor);
-		if (!da) {
-			talloc_free(vp);
-			return NULL;
-		}
-
-		vp->da = da;
-		return vp;
-	}
+	if (!da) return NULL;
 
 	return fr_pair_afrom_da(ctx, da);
 }
@@ -286,6 +269,43 @@ void fr_pair_add(VALUE_PAIR **first, VALUE_PAIR *add)
 	i->next = add;
 }
 
+/** Add a VP to the start of the list.
+ *
+ * Links the new VP to 'first', then points 'first' at the new VP.
+ *
+ * @param[in] first VP in linked list. Will add new VP to the start of this list.
+ * @param[in] add VP to add to list.
+ */
+void fr_pair_prepend(VALUE_PAIR **first, VALUE_PAIR *add)
+{
+	VALUE_PAIR *i;
+
+	if (!add) return;
+
+	VERIFY_VP(add);
+
+	if (*first == NULL) {
+		*first = add;
+		return;
+	}
+
+	/*
+	 *	Find the end of the list to be prepended
+	 */
+	for (i = add; i->next; i = i->next) {
+#ifdef WITH_VERIFY_PTR
+		VERIFY_VP(i);
+		/*
+		 *	The same VP should never by added multiple times
+		 *	to the same list.
+		 */
+		fr_assert(*first != i);
+#endif
+	}
+	i->next = *first;
+	*first = add;
+}
+
 /** Replace all matching VPs
  *
  * Walks over 'first', and replaces the first VP that matches 'replace'.
@@ -851,13 +871,15 @@ void fr_pair_steal(TALLOC_CTX *ctx, VALUE_PAIR *vp)
  * @param[in] ctx for talloc
  * @param[in,out] to destination list.
  * @param[in,out] from source list.
+ * @param[in] op operator for move.
  *
  * @see radius_pairmove
  */
-void fr_pair_list_move(TALLOC_CTX *ctx, VALUE_PAIR **to, VALUE_PAIR **from)
+void fr_pair_list_move(TALLOC_CTX *ctx, VALUE_PAIR **to, VALUE_PAIR **from, FR_TOKEN op)
 {
 	VALUE_PAIR *i, *found;
 	VALUE_PAIR *head_new, **tail_new;
+	VALUE_PAIR *head_prepend;
 	VALUE_PAIR **tail_from;
 
 	if (!to || !from || !*from) return;
@@ -871,6 +893,12 @@ void fr_pair_list_move(TALLOC_CTX *ctx, VALUE_PAIR **to, VALUE_PAIR **from)
 	head_new = NULL;
 	tail_new = &head_new;
 
+	/*
+	 *	Any attributes that are requested to be prepended
+	 *	are added to a temporary list here
+	 */
+	head_prepend = NULL;
+
 	/*
 	 *	We're looping over the "from" list, moving some
 	 *	attributes out, but leaving others in place.
@@ -983,13 +1011,36 @@ void fr_pair_list_move(TALLOC_CTX *ctx, VALUE_PAIR **to, VALUE_PAIR **from)
 			fr_pair_steal(ctx, i);
 			tail_new = &(i->next);
 			continue;
+		case T_OP_PREPEND:
+			i->next = head_prepend;
+			head_prepend = i;
+			fr_pair_steal(ctx, i);
+			continue;
 		}
 	} /* loop over the "from" list. */
 
 	/*
-	 *	Take the "new" list, and append it to the "to" list.
+	 * 	If the op parameter was prepend, add the "new" list
+	 *	attributes first as those whose individual operator
+	 *	is prepend should be prepended to the resulting list
 	 */
-	fr_pair_add(to, head_new);
+	if (op == T_OP_PREPEND) {
+		fr_pair_prepend(to, head_new);
+	}
+
+	/*
+	 *	If there are any items in the prepend list prepend
+	 *	it to the "to" list
+	 */
+	fr_pair_prepend(to, head_prepend);
+
+	/*
+	 *	If the op parameter was not prepend, take the "new"
+	 *	list, and append it to the "to" list.
+	 */
+	if (op != T_OP_PREPEND) {
+		fr_pair_add(to, head_new);
+	}
 }
 
 /** Move matching pairs between VALUE_PAIR lists
@@ -1823,7 +1874,7 @@ FR_TOKEN fr_pair_raw_from_str(char const **ptr, VALUE_PAIR_RAW *raw)
 		break;
 
 	default:
-		fr_strerror_printf("Failed to find expected value on right hand side");
+		fr_strerror_printf("Failed to find expected value on right hand side in %s", raw->l_opand);
 		return T_INVALID;
 	}
 
diff --git a/src/lib/print.c b/src/lib/print.c
index 9ac927358b..57455b6f30 100644
--- a/src/lib/print.c
+++ b/src/lib/print.c
@@ -495,33 +495,28 @@ char *vp_aprints_type(TALLOC_CTX *ctx, PW_TYPE type)
  * @param out Where to write the string.
  * @param outlen Length of output buffer.
  * @param vp to print.
+ * @param raw_value if true, the raw value is printed and not the enumerated attribute value
  * @return the length of data written to out, or a value >= outlen on truncation.
  */
-size_t vp_prints_value_json(char *out, size_t outlen, VALUE_PAIR const *vp)
+size_t vp_prints_value_json(char *out, size_t outlen, VALUE_PAIR const *vp, bool raw_value)
 {
 	char const	*q;
 	size_t		len, freespace = outlen;
+	/* attempt to print raw_value when has_value is false, or raw_value is false, but only
+	   if has_tag is also false */
+	bool		raw = (raw_value || !vp->da->flags.has_value) && !vp->da->flags.has_tag;
 
-	if (!vp->da->flags.has_tag) {
+	if (raw) {
 		switch (vp->da->type) {
 		case PW_TYPE_INTEGER:
-			if (vp->da->flags.has_value) break;
-
 			return snprintf(out, freespace, "%u", vp->vp_integer);
 
 		case PW_TYPE_SHORT:
-			if (vp->da->flags.has_value) break;
-
 			return snprintf(out, freespace, "%u", (unsigned int) vp->vp_short);
 
 		case PW_TYPE_BYTE:
-			if (vp->da->flags.has_value) break;
-
 			return snprintf(out, freespace, "%u", (unsigned int) vp->vp_byte);
 
-		case PW_TYPE_SIGNED:
-			return snprintf(out, freespace, "%d", vp->vp_signed);
-
 		default:
 			break;
 		}
@@ -775,7 +770,7 @@ char *vp_aprints(TALLOC_CTX *ctx, VALUE_PAIR const *vp, char quote)
 
 	value = vp_aprints_value(ctx, vp, quote);
 
-	if (vp->da->flags.has_tag) {
+	if (vp->da->flags.has_tag && (vp->tag != TAG_ANY)) {
 		if (quote && (vp->da->type == PW_TYPE_STRING)) {
 			str = talloc_asprintf(ctx, "%s:%d %s %c%s%c", vp->da->name, vp->tag, token, quote, value, quote);
 		} else {
diff --git a/src/lib/radius.c b/src/lib/radius.c
index 3881111f7d..524e68088e 100644
--- a/src/lib/radius.c
+++ b/src/lib/radius.c
@@ -28,6 +28,7 @@ RCSID("$Id$")
 #include	<freeradius-devel/libradius.h>
 
 #include	<freeradius-devel/md5.h>
+#include	<freeradius-devel/rfc4849.h>
 
 #include	<fcntl.h>
 #include	<ctype.h>
@@ -528,6 +529,8 @@ static void make_secret(uint8_t *digest, uint8_t const *vector,
 	for ( i = 0; i < length; i++ ) {
 		digest[i] ^= value[i];
 	}
+
+	fr_md5_destroy(&context);
 }
 
 #define MAX_PASS_LEN (128)
@@ -562,8 +565,9 @@ static void make_passwd(uint8_t *output, ssize_t *outlen,
 	*outlen = len;
 
 	fr_md5_init(&context);
+	fr_md5_init(&old);
 	fr_md5_update(&context, (uint8_t const *) secret, strlen(secret));
-	old = context;
+	fr_md5_copy(old, context);
 
 	/*
 	 *	Do first pass.
@@ -572,7 +576,7 @@ static void make_passwd(uint8_t *output, ssize_t *outlen,
 
 	for (n = 0; n < len; n += AUTH_PASS_LEN) {
 		if (n > 0) {
-			context = old;
+			fr_md5_copy(context, old);
 			fr_md5_update(&context,
 				       passwd + n - AUTH_PASS_LEN,
 				       AUTH_PASS_LEN);
@@ -585,6 +589,9 @@ static void make_passwd(uint8_t *output, ssize_t *outlen,
 	}
 
 	memcpy(output, passwd, len);
+
+	fr_md5_destroy(&old);
+	fr_md5_destroy(&context);
 }
 
 
@@ -653,8 +660,9 @@ static void make_tunnel_passwd(uint8_t *output, ssize_t *outlen,
 	output[2] = inlen;	/* length of the password string */
 
 	fr_md5_init(&context);
+	fr_md5_init(&old);
 	fr_md5_update(&context, (uint8_t const *) secret, strlen(secret));
-	old = context;
+	fr_md5_copy(old, context);
 
 	fr_md5_update(&context, vector, AUTH_VECTOR_LEN);
 	fr_md5_update(&context, &output[0], 2);
@@ -663,7 +671,7 @@ static void make_tunnel_passwd(uint8_t *output, ssize_t *outlen,
 		size_t block_len;
 
 		if (n > 0) {
-			context = old;
+			fr_md5_copy(context, old);
 			fr_md5_update(&context,
 				       output + 2 + n - AUTH_PASS_LEN,
 				       AUTH_PASS_LEN);
@@ -681,6 +689,8 @@ static void make_tunnel_passwd(uint8_t *output, ssize_t *outlen,
 			output[i + 2 + n] ^= digest[i];
 		}
 	}
+	fr_md5_destroy(&old);
+	fr_md5_destroy(&context);
 }
 
 static int do_next_tlv(VALUE_PAIR const *vp, VALUE_PAIR const *next, int nest)
@@ -1552,6 +1562,8 @@ int rad_vp2rfc(RADIUS_PACKET const *packet,
 
 	VERIFY_VP(vp);
 
+	if (room < 2) return -1;
+
 	if (vp->da->vendor != 0) {
 		fr_strerror_printf("rad_vp2rfc called with VSA");
 		return -1;
@@ -1594,6 +1606,88 @@ int rad_vp2rfc(RADIUS_PACKET const *packet,
 		return 18;
 	}
 
+	/*
+	 *	Hacks for NAS-Filter-Rule.  They all get concatenated
+	 *	with 0x00 bytes in between the values.  We rely on the
+	 *	decoder to do the opposite transformation on incoming
+	 *	packets.
+	 */
+	if (vp->da->attr == PW_NAS_FILTER_RULE) {
+		uint8_t const *end = ptr + room;
+		uint8_t *p, *attr = ptr;
+		bool zero = false;
+
+		attr[0] = PW_NAS_FILTER_RULE;
+		attr[1] = 2;
+		p = ptr + 2;
+
+		while (vp && !vp->da->vendor && (vp->da->attr == PW_NAS_FILTER_RULE)) {
+			if ((p + zero + vp->vp_length) > end) {				
+				break;
+			}
+
+			if (zero) {
+				if (attr[1] == 255) {
+					attr = p;
+					if ((attr + 3) >= end) break;
+
+					attr[0] = PW_NAS_FILTER_RULE;
+					attr[1] = 2;
+					p = attr + 2;
+				}
+
+				*(p++) = 0;
+				attr[1]++;
+			}
+
+			/*
+			 *	Check for overflow
+			 */
+			if ((attr[1] + vp->vp_length) < 255) {
+				memcpy(p, vp->vp_strvalue, vp->vp_length);
+				attr[1] += vp->vp_length;
+				p += vp->vp_length;
+
+			} else if (attr + (attr[1] + 2 + vp->vp_length) > end) {
+				break;
+
+			} else if (vp->vp_length > 253) {
+				/*
+				 *	Drop VPs which are too long.
+				 *	We don't (yet) split one VP
+				 *	across multiple attributes.
+				 */
+				vp = vp->next;
+				continue;
+
+			} else {
+				size_t first, second;
+
+				first = 255 - attr[1];
+				second = vp->vp_length - first;
+
+				memcpy(p, vp->vp_strvalue, first);
+				p += first;
+				attr[1] = 255;
+				attr = p;
+
+				attr[0] = PW_NAS_FILTER_RULE;
+				attr[1] = 2;
+				p = attr + 2;
+
+				memcpy(p, vp->vp_strvalue + first, second);
+				attr[1] += second;
+				p += second;
+			}
+
+			vp = vp->next;
+			zero = true;
+		}
+
+		*pvp = vp;
+		return p - ptr;
+	}
+
 	/*
 	 *	EAP-Message is special.
 	 */
@@ -2036,6 +2130,7 @@ int rad_sign(RADIUS_PACKET *packet, RADIUS_PACKET const *original,
 			fr_md5_update(&context, (uint8_t const *) secret,
 				     strlen(secret));
 			fr_md5_final(digest, &context);
+			fr_md5_destroy(&context);
 
 			memcpy(hdr->vector, digest, AUTH_VECTOR_LEN);
 			memcpy(packet->vector, digest, AUTH_VECTOR_LEN);
@@ -2159,6 +2254,7 @@ static int calc_acctdigest(RADIUS_PACKET *packet, char const *secret)
 	fr_md5_update(&context, packet->data, packet->data_len);
 	fr_md5_update(&context, (uint8_t const *) secret, strlen(secret));
 	fr_md5_final(digest, &context);
+	fr_md5_destroy(&context);
 
 	/*
 	 *	Return 0 if OK, 2 if not OK.
@@ -2198,6 +2294,7 @@ static int calc_replydigest(RADIUS_PACKET *packet, RADIUS_PACKET *original,
 	fr_md5_update(&context, packet->data, packet->data_len);
 	fr_md5_update(&context, (uint8_t const *) secret, strlen(secret));
 	fr_md5_final(calc_digest, &context);
+	fr_md5_destroy(&context);
 
 	/*
 	 *  Copy the packet's vector back to the packet.
@@ -2896,6 +2993,115 @@ int rad_verify(RADIUS_PACKET *packet, RADIUS_PACKET *original, char const *secre
 }
 
 
+/** Convert one or more NAS-Filter-Rule attributes to one or more
+ * attributes.
+ *
+ */
+static ssize_t data2vp_nas_filter_rule(TALLOC_CTX *ctx,
+				       DICT_ATTR const *da, uint8_t const *start,
+				       size_t const packetlen, VALUE_PAIR **pvp)
+{
+	uint8_t const *p = start;
+	uint8_t const *attr = start;
+	uint8_t const *end = start + packetlen;
+	uint8_t const *attr_end;
+	uint8_t *q;
+	VALUE_PAIR *vp;
+	uint8_t buffer[253];
+
+	q = buffer;
+
+	/*
+	 *	The packet has already been sanity checked, so we
+	 *	don't care about walking off of the end of it.
+	 */
+	while (attr < end) {
+		if ((attr + 2) > end) {
+			fr_strerror_printf("decode NAS-Filter-Rule: Failure (1) to call rad_packet_ok");
+			return -1;
+		}
+
+		if (attr[1] < 2) {
+			fr_strerror_printf("decode NAS-Filter-Rule: Failure (2) to call rad_packet_ok");
+			return -1;
+		}
+		if (attr[0] != PW_NAS_FILTER_RULE) break;
+
+		/*
+		 *	Now decode one, or part of one rule.
+		 */
+		p = attr + 2;
+		attr_end = attr + attr[1];
+
+		if (attr_end > end) {
+			fr_strerror_printf("decode NAS-Filter-Rule: Failure (3) to call rad_packet_ok");
+			return -1;
+		}
+
+		/*
+		 *	Coalesce data until the zero byte.
+		 */
+		while (p < attr_end) {
+			/*
+			 *	Once we hit the zero byte, create the
+			 *	VP, skip the zero byte, and reset the
+			 *	counters.
+			 */
+			if (*p == 0) {
+				/*
+				 *	Discard consecutive zeroes.
+				 */
+				if (q > buffer) {
+					vp = fr_pair_afrom_da(ctx, da);
+					if (!vp) {
+						fr_strerror_printf("decode NAS-Filter-Rule: Out of memory");
+						return -1;
+					}
+
+					fr_pair_value_bstrncpy(vp, buffer, q - buffer);
+
+					*pvp = vp;
+					pvp = &(vp->next);
+					q = buffer;
+				}
+
+				p++;
+				continue;
+			}
+			*(q++) = *(p++);
+
+			/*
+			 *	Not much reason to have rules which
+			 *	are too long.
+			 */
+			if ((size_t) (q - buffer) > sizeof(buffer)) {
+				fr_strerror_printf("decode NAS-Filter-Rule: decoded attribute is too long");
+				return -1;
+			}
+		}
+
+		/*
+		 *	Done this attribute.  There MAY be things left
+		 *	in the buffer.
+		 */
+		attr = attr_end;
+	}
+
+	if (q == buffer) return attr + attr[2] - start;
+
+	vp = fr_pair_afrom_da(ctx, da);
+	if (!vp) {
+		fr_strerror_printf("decode NAS-Filter-Rule: Out of memory");
+		return -1;
+	}
+				
+	fr_pair_value_bstrncpy(vp, buffer, q - buffer);
+
+	*pvp = vp;
+
+	return p - start;
+}
+
 /** Convert a "concatenated" attribute to one long VP
  *
  */
@@ -3503,7 +3709,7 @@ static ssize_t data2vp_vsas(TALLOC_CTX *ctx, RADIUS_PACKET *packet,
 	/*
 	 *	WiMAX craziness
 	 */
-	if ((vendor == VENDORPEC_WIMAX) && dv->flags) {
+	if (dv->flags) {
 		rcode = data2vp_wimax(ctx, packet, original, secret, vendor,
 				      data, attrlen, packetlen, pvp);
 		return rcode;
@@ -3610,19 +3816,12 @@ ssize_t data2vp(TALLOC_CTX *ctx,
 			return 0;
 		}
 
-#if !defined(NDEBUG) || defined(__clang_analyzer__)
 		/*
-		 *	Hacks for Coverity.  Editing the dictionary
-		 *	will break assumptions about CUI.  We know
-		 *	this, but Coverity doesn't.
+		 *	Create a zero-length attribute.
 		 */
-		if (da->type != PW_TYPE_OCTETS) return -1;
-#endif
-
-		data = buffer;
-		*buffer = '\0';
-		datalen = 0;
-		goto alloc_cui;	/* skip everything */
+		vp = fr_pair_afrom_da(ctx, da);
+		if (!vp) return -1;
+		goto done;
 	}
 
 	/*
@@ -3926,44 +4125,44 @@ ssize_t data2vp(TALLOC_CTX *ctx,
 
 	default:
 	raw:
+		/*
+		 *	If it's already unknown, don't create a new
+		 *	unknown one.
+		 */
+		if (da->flags.is_unknown) break;
+
 		/*
 		 *	Re-write the attribute to be "raw".  It is
 		 *	therefore of type "octets", and will be
 		 *	handled below.
+		 *
+		 *      We allocate the VP *first*, and then the da
+		 *      from it, so that there are no memory leaks.
 		 */
-		da = dict_unknown_afrom_fields(ctx, da->attr, da->vendor);
+		vp = fr_pair_alloc(ctx);
+		if (!vp) return -1;
+
+		da = dict_unknown_afrom_fields(vp, da->attr, da->vendor);
 		if (!da) {
 			fr_strerror_printf("Internal sanity check %d", __LINE__);
 			return -1;
 		}
 		tag = TAG_NONE;
-#ifndef NDEBUG
-		/*
-		 *	Fix for Coverity.
-		 */
-		if (da->type != PW_TYPE_OCTETS) {
-			dict_attr_free(&da);
-			return -1;
-		}
-#endif
-		break;
+		vp->da = da;
+		goto alloc_raw;
 	}
 
 	/*
 	 *	And now that we've verified the basic type
 	 *	information, decode the actual data.
 	 */
- alloc_cui:
 	vp = fr_pair_afrom_da(ctx, da);
 	if (!vp) return -1;
 
+alloc_raw:
 	vp->vp_length = datalen;
 	vp->tag = tag;
 
-#ifdef __clang_analyzer__
-	if (!datalen && da->type != PW_TYPE_OCTETS) return -1;
-#endif
-
 	switch (da->type) {
 	case PW_TYPE_STRING:
 		p = talloc_array(vp, char, vp->vp_length + 1);
@@ -4072,6 +4271,8 @@ ssize_t data2vp(TALLOC_CTX *ctx,
 		fr_strerror_printf("Internal sanity check %d", __LINE__);
 		return -1;
 	}
+
+done:
 	vp->type = VT_DATA;
 	*pvp = vp;
 
@@ -4112,6 +4313,11 @@ ssize_t rad_attr2vp(TALLOC_CTX *ctx,
 		return data2vp_concat(ctx, da, data, length, pvp);
 	}
 
+	if (!da->vendor && (da->attr == PW_NAS_FILTER_RULE)) {
+		VP_TRACE("attr2vp: NAS-Filter-Rule attribute\n");
+		return data2vp_nas_filter_rule(ctx, da, data, length, pvp);
+	}
+
 	/*
 	 *	Note that we pass the entire length, not just the
 	 *	length of this attribute.  The Extended or WiMAX
@@ -4261,7 +4467,7 @@ int rad_decode(RADIUS_PACKET *packet, RADIUS_PACKET *original,
 	uint32_t		num_attributes;
 	uint8_t			*ptr;
 	radius_packet_t		*hdr;
-	VALUE_PAIR *head, **tail, *vp;
+	VALUE_PAIR *head, **tail, *vp = NULL;
 
 	/*
 	 *	Extract attribute-value pairs
@@ -4385,8 +4591,9 @@ int rad_pwencode(char *passwd, size_t *pwlen, char const *secret,
 	secretlen = strlen(secret);
 
 	fr_md5_init(&context);
+	fr_md5_init(&old);
 	fr_md5_update(&context, (uint8_t const *) secret, secretlen);
-	old = context;		/* save intermediate work */
+	fr_md5_copy(old, context);		/* save intermediate work */
 
 	/*
 	 *	Encrypt it in place.  Don't bother checking
@@ -4397,7 +4604,7 @@ int rad_pwencode(char *passwd, size_t *pwlen, char const *secret,
 			fr_md5_update(&context, vector, AUTH_PASS_LEN);
 			fr_md5_final(digest, &context);
 		} else {
-			context = old;
+			fr_md5_copy(context, old);
 			fr_md5_update(&context,
 				     (uint8_t *) passwd + n - AUTH_PASS_LEN,
 				     AUTH_PASS_LEN);
@@ -4409,6 +4616,9 @@ int rad_pwencode(char *passwd, size_t *pwlen, char const *secret,
 		}
 	}
 
+	fr_md5_destroy(&old);
+	fr_md5_destroy(&context);
+
 	return 0;
 }
 
@@ -4441,8 +4651,9 @@ int rad_pwdecode(char *passwd, size_t pwlen, char const *secret,
 	secretlen = strlen(secret);
 
 	fr_md5_init(&context);
+	fr_md5_init(&old);
 	fr_md5_update(&context, (uint8_t const *) secret, secretlen);
-	old = context;		/* save intermediate work */
+	fr_md5_copy(old, context);		/* save intermediate work */
 
 	/*
 	 *	The inverse of the code above.
@@ -4452,7 +4663,7 @@ int rad_pwdecode(char *passwd, size_t pwlen, char const *secret,
 			fr_md5_update(&context, vector, AUTH_VECTOR_LEN);
 			fr_md5_final(digest, &context);
 
-			context = old;
+			fr_md5_copy(context, old);
 			if (pwlen > AUTH_PASS_LEN) {
 				fr_md5_update(&context, (uint8_t *) passwd,
 					     AUTH_PASS_LEN);
@@ -4460,7 +4671,7 @@ int rad_pwdecode(char *passwd, size_t pwlen, char const *secret,
 		} else {
 			fr_md5_final(digest, &context);
 
-			context = old;
+			fr_md5_copy(context, old);
 			if (pwlen > (n + AUTH_PASS_LEN)) {
 				fr_md5_update(&context, (uint8_t *) passwd + n,
 					     AUTH_PASS_LEN);
@@ -4473,6 +4684,9 @@ int rad_pwdecode(char *passwd, size_t pwlen, char const *secret,
 	}
 
  done:
+	fr_md5_destroy(&old);
+	fr_md5_destroy(&context);
+
 	passwd[pwlen] = '\0';
 	return strlen(passwd);
 }
@@ -4609,8 +4823,9 @@ ssize_t rad_tunnel_pwdecode(uint8_t *passwd, size_t *pwlen, char const *secret,
 	secretlen = strlen(secret);
 
 	fr_md5_init(&context);
+	fr_md5_init(&old);
 	fr_md5_update(&context, (uint8_t const *) secret, secretlen);
-	old = context;		/* save intermediate work */
+	fr_md5_copy(old, context);		/* save intermediate work */
 
 	/*
 	 *	Set up the initial key:
@@ -4637,7 +4852,7 @@ ssize_t rad_tunnel_pwdecode(uint8_t *passwd, size_t *pwlen, char const *secret,
 
 			fr_md5_final(digest, &context);
 
-			context = old;
+			fr_md5_copy(context, old);
 
 			/*
 			 *	A quick check: decrypt the first octet
@@ -4657,7 +4872,7 @@ ssize_t rad_tunnel_pwdecode(uint8_t *passwd, size_t *pwlen, char const *secret,
 
 			fr_md5_final(digest, &context);
 
-			context = old;
+			fr_md5_copy(context, old);
 			fr_md5_update(&context, passwd + n + 2, block_len);
 		}
 
@@ -4669,6 +4884,9 @@ ssize_t rad_tunnel_pwdecode(uint8_t *passwd, size_t *pwlen, char const *secret,
 	*pwlen = reallen;
 	passwd[reallen] = 0;
 
+	fr_md5_destroy(&old);
+	fr_md5_destroy(&context);
+
 	return reallen;
 }
 
diff --git a/src/lib/token.c b/src/lib/token.c
index 8ae41b392f..a7978622e6 100644
--- a/src/lib/token.c
+++ b/src/lib/token.c
@@ -42,6 +42,7 @@ const FR_NAME_NUMBER fr_tokens[] = {
 	{ "=*", T_OP_CMP_TRUE,  },
 	{ "!*", T_OP_CMP_FALSE, },
 	{ "==",	T_OP_CMP_EQ,	},
+	{ "^=", T_OP_PREPEND,	},
 	{ "=",	T_OP_EQ,	},
 	{ "!=",	T_OP_NE,	},
 	{ ">=",	T_OP_GE,	},
@@ -78,9 +79,10 @@ const bool fr_assignment_op[] = {
 	false,		/* =* 		20 */
 	false,		/* !* */
 	false,		/* == */
-	false,				/* # */
-	false,		/* bare word */
-	false,		/* "foo" 	25 */
+	true,		/* ^= */
+	false,		/* # */
+	false,		/* bare word 	25 */
+	false,		/* "foo" */
 	false,		/* 'foo' */
 	false,		/* `foo` */
 	false
@@ -108,12 +110,13 @@ const bool fr_equality_op[] = {
 	true,		/* < */
 	true,		/* =~ */
 	true,		/* !~ */
-	true,		/* =* 		20 */
+	true,		/* =*		20 */
 	true,		/* !* */
 	true,		/* == */
-	false,				/* # */
-	false,		/* bare word */
-	false,		/* "foo" 	25 */
+	false,		/* ^= */
+	false,		/* # */
+	false,		/* bare word 	25 */
+	false,		/* "foo" */
 	false,		/* 'foo' */
 	false,		/* `foo` */
 	false
@@ -144,9 +147,10 @@ const bool fr_str_tok[] = {
 	false,		/* =* 		20 */
 	false,		/* !* */
 	false,		/* == */
-	false,				/* # */
-	true,		/* bare word */
-	true,		/* "foo" 	25 */
+	false,		/* ^= */
+	false,		/* # */
+	true,		/* bare word 	25 */
+	true,		/* "foo" */
 	true,		/* 'foo' */
 	true,		/* `foo` */
 	false
diff --git a/src/main/cb.c b/src/main/cb.c
index 4ae14e575b..f8b2edbecc 100644
--- a/src/main/cb.c
+++ b/src/main/cb.c
@@ -29,45 +29,94 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 #ifdef WITH_TLS
 void cbtls_info(SSL const *s, int where, int ret)
 {
-	char const *str, *state;
+	char const *role, *state;
 	REQUEST *request = SSL_get_ex_data(s, FR_TLS_EX_INDEX_REQUEST);
 
 	if ((where & ~SSL_ST_MASK) & SSL_ST_CONNECT) {
-		str="TLS_connect";
+		role = "Client ";
 	} else if (((where & ~SSL_ST_MASK)) & SSL_ST_ACCEPT) {
-		str="TLS_accept";
+		role = "Server ";
 	} else {
-		str="(other)";
+		role = "";
 	}
 
 	state = SSL_state_string_long(s);
 	state = state ? state : "<none>";
 
 	if ((where & SSL_CB_LOOP) || (where & SSL_CB_HANDSHAKE_START) || (where & SSL_CB_HANDSHAKE_DONE)) {
-		RDEBUG2("%s: %s", str, state);
+		if (RDEBUG_ENABLED3) {
+			char const *abbrv = SSL_state_string(s);
+			size_t len;
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+			STACK_OF(SSL_CIPHER) *client_ciphers;
+			STACK_OF(SSL_CIPHER) *server_ciphers;
+#endif
+
+			/*
+			 *	Trim crappy OpenSSL state strings...
+			 */
+			len = strlen(abbrv);
+			if ((len > 1) && (abbrv[len - 1] == ' ')) len--;
+
+			RDEBUG3("(TLS) Handshake state [%.*s] - %s%s (%d)",
+				(int)len, abbrv, role, state, SSL_get_state(s));
+
+			/*
+			 *	After a ClientHello, list all the proposed ciphers from the client
+			 */
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+			if (SSL_get_state(s) == TLS_ST_SR_CLNT_HELLO) {
+				int i;
+				int num_ciphers;
+				const SSL_CIPHER *this_cipher;
+
+				server_ciphers = SSL_get_ciphers(s);
+				if (server_ciphers) {
+					RDEBUG3("Server preferred ciphers (by priority)");
+					num_ciphers = sk_SSL_CIPHER_num(server_ciphers);
+					for (i = 0; i < num_ciphers; i++) {
+						this_cipher = sk_SSL_CIPHER_value(server_ciphers, i);
+						RDEBUG3("(TLS)    [%i] %s", i, SSL_CIPHER_get_name(this_cipher));
+					}
+				}
+	
+				client_ciphers = SSL_get_client_ciphers(s);
+				if (client_ciphers) {
+					RDEBUG3("Client preferred ciphers (by priority)");
+					num_ciphers = sk_SSL_CIPHER_num(client_ciphers);
+					for (i = 0; i < num_ciphers; i++) {
+						this_cipher = sk_SSL_CIPHER_value(client_ciphers, i);
+						RDEBUG3("(TLS)    [%i] %s", i, SSL_CIPHER_get_name(this_cipher));
+					}
+				}
+			}
+#endif
+		} else {
+			RDEBUG2("(TLS) Handshake state - %s%s", role, state);
+		}
 		return;
 	}
 
 	if (where & SSL_CB_ALERT) {
 		if ((ret & 0xff) == SSL_AD_CLOSE_NOTIFY) return;
 
-		RERROR("TLS Alert %s:%s:%s", (where & SSL_CB_READ) ? "read": "write",
+		RERROR("(TLS) Alert %s:%s:%s", (where & SSL_CB_READ) ? "read": "write",
 		       SSL_alert_type_string_long(ret), SSL_alert_desc_string_long(ret));
 		return;
 	}
 
 	if (where & SSL_CB_EXIT) {
 		if (ret == 0) {
-			RERROR("%s: Failed in %s", str, state);
+			RERROR("(TLS) %s: Failed in %s", role, state);
 			return;
 		}
 
 		if (ret < 0) {
 			if (SSL_want_read(s)) {
-				RDEBUG2("%s: Need to read more data: %s", str, state);
+				RDEBUG2("(TLS) %s: Need to read more data: %s", role, state);
 				return;
 			}
-			ERROR("tls: %s: Error in %s", str, state);
+			RERROR("(TLS) %s: Error in %s", role, state);
 		}
 	}
 }
@@ -87,14 +136,24 @@ void cbtls_msg(int write_p, int msg_version, int content_type,
 	 *	content types.  Which breaks our tracking of
 	 *	the SSL Session state.
 	 */
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
 	if ((msg_version == 0) && (content_type > UINT8_MAX)) {
-		DEBUG4("Ignoring cbtls_msg call with pseudo content type %i, version %i",
+#else
+	/*
+         *      "...we do not see the need to resolve application breakage
+         *      just because the documentation now is incorrect."
+         *
+         *      https://github.com/openssl/openssl/issues/17262
+	 */
+	if ((content_type > UINT8_MAX) && (content_type != SSL3_RT_INNER_CONTENT_TYPE)) {
+#endif
+		DEBUG4("(TLS) Ignoring cbtls_msg call with pseudo content type %i, version %i",
 		       content_type, msg_version);
 		return;
 	}
 
 	if ((write_p != 0) && (write_p != 1)) {
-		DEBUG4("Ignoring cbtls_msg call with invalid write_p %d", write_p);
+		DEBUG4("(TLS) Ignoring cbtls_msg call with invalid write_p %d", write_p);
 		return;
 	}
 
@@ -104,6 +163,25 @@ void cbtls_msg(int write_p, int msg_version, int content_type,
 	 */
 	if (!state) return;
 
+	if (rad_debug_lvl > 3) {
+		size_t i, j, data_len = len;
+		char buffer[3*16 + 1];
+		uint8_t const *in = inbuf;
+
+		DEBUG("(TLS) Received %zu bytes of TLS data", len);
+		if (data_len > 256) data_len = 256;
+
+		for (i = 0; i < data_len; i += 16) {
+			for (j = 0; j < 16; j++) {
+				if ((i + j) >= data_len) break;
+
+				sprintf(buffer + 3 * j, "%02x ", in[i + j]);
+			}
+
+			DEBUG("(TLS)        %s", buffer);
+		}
+	}
+
 	/*
 	 *	0 - received (from peer)
 	 *	1 - sending (to peer)
@@ -111,7 +189,6 @@ void cbtls_msg(int write_p, int msg_version, int content_type,
 	state->info.origin = write_p;
 	state->info.content_type = content_type;
 	state->info.record_len = len;
-	state->info.version = msg_version;
 	state->info.initialized = true;
 
 	if (content_type == SSL3_RT_ALERT) {
@@ -124,6 +201,12 @@ void cbtls_msg(int write_p, int msg_version, int content_type,
 		state->info.alert_level = 0x00;
 		state->info.alert_description = 0x00;
 
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+	} else if (content_type == SSL3_RT_INNER_CONTENT_TYPE && buf[0] == SSL3_RT_APPLICATION_DATA) {
+		/* let tls_ack_handler set application_data */
+		state->info.content_type = SSL3_RT_HANDSHAKE;
+#endif
+
 #ifdef SSL3_RT_HEARTBEAT
 	} else if (content_type == TLS1_RT_HEARTBEAT) {
 		uint8_t *p = buf;
@@ -141,16 +224,24 @@ void cbtls_msg(int write_p, int msg_version, int content_type,
 		}
 #endif
 	}
+
 	tls_session_information(state);
 }
 
 int cbtls_password(char *buf,
-		   int num UNUSED,
+		   int num,
 		   int rwflag UNUSED,
 		   void *userdata)
 {
-	strcpy(buf, (char *)userdata);
-	return(strlen((char *)userdata));
+	size_t len;
+
+	len = strlcpy(buf, (char *)userdata, num);
+	if (len >= (size_t) num) {
+		ERROR("Password too long.  Maximum length is %i bytes", num - 1);
+		return 0;
+	}
+
+	return len;
 }
 
 #endif
diff --git a/src/main/map.c b/src/main/map.c
index 6275ba124d..17988d27f9 100644
--- a/src/main/map.c
+++ b/src/main/map.c
@@ -245,6 +245,18 @@ int map_afrom_cp(TALLOC_CTX *ctx, vp_map_t **out, CONF_PAIR *cp,
 		}
 		break;
 
+	case T_BARE_WORD:
+		/*
+		 *	Foo = %{...}
+		 *
+		 *	Not allowed!
+		 */
+		if ((attr[0] == '%') && (attr[1] == '{')) {
+			cf_log_err_cp(cp, "Bare expansions are not permitted.  They must be in a double-quoted string.");
+			goto error;
+		}
+		/* FALL-THROUGH */
+
 	default:
 		slen = tmpl_afrom_attr_str(ctx, &map->lhs, attr, dst_request_def, dst_list_def, true, true);
 		if (slen <= 0) {
@@ -285,14 +297,20 @@ int map_afrom_cp(TALLOC_CTX *ctx, vp_map_t **out, CONF_PAIR *cp,
 		goto error;
 	}
 
-	/*
-	 *	We cannot assign a count to an attribute.  That must
-	 *	be done in an xlat.
-	 */
-	if ((map->rhs->type == TMPL_TYPE_ATTR) &&
-	    (map->rhs->tmpl_num == NUM_COUNT)) {
-		cf_log_err_cp(cp, "Cannot assign from a count");
-		goto error;
+	if (map->rhs->type == TMPL_TYPE_ATTR) {
+		/*
+		 *	We cannot assign a count to an attribute.  That must
+		 *	be done in an xlat.
+		 */
+		if (map->rhs->tmpl_num == NUM_COUNT) {
+			cf_log_err_cp(cp, "Cannot assign from a count");
+			goto error;
+		}
+
+		if (map->rhs->tmpl_da->flags.virtual) {
+			cf_log_err_cp(cp, "Virtual attributes must be in an expansion such as \"%%{%s}\".", map->rhs->tmpl_da->name);
+			goto error;
+		}
 	}
 
 	VERIFY_MAP(map);
@@ -1091,6 +1109,12 @@ int map_to_request(REQUEST *request, vp_map_t const *map, radius_map_getvalue_t
 	 */
 	if (((map->lhs->tmpl_list == PAIR_LIST_COA) ||
 	     (map->lhs->tmpl_list == PAIR_LIST_DM)) && !request->coa) {
+		if ((request->packet->code == PW_CODE_COA_REQUEST) ||
+		    (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) {
+			REDEBUG("You cannot do 'update coa' when processing a CoA / Disconnect request.  Use 'update request' instead.");
+			return -2;
+		}
+
 		if (!request_alloc_coa(context)) {
 			REDEBUG("Failed to create a CoA/Disconnect Request message");
 			return -2;
@@ -1173,10 +1197,14 @@ int map_to_request(REQUEST *request, vp_map_t const *map, radius_map_getvalue_t
 				rad_assert(map->rhs->type == TMPL_TYPE_EXEC);
 				/* FALL-THROUGH */
 		case T_OP_ADD:
-				fr_pair_list_move(parent, list, &head);
+				fr_pair_list_move(parent, list, &head, map->op);
 				fr_pair_list_free(&head);
 			}
 			goto finish;
+		case T_OP_PREPEND:
+			fr_pair_list_move(parent, list, &head, T_OP_PREPEND);
+			fr_pair_list_free(&head);
+			goto finish;
 
 		default:
 			fr_pair_list_free(&head);
@@ -1341,6 +1369,14 @@ int map_to_request(REQUEST *request, vp_map_t const *map, radius_map_getvalue_t
 		fr_pair_list_free(&head);
 		break;
 
+	/*
+	 *	^= - Prepend src_list attributes to the destination
+	 */
+	case T_OP_PREPEND:
+		fr_pair_prepend(list, head);
+		head = NULL;
+		break;
+
 	/*
 	 *	+= - Add all src_list attributes to the destination
 	 */
diff --git a/src/main/radclient.c b/src/main/radclient.c
index 52d2872b13..09d27c8711 100644
--- a/src/main/radclient.c
+++ b/src/main/radclient.c
@@ -28,6 +28,10 @@ RCSID("$Id$")
 #include <freeradius-devel/radpaths.h>
 #include <freeradius-devel/udpfromto.h>
 #include <freeradius-devel/conf.h>
+#ifdef HAVE_OPENSSL_SSL_H
+#include <openssl/ssl.h>
+#include <freeradius-devel/openssl3.h>
+#endif
 #include <ctype.h>
 
 #ifdef HAVE_GETOPT_H
@@ -36,6 +40,8 @@ RCSID("$Id$")
 
 #include <assert.h>
 
+USES_APPLE_DEPRECATED_API
+
 typedef struct REQUEST REQUEST;	/* to shut up warnings about mschap.h */
 
 #include "smbdes.h"
@@ -155,9 +161,60 @@ static int _rc_request_free(rc_request_t *request)
 	return 0;
 }
 
+#if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L
+#  include <openssl/provider.h>
+
+static OSSL_PROVIDER *openssl_default_provider = NULL;
+static OSSL_PROVIDER *openssl_legacy_provider = NULL;
+
+static int openssl3_init(void)
+{
+	/*
+	 *	Load the default provider for most algorithms
+	 */
+	openssl_default_provider = OSSL_PROVIDER_load(NULL, "default");
+	if (!openssl_default_provider) {
+		ERROR("(TLS) Failed loading default provider");
+		return -1;
+	}
+
+	/*
+	 *	Needed for MD4
+	 *
+	 *	https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Legacy-Algorithms
+	 */
+	openssl_legacy_provider = OSSL_PROVIDER_load(NULL, "legacy");
+	if (!openssl_legacy_provider) {
+		ERROR("(TLS) Failed loading legacy provider");
+		return -1;
+	}
+
+	return 0;
+}
+
+static void openssl3_free(void)
+{
+	if (openssl_default_provider && !OSSL_PROVIDER_unload(openssl_default_provider)) {
+		ERROR("Failed unloading default provider");
+	}
+	openssl_default_provider = NULL;
+
+	if (openssl_legacy_provider && !OSSL_PROVIDER_unload(openssl_legacy_provider)) {
+		ERROR("Failed unloading legacy provider");
+	}
+	openssl_legacy_provider = NULL;
+}
+#else
+#define openssl3_init()
+#define openssl3_free()
+#endif
+
+
+
 static int mschapv1_encode(RADIUS_PACKET *packet, VALUE_PAIR **request,
 			   char const *password)
 {
+	int rcode;
 	unsigned int i;
 	uint8_t *p;
 	VALUE_PAIR *challenge, *reply;
@@ -190,9 +247,8 @@ static int mschapv1_encode(RADIUS_PACKET *packet, VALUE_PAIR **request,
 
 	p[1] = 0x01; /* NT hash */
 
-	if (mschap_ntpwdhash(nthash, password) < 0) {
-		return 0;
-	}
+	rcode = mschap_ntpwdhash(nthash, password);
+	if (rcode < 0) return 0;
 
 	smbdes_mschap(nthash, challenge->vp_octets, p + 26);
 	return 1;
@@ -960,8 +1016,8 @@ static int send_one_packet(rc_request_t *request)
 			 */
 			fr_packet_list_yank(pl, request->packet);
 
-			REDEBUG("No reply from server for ID %d socket %d",
-				request->packet->id, request->packet->sockfd);
+			RDEBUG("No reply from server for ID %d socket %d",
+			       request->packet->id, request->packet->sockfd);
 			deallocate_id(request);
 
 			/*
@@ -1197,9 +1253,11 @@ int main(int argc, char **argv)
 			break;
 
 		case 'c':
-			if (!isdigit((int) *optarg))
-				usage();
+			if (!isdigit((int) *optarg)) usage();
+
 			resend_count = atoi(optarg);
+
+			if (resend_count < 1) usage();
 			break;
 
 		case 'D':
@@ -1421,6 +1479,8 @@ int main(int argc, char **argv)
 		exit(1);
 	}
 
+	openssl3_init();
+
 	/*
 	 *	Bind to the first specified IP address and port.
 	 *	This means we ignore later ones.
@@ -1637,5 +1697,8 @@ int main(int argc, char **argv)
 	if ((stats.lost > 0) || (stats.failed > 0)) {
 		exit(1);
 	}
+
+	openssl3_free();
+
 	exit(0);
 }
diff --git a/src/main/tls.c b/src/main/tls.c
index 78c7370a63..118978b52a 100644
--- a/src/main/tls.c
+++ b/src/main/tls.c
@@ -27,6 +27,7 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 
 #include <freeradius-devel/radiusd.h>
 #include <freeradius-devel/process.h>
+#include <freeradius-devel/modules.h>
 #include <freeradius-devel/rad_assert.h>
 
 #ifdef HAVE_SYS_STAT_H
@@ -37,6 +38,10 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 #include <fcntl.h>
 #endif
 
+#ifdef HAVE_DIRENT_H
+#include <dirent.h>
+#endif
+
 #ifdef HAVE_UTIME_H
 #include <utime.h>
 #endif
@@ -56,8 +61,25 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 #  endif
 #  include <openssl/ssl.h>
 
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+#  include <openssl/provider.h>
+
+static OSSL_PROVIDER *openssl_default_provider = NULL;
+static OSSL_PROVIDER *openssl_legacy_provider = NULL;
+#endif
+
 #define LOG_PREFIX "tls"
 
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+#define ERR_get_error_line(_file, _line) ERR_get_error_all(_file, _line, NULL, NULL, NULL)
+
+#define FIPS_mode(_x) EVP_default_properties_is_fips_enabled(NULL)
+#define PEM_read_bio_DHparams(_bio, _x, _y, _z) PEM_read_bio_Parameters(_bio, &dh)
+#define SSL_CTX_set0_tmp_dh_pkey(_ctx, _dh) SSL_CTX_set_tmp_dh(_ctx, _dh)
+#define DH EVP_PKEY
+#define DH_free(_dh)
+#endif
+
 #ifdef ENABLE_OPENSSL_VERSION_CHECK
 typedef struct libssl_defect {
 	uint64_t	high;
@@ -153,6 +175,11 @@ static unsigned int 	record_plus(record_t *buf, void const *ptr,
 static unsigned int 	record_minus(record_t *buf, void *ptr,
 				     unsigned int size);
 
+typedef struct {
+	char const	*name;
+	SSL_CTX		*ctx;
+} fr_realm_ctx_t;
+
 DIAG_OFF(format-nonliteral)
 /** Print errors in the TLS thread local error stack
  *
@@ -191,9 +218,9 @@ static int tls_verror_log(REQUEST *request, char const *msg, va_list ap)
 
 			/* Extra verbose */
 			if ((request && RDEBUG_ENABLED3) || DEBUG_ENABLED3) {
-				ROPTIONAL(REDEBUG, ERROR, "%s: %s[%i]:%s", p, file, line, buffer);
+				ROPTIONAL(REDEBUG, ERROR, "(TLS) %s: %s[%i]:%s", p, file, line, buffer);
 			} else {
-				ROPTIONAL(REDEBUG, ERROR, "%s: %s", p, buffer);
+				ROPTIONAL(REDEBUG, ERROR, "(TLS) %s: %s", p, buffer);
 			}
 
 			talloc_free(p);
@@ -205,7 +232,7 @@ static int tls_verror_log(REQUEST *request, char const *msg, va_list ap)
 		 *	Print the error we were given, irrespective
 		 *	of whether there were any OpenSSL errors.
 		 */
-		ROPTIONAL(RERROR, ERROR, "%s", p);
+		ROPTIONAL(RERROR, ERROR, "(TLS) %s", p);
 		talloc_free(p);
 	}
 
@@ -217,9 +244,9 @@ static int tls_verror_log(REQUEST *request, char const *msg, va_list ap)
 		ERR_error_string_n(error, buffer, sizeof(buffer));
 		/* Extra verbose */
 		if ((request && RDEBUG_ENABLED3) || DEBUG_ENABLED3) {
-			ROPTIONAL(REDEBUG, ERROR, "%s[%i]:%s", file, line, buffer);
+			ROPTIONAL(REDEBUG, ERROR, "(TLS) %s[%i]:%s", file, line, buffer);
 		} else {
-			ROPTIONAL(REDEBUG, ERROR, "%s", buffer);
+			ROPTIONAL(REDEBUG, ERROR, "(TLS) %s", buffer);
 		}
 		in_stack++;
 	} while ((error = ERR_get_error_line(&file, &line)));
@@ -309,11 +336,11 @@ int tls_error_io_log(REQUEST *request, tls_session_t *session, int ret, char con
 	 *	being regarded as "dead".
 	 */
 	case SSL_ERROR_SYSCALL:
-		ROPTIONAL(REDEBUG, ERROR, "System call (I/O) error (%i)", ret);
+		ROPTIONAL(REDEBUG, ERROR, "(TLS) System call (I/O) error (%i)", ret);
 		return 0;
 
 	case SSL_ERROR_SSL:
-		ROPTIONAL(REDEBUG, ERROR, "TLS protocol error (%i)", ret);
+		ROPTIONAL(REDEBUG, ERROR, "(TLS) Protocol error (%i)", ret);
 		return 0;
 
 	/*
@@ -323,7 +350,7 @@ int tls_error_io_log(REQUEST *request, tls_session_t *session, int ret, char con
 	 *	the code needs updating here.
 	 */
 	default:
-		ROPTIONAL(REDEBUG, ERROR, "TLS session error %i (%i)", error, ret);
+		ROPTIONAL(REDEBUG, ERROR, "(TLS) Session error %i (%i)", error, ret);
 		return 0;
 	}
 
@@ -376,7 +403,7 @@ static unsigned int psk_server_callback(SSL *ssl, const char *identity,
 		 *	The passed identity is weird.  Deny it.
 		 */
 		if (!identity_is_safe(identity)) {
-			RWDEBUG("Invalid characters in PSK identity %s", identity);
+			RWDEBUG("(TLS) Invalid characters in PSK identity %s", identity);
 			return 0;
 		}
 
@@ -386,7 +413,7 @@ static unsigned int psk_server_callback(SSL *ssl, const char *identity,
 		hex_len = radius_xlat(buffer, sizeof(buffer), request, conf->psk_query,
 				      NULL, NULL);
 		if (!hex_len) {
-			RWDEBUG("PSK expansion returned an empty string.");
+			RWDEBUG("(TLS) PSK expansion returned an empty string.");
 			return 0;
 		}
 
@@ -396,7 +423,7 @@ static unsigned int psk_server_callback(SSL *ssl, const char *identity,
 		 *	the truncation, and complain about it.
 		 */
 		if (hex_len > (2 * max_psk_len)) {
-			RWDEBUG("Returned PSK is too long (%u > %u)",
+			RWDEBUG("(TLS) Returned PSK is too long (%u > %u)",
 				(unsigned int) hex_len, 2 * max_psk_len);
 			return 0;
 		}
@@ -419,7 +446,7 @@ static unsigned int psk_server_callback(SSL *ssl, const char *identity,
 	 *	static identity.
 	 */
 	if (strcmp(identity, conf->psk_identity) != 0) {
-		ERROR("Supplied PSK identity %s does not match configuration.  Rejecting.",
+		ERROR("(TKS) Supplied PSK identity %s does not match configuration.  Rejecting.",
 		      identity);
 		return 0;
 	}
@@ -475,8 +502,6 @@ void tls_session_id(SSL_SESSION *ssn, char *buffer, size_t bufsize)
 #endif
 }
 
-
-
 static int _tls_session_free(tls_session_t *ssn)
 {
 	/*
@@ -492,6 +517,52 @@ static int _tls_session_free(tls_session_t *ssn)
 	return 0;
 }
 
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
+/*
+ *  By setting the environment variable SSLKEYLOGFILE to a filename keying
+ *  material will be exported that you may use with Wireshark to decode any
+ *  TLS flows. Please see the following for more details:
+ *
+ *	https://gitlab.com/wireshark/wireshark/-/wikis/TLS#tls-decryption
+ *
+ *  An example logging session is (you should delete the file on each run):
+ *
+ *	rm -f /tmp/sslkey.log; env SSLKEYLOGFILE=/tmp/sslkey.log freeradius -X | tee /tmp/debug
+ */
+static void tls_keylog_cb(UNUSED const SSL *ssl, const char *line)
+{
+	int fd;
+	size_t len;
+	const char *filename;
+	// less than _POSIX_PIPE_BUF (512) guarantees writes are atomic for O_APPEND
+	char buffer[64 + 2*SSL3_RANDOM_SIZE + 2*SSL_MAX_MASTER_KEY_LENGTH];
+
+	filename = getenv("SSLKEYLOGFILE");
+	if (!filename) return;
+
+	len = strlen(line);
+	if ((len + 1) > sizeof(buffer)) {
+		DEBUG("SSLKEYLOGFILE buffer not large enough, max %lu, required %lu", sizeof(buffer), len + 1);
+		return;
+	}
+
+	memcpy(buffer, line, len);
+	buffer[len] = '\n';
+
+	fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
+	if (fd < 0) {
+		fr_strerror_printf("Failed to open file %s: %s", filename, strerror(errno));
+		return;
+	}
+
+	if (write(fd, buffer, len + 1) == -1) {
+		DEBUG("Failed to write to file %s: %s", filename, strerror(errno));
+	}
+
+	close(fd);
+}
+#endif
+
 tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, int fd, VALUE_PAIR **certs)
 {
 	int ret;
@@ -506,6 +577,7 @@ tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *con
 
 	ssn->ctx = conf->ctx;
 	ssn->mtu = conf->fragment_size;
+	ssn->conf = conf;
 
 	SSL_CTX_set_mode(ssn->ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_AUTO_RETRY);
 
@@ -516,6 +588,9 @@ tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *con
 	}
 
 	request = request_alloc(ssn);
+	request->packet = rad_alloc(request, false);
+	request->reply = rad_alloc(request, false);
+
 	SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_REQUEST, (void *)request);
 
 	/*
@@ -537,15 +612,14 @@ tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *con
 	SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_CONF, (void *)conf);
 	SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_SSN, (void *)ssn);
 	if (certs) SSL_set_ex_data(ssn->ssl, fr_tls_ex_index_certs, (void *)certs);
+
 	SSL_set_fd(ssn->ssl, fd);
-	ret = SSL_connect(ssn->ssl);
 
+	ret = SSL_connect(ssn->ssl);
 	if (ret < 0) {
 		switch (SSL_get_error(ssn->ssl, ret)) {
-			default:
-				break;
-
-
+		default:
+			break;
 
 		case SSL_ERROR_WANT_READ:
 		case SSL_ERROR_WANT_WRITE:
@@ -555,7 +629,7 @@ tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *con
 	}
 
 	if (ret <= 0) {
-		tls_error_io_log(NULL, ssn, ret, "Failed in " STRINGIFY(__FUNCTION__) " (SSL_connect)");
+		tls_error_io_log(NULL, ssn, ret, "Failed in connecting TLS session.");
 		talloc_free(ssn);
 
 		return NULL;
@@ -575,18 +649,61 @@ tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *con
  * @param conf to use to configure the tls session.
  * @param request The current #REQUEST.
  * @param client_cert Whether to require a client_cert.
+ * @param allow_tls13 Whether to allow or forbid TLS 1.3.
  * @return a new session on success, or NULL on error.
  */
-tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQUEST *request, bool client_cert)
+tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQUEST *request, bool client_cert,
+#ifndef TLS1_3_VERSION
+			       UNUSED
+#endif
+			       bool allow_tls13)
 {
 	tls_session_t	*state = NULL;
 	SSL		*new_tls = NULL;
 	int		verify_mode = 0;
 	VALUE_PAIR	*vp;
+	X509_STORE	*new_cert_store;
 
 	rad_assert(request != NULL);
 
-	RDEBUG2("Initiating new TLS session");
+	RDEBUG2("(TLS) Initiating new session");
+
+	/*
+	 *	Replace X509 store if it is time to update CRLs/certs in ca_path
+	 */
+	if (conf->ca_path_reload_interval > 0 && conf->ca_path_last_reload + conf->ca_path_reload_interval <= request->timestamp) {
+		pthread_mutex_lock(&conf->mutex);
+		/* recheck conf->ca_path_last_reload because it may be inaccurate without mutex */
+		if (conf->ca_path_last_reload + conf->ca_path_reload_interval <= request->timestamp) {
+			RDEBUG2("Flushing X509 store to re-read data from ca_path dir");
+
+			if ((new_cert_store = fr_init_x509_store(conf)) == NULL) {
+				RERROR("(TLS) Error replacing X509 store, out of memory (?)");
+			} else {
+				if (conf->old_x509_store) X509_STORE_free(conf->old_x509_store);
+				/*
+				 * Swap empty store with the old one.
+				 */
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+				conf->old_x509_store = SSL_CTX_get_cert_store(conf->ctx);
+				/* Bump refcnt so the store is kept allocated till next store replacement */
+				X509_STORE_up_ref(conf->old_x509_store);
+				SSL_CTX_set_cert_store(conf->ctx, new_cert_store);
+#else
+				/*
+				 * We do not use SSL_CTX_set_cert_store() call here because
+				 * we are not sure that old X509 store is not in the use by some
+				 * thread (i.e. cert check in progress).
+				 * Keep it allocated till next store replacement.
+				 */
+				conf->old_x509_store = conf->ctx->cert_store;
+				conf->ctx->cert_store = new_cert_store;
+#endif
+				conf->ca_path_last_reload = request->timestamp;
+			}
+		}
+		pthread_mutex_unlock(&conf->mutex);
+	}
 
 	new_tls = SSL_new(conf->ctx);
 	if (new_tls == NULL) {
@@ -594,11 +711,35 @@ tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQU
 		return NULL;
 	}
 
+#ifdef TLS1_3_VERSION
+	/*
+	 *	Disallow TLS 1.3 for TTLS, PEAP, and FAST.
+	 *
+	 *	We need another magic configuration option to allow
+	 *	it.
+	 */
+	if (!allow_tls13 && (conf->max_version == TLS1_3_VERSION)) {
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+		WARN("!!                    FORCING MAXIMUM TLS VERSION TO TLS 1.2                  !!");
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+		WARN("!! There is either no standard for using this EAP method with TLS 1.3,");
+		WARN("!! or FreeRADIUS does not fully support TLS 1.3 for this EAP method.");
+		WARN("!!");
+		WARN("!! This message can be removed by setting tls_max_version = \"1.2\"");
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+
+		if (SSL_set_max_proto_version(new_tls, TLS1_2_VERSION) == 0) {
+			tls_error_log(request, "Failed limiting maximum version to TLS 1.2");
+			return NULL;
+		}
+	}
+#endif
+
 	/* We use the SSL's "app_data" to indicate a call-back */
 	SSL_set_app_data(new_tls, NULL);
 
 	if ((state = talloc_zero(ctx, tls_session_t)) == NULL) {
-		RERROR("Error allocating memory for SSL state");
+		RERROR("(TLS) Error allocating memory for SSL state");
 		return NULL;
 	}
 	session_init(state);
@@ -606,6 +747,14 @@ tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQU
 
 	state->ctx = conf->ctx;
 	state->ssl = new_tls;
+	state->conf = conf;
+
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
+	/*
+	 *	Set the keylog file if the admin requested it.
+	 */
+	if (getenv("SSLKEYLOGFILE") != NULL) SSL_CTX_set_keylog_callback(state->ctx, tls_keylog_cb);
+#endif
 
 	/*
 	 *	Initialize callbacks
@@ -637,6 +786,85 @@ tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQU
 	SSL_set_msg_callback_arg(new_tls, state);
 	SSL_set_info_callback(new_tls, cbtls_info);
 
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+	/*
+	 *	Allow policies to load context-specific certificate chains.
+	 */
+	vp = fr_pair_find_by_num(request->config, PW_TLS_SESSION_CERT_FILE, 0, TAG_ANY);
+	if (vp) {
+		VALUE_PAIR *key = fr_pair_find_by_num(request->config, PW_TLS_SESSION_CERT_PRIVATE_KEY_FILE, 0, TAG_ANY);
+		if (!key) key = vp;
+
+		RDEBUG2("(TLS) Loading session certificate file \"%s\"", vp->vp_strvalue);
+
+		if (conf->realms) {
+			fr_realm_ctx_t my_r, *r;
+
+			/*
+			 *	Use a pre-existing SSL CTX, if
+			 *	available.  Note that due to OpenSSL
+			 *	issues, this really changes only the
+			 *	certificate files, and leaves all
+			 *	other fields alone.  e.g. you can't
+			 *	select a different TLS version.
+			 *
+			 *	This is fine for our purposes in v3.
+			 *	Due to how we build them, the various
+			 *	additional SSL_CTXs are identical to
+			 *	the main one, except for certs.
+			 */
+			my_r.name = vp->vp_strvalue;
+			r = fr_hash_table_finddata(conf->realms, &my_r);
+			if (r) {
+				(void) SSL_set_SSL_CTX(state->ssl, r->ctx);
+				goto after_chain;
+			}
+
+			/*
+			 *	Else fall through to trying to dynamically load the certs.
+			 */
+		}
+
+		if (conf->file_type) {
+			if (SSL_use_certificate_chain_file(state->ssl, vp->vp_strvalue) != 1) {
+				tls_error_log(request, "Failed loading TLS session certificate \"%s\"",
+					      vp->vp_strvalue);
+			error:
+				talloc_free(state);
+				return NULL;
+			}
+		} else {
+			if (SSL_use_certificate_file(state->ssl, vp->vp_strvalue, SSL_FILETYPE_ASN1) != 1) {
+				tls_error_log(request, "Failed loading TLS session certificate \"%s\"",
+					      vp->vp_strvalue);
+				goto error;
+			}
+		}
+
+		/*
+		 *	Note that there is either no password, or it
+		 *	has to be the same as what's in the
+		 *	configuration.
+		 *
+		 *	There is just no additional security to
+		 *	putting a password into the same file system
+		 *	as the private key.
+		 */
+		if (SSL_use_PrivateKey_file(state->ssl, key->vp_strvalue, SSL_FILETYPE_PEM) != 1) {
+			tls_error_log(request, "Failed loading TLS session certificate \"%s\"",
+				      key->vp_strvalue);
+			goto error;
+		}
+
+		if (SSL_check_private_key(state->ssl) != 1) {
+			tls_error_log(request, "Failed validating TLS session certificate \"%s\"",
+				      vp->vp_strvalue);
+			goto error;
+		}
+	}
+after_chain:
+#endif
+
 	/*
 	 *	In Server mode we only accept.
 	 */
@@ -646,7 +874,7 @@ tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQU
 	 *	Verify the peer certificate, if asked.
 	 */
 	if (client_cert) {
-		RDEBUG2("Setting verify mode to require certificate from client");
+		RDEBUG2("(TLS) Setting verify mode to require certificate from client");
 		verify_mode = SSL_VERIFY_PEER;
 		verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
 		verify_mode |= SSL_VERIFY_CLIENT_ONCE;
@@ -670,10 +898,41 @@ tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQU
 	 *	just too much.
 	 */
 	state->mtu = conf->fragment_size;
+#define EAP_TLS_MAGIC_OVERHEAD (63)
+
+	/*
+	 *	If the packet contains an MTU, then use that.  We
+	 *	trust the admin!
+	 */
 	vp = fr_pair_find_by_num(request->packet->vps, PW_FRAMED_MTU, 0, TAG_ANY);
-	if (vp && (vp->vp_integer > 100) && (vp->vp_integer < state->mtu)) {
-		state->mtu = vp->vp_integer;
+	if (vp) {
+		if ((vp->vp_integer > 100) && (vp->vp_integer < state->mtu)) {
+			state->mtu = vp->vp_integer;
+		}
+
+	} else if (request->parent) {
+		/*
+		 *	If there's a parent request, we look for what
+		 *	MTU was set there.  Then, we use an MTU which
+		 *	accounts for the extra overhead of nesting EAP
+		 *	+ TLS inside of EAP + TLS.
+		 */
+		vp = fr_pair_find_by_num(request->parent->state, PW_FRAMED_MTU, 0, TAG_ANY);
+		if (vp && (vp->vp_integer > (100 + EAP_TLS_MAGIC_OVERHEAD)) && (vp->vp_integer <= state->mtu)) {
+			state->mtu = vp->vp_integer - EAP_TLS_MAGIC_OVERHEAD;
+		}
+	}
+
+	/*
+	 *	Cache / update the Framed-MTU in the session-state
+	 *	list.
+	 */
+	vp = fr_pair_find_by_num(request->state, PW_FRAMED_MTU, 0, TAG_ANY);
+	if (!vp) {
+		vp = fr_pair_afrom_num(request->state_ctx, PW_FRAMED_MTU, 0);
+		fr_pair_add(&request->state, vp);
 	}
+	if (vp) vp->vp_integer = state->mtu;
 
 	if (conf->session_cache_enable) state->allow_session_resumption = true; /* otherwise it's false */
 
@@ -697,12 +956,15 @@ int tls_handshake_recv(REQUEST *request, tls_session_t *ssn)
 {
 	int err;
 
-	if (ssn->invalid_hb_used) return 0;
+	if (ssn->invalid_hb_used) {
+		REDEBUG("(TLS) OpenSSL Heartbeat attack detected.  Closing connection");
+		return 0;
+	}
 
 	if (ssn->dirty_in.used > 0) {
 		err = BIO_write(ssn->into_ssl, ssn->dirty_in.data, ssn->dirty_in.used);
 		if (err != (int) ssn->dirty_in.used) {
-			REDEBUG("TLS - Failed writing %zd bytes to SSL BIO: %d", ssn->dirty_in.used, err);
+			REDEBUG("(TLS) Failed writing %zd bytes to SSL BIO: %d", ssn->dirty_in.used, err);
 			record_init(&ssn->dirty_in);
 			return 0;
 		}
@@ -716,24 +978,26 @@ int tls_handshake_recv(REQUEST *request, tls_session_t *ssn)
 		return 1;
 	}
 
-	if (!tls_error_io_log(request, ssn, err, "Failed in " STRINGIFY(__FUNCTION__) " (SSL_read)")) return 0;
+	if (!tls_error_io_log(request, ssn, err, "Failed reading from OpenSSL")) return 0;
 
 	/* Some Extra STATE information for easy debugging */
 	if (!ssn->is_init_finished && SSL_is_init_finished(ssn->ssl)) {
 		VALUE_PAIR *vp;
 		char const *str_version;
 
-		RDEBUG2("TLS - Connection Established");
+		RDEBUG2("(TLS) Connection Established");
 		ssn->is_init_finished = true;
 
 		vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_SESSION_CIPHER_SUITE, 0);
 		if (vp) {
 			fr_pair_value_strcpy(vp, SSL_CIPHER_get_name(SSL_get_current_cipher(ssn->ssl)));
 			fr_pair_add(&request->state, vp);
+			RINDENT();
 			rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+			REXDENT();
 		}
 
-		switch (ssn->info.version) {
+		switch (SSL_version(ssn->ssl)) {
 		case SSL2_VERSION:
 			str_version = "SSL 2.0";
 			break;
@@ -767,13 +1031,15 @@ int tls_handshake_recv(REQUEST *request, tls_session_t *ssn)
 		if (vp) {
 			fr_pair_value_strcpy(vp, str_version);
 			fr_pair_add(&request->state, vp);
+			RINDENT();
 			rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+			REXDENT();
 		}
 	}
-	else if (SSL_in_init(ssn->ssl)) { RDEBUG2("TLS - In Handshake Phase"); }
-	else if (SSL_in_before(ssn->ssl)) { RDEBUG2("TLS - Before Handshake Phase"); }
-	else if (SSL_in_accept_init(ssn->ssl)) { RDEBUG2("TLS - In Accept mode"); }
-	else if (SSL_in_connect_init(ssn->ssl)) { RDEBUG2("TLS - In Connect mode"); }
+	else if (SSL_in_init(ssn->ssl)) { RDEBUG2("(TLS) In Handshake Phase"); }
+	else if (SSL_in_before(ssn->ssl)) { RDEBUG2("(TLS) Before Handshake Phase"); }
+	else if (SSL_in_accept_init(ssn->ssl)) { RDEBUG2("(TLS) In Accept mode"); }
+	else if (SSL_in_connect_init(ssn->ssl)) { RDEBUG2("(TLS) In Connect mode"); }
 
 #if OPENSSL_VERSION_NUMBER >= 0x10001000L
 	/*
@@ -791,7 +1057,7 @@ int tls_handshake_recv(REQUEST *request, tls_session_t *ssn)
 		 *	to get the session is a hard fail.
 		 */
 		if (!ssn->ssl_session && ssn->is_init_finished) {
-			RDEBUG("TLS - Failed getting session");
+			RDEBUG("(TLS) Failed getting session");
 			return 0;
 		}
 	}
@@ -805,25 +1071,25 @@ int tls_handshake_recv(REQUEST *request, tls_session_t *ssn)
 		err = BIO_read(ssn->from_ssl, ssn->dirty_out.data,
 			       sizeof(ssn->dirty_out.data));
 		if (err > 0) {
-			RDEBUG2("TLS - got %d bytes of data", err);
+			RDEBUG3("(TLS) got %d bytes of data", err);
 			ssn->dirty_out.used = err;
 
 		} else if (BIO_should_retry(ssn->from_ssl)) {
 			record_init(&ssn->dirty_in);
-			RDEBUG2("TLS - Asking for more data in tunnel.");
+			RDEBUG2("(TLS) Asking for more data in tunnel.");
 			return 1;
 
 		} else {
-			tls_error_log(NULL, "Error reading from SSL BIO");
+			tls_error_log(NULL, "Error reading from OpenSSL");
 			record_init(&ssn->dirty_in);
-			RDEBUG2("TLS - Tunnel data is established.");
 			return 0;
 		}
 	} else {
-
-		RDEBUG2("TLS - Application data.");
-		/* Its clean application data, do whatever we want */
+		RDEBUG2("(TLS) Application data.");
+		/* Its clean application data, leave whatever is in the buffer */
+#if 0
 		record_init(&ssn->clean_out);
+#endif
 	}
 
 	/* We are done with dirty_in, reinitialize it */
@@ -855,13 +1121,12 @@ int tls_handshake_send(REQUEST *request, tls_session_t *ssn)
 		record_minus(&ssn->clean_in, NULL, written);
 
 		/* Get the dirty data from Bio to send it */
-		err = BIO_read(ssn->from_ssl, ssn->dirty_out.data,
-			       sizeof(ssn->dirty_out.data));
+		err = BIO_read(ssn->from_ssl, ssn->dirty_out.data + ssn->dirty_out.used,
+			       sizeof(ssn->dirty_out.data) - ssn->dirty_out.used);
 		if (err > 0) {
-			ssn->dirty_out.used = err;
+			ssn->dirty_out.used += err;
 		} else {
-			if (!tls_error_io_log(request, ssn, err,
-					      "Failed in " STRINGIFY(__FUNCTION__) " (SSL_write)")) {
+			if (!tls_error_io_log(request, ssn, err, "Failed writing to OpenSSL")) {
 				return 0;
 			}
 		}
@@ -963,7 +1228,10 @@ void tls_session_information(tls_session_t *tls_session)
 {
 	char const *str_write_p, *str_version, *str_content_type = "";
 	char const *str_details1 = "", *str_details2= "";
+	char const *details = NULL;
 	REQUEST *request;
+	VALUE_PAIR *vp;
+	char content_type[16], alert_buf[16];
 	char buffer[32];
 
 	/*
@@ -972,9 +1240,20 @@ void tls_session_information(tls_session_t *tls_session)
 	 */
 	if (rad_debug_lvl == 0) return;
 
-	str_write_p = tls_session->info.origin ? ">>> send" : "<<< recv";
+	/*
+	 *	OpenSSL calls this function with 'pseudo' content
+	 *	types.  The user doesn't care about them, so suppress them.
+	 */
+	if (tls_session->info.content_type > UINT8_MAX) return;
+
+	request = SSL_get_ex_data(tls_session->ssl, FR_TLS_EX_INDEX_REQUEST);
+	if (!request) return;
+
+	str_write_p = tls_session->info.origin ? "(TLS) send" : "(TLS) recv";
+
+#define FROM_CLIENT (tls_session->info.origin == 0)
 
-	switch (tls_session->info.version) {
+	switch (SSL_version(tls_session->ssl)) {
 	case SSL2_VERSION:
 		str_version = "SSL 2.0 ";
 		break;
@@ -1001,13 +1280,12 @@ void tls_session_information(tls_session_t *tls_session)
 #endif
 
 	default:
-		sprintf(buffer, "UNKNOWN TLS VERSION '%04X'", tls_session->info.version);
+		sprintf(buffer, "UNKNOWN TLS VERSION '%04X'", SSL_version(tls_session->ssl));
 		str_version = buffer;
 		break;
 	}
 
-	if (tls_session->info.version == SSL3_VERSION ||
-	    tls_session->info.version == TLS1_VERSION) {
+	if (1) {
 		switch (tls_session->info.content_type) {
 		case SSL3_RT_CHANGE_CIPHER_SPEC:
 			str_content_type = "ChangeCipherSpec";
@@ -1026,7 +1304,8 @@ void tls_session_information(tls_session_t *tls_session)
 			break;
 
 		default:
-			str_content_type = "UnknownContentType";
+			snprintf(content_type, sizeof(content_type), "content=%d", tls_session->info.content_type);
+			str_content_type = content_type;
 			break;
 		}
 
@@ -1045,9 +1324,12 @@ void tls_session_information(tls_session_t *tls_session)
 				}
 
 				str_details2 = " ???";
+				details = "there is a failure inside the TLS protocol exchange";
+
 				switch (tls_session->info.alert_description) {
 				case SSL3_AD_CLOSE_NOTIFY:
 					str_details2 = " close_notify";
+					details = "the connection has been closed, and no further TLS exchanges will take place";
 					break;
 
 				case SSL3_AD_UNEXPECTED_MESSAGE:
@@ -1074,24 +1356,34 @@ void tls_session_information(tls_session_t *tls_session)
 					str_details2 = " handshake_failure";
 					break;
 
+				case SSL3_AD_NO_CERTIFICATE:
+					str_details2 = " no_certificate";
+					details = "the server did not present a certificate to the client";
+					break;
+
 				case SSL3_AD_BAD_CERTIFICATE:
 					str_details2 = " bad_certificate";
+					details = "it believes the server certificate is invalid or malformed";
 					break;
 
 				case SSL3_AD_UNSUPPORTED_CERTIFICATE:
 					str_details2 = " unsupported_certificate";
+					details = "it does not understand the certificate presented by the server";
 					break;
 
 				case SSL3_AD_CERTIFICATE_REVOKED:
 					str_details2 = " certificate_revoked";
+					details = "it believes that the server certificate has been revoked";
 					break;
 
 				case SSL3_AD_CERTIFICATE_EXPIRED:
 					str_details2 = " certificate_expired";
+					details = "it believes that the server certificate has expired.  Either renew the server certificate, or check the time on the client";
 					break;
 
 				case SSL3_AD_CERTIFICATE_UNKNOWN:
 					str_details2 = " certificate_unknown";
+					details = "it does not recognize the server certificate";
 					break;
 
 				case SSL3_AD_ILLEGAL_PARAMETER:
@@ -1100,6 +1392,7 @@ void tls_session_information(tls_session_t *tls_session)
 
 				case TLS1_AD_UNKNOWN_CA:
 					str_details2 = " unknown_ca";
+					details = "it does not recognize the CA used to issue the server certificate.  Please update the client so that it knows about the CA";
 					break;
 
 				case TLS1_AD_ACCESS_DENIED:
@@ -1120,6 +1413,18 @@ void tls_session_information(tls_session_t *tls_session)
 
 				case TLS1_AD_PROTOCOL_VERSION:
 					str_details2 = " protocol_version";
+					details = "the client does not accept the version of TLS negotiated by the server";
+
+#ifdef TLS1_3_VERSION
+					/*
+					 *	Complain about OpenSSL bugs.
+					 */
+					if ((SSL_version(tls_session->ssl) > tls_session->conf->max_version) &&
+					    (rad_debug_lvl > 0)) {
+						WARN("TLS 1.3 has been negotiated even though it was disabled.  This is an OpenSSL Bug.");
+						WARN("Please set: cipher_list = \"DEFAULT@SECLEVEL=1\" in the tls {...} section.");
+					}
+#endif
 					break;
 
 				case TLS1_AD_INSUFFICIENT_SECURITY:
@@ -1137,12 +1442,69 @@ void tls_session_information(tls_session_t *tls_session)
 				case TLS1_AD_NO_RENEGOTIATION:
 					str_details2 = " no_renegotiation";
 					break;
+
+#ifdef TLS13_AD_MISSING_EXTENSIONS
+				case TLS13_AD_MISSING_EXTENSIONS:
+					str_details2 = " missing_extensions";
+					details = "the server did not present a TLS extension which the client expected to be present.  Please check the TLS libraries on the client and server for compatibility";
+					break;
+#endif
+
+#ifdef TLS13_AD_CERTIFICATE_REQUIRED
+				case TLS13_AD_CERTIFICATE_REQUIRED:
+					str_details2 = " certificate_required";
+					details = "the server did not present a certificate";
+					break;
+#endif
+
+#ifdef TLS1_AD_UNSUPPORTED_EXTENSION
+				case TLS1_AD_UNSUPPORTED_EXTENSION:
+					str_details2 = " unsupported_extension";
+					details = "the server has sent a TLS message which the client does not recognize.  Please check the TLS libraries on the client and server for compatibility";
+					break;
+#endif
+
+#ifdef TLS1_AD_CERTIFICATE_UNOBTAINABLE
+				case TLS1_AD_CERTIFICATE_UNOBTAINABLE:
+					str_details2 = " certificate_unobtainable";
+					break;
+#endif
+
+#ifdef TLS1_AD_UNRECOGNIZED_NAME
+				case TLS1_AD_UNRECOGNIZED_NAME:
+					str_details2 = " unrecognized_name";
+					break;
+#endif
+
+#ifdef TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE
+				case TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE:
+					str_details2 = " bad_certificate_status_response";
+					break;
+#endif
+
+#ifdef TLS1_AD_BAD_CERTIFICATE_HASH_VALUE
+				case TLS1_AD_BAD_CERTIFICATE_HASH_VALUE:
+					str_details2 = " bad_certificate_hash_value";
+					break;
+#endif
+
+#ifdef TLS1_AD_UNKNOWN_PSK_IDENTITY
+				case TLS1_AD_UNKNOWN_PSK_IDENTITY:
+					str_details2 = " unknown_psk_identity";
+					break;
+#endif
+
+#ifdef TLS1_AD_NO_APPLICATION_PROTOCOL
+				case TLS1_AD_NO_APPLICATION_PROTOCOL:
+					str_details2 = " no_application_protocol";
+					break;
+#endif
 				}
 			}
 		}
 
 		if (tls_session->info.content_type == SSL3_RT_HANDSHAKE) {
-			str_details1 = "???";
+			str_details1 = "";
 
 			if (tls_session->info.record_len > 0) switch (tls_session->info.handshake_type) {
 			case SSL3_MT_HELLO_REQUEST:
@@ -1157,6 +1519,18 @@ void tls_session_information(tls_session_t *tls_session)
 				str_details1 = ", ServerHello";
 				break;
 
+#ifdef SSL3_MT_NEWSESSION_TICKET
+			case SSL3_MT_NEWSESSION_TICKET:
+				str_details1 = ", NewSessionTicket";
+				break;
+#endif
+
+#ifdef SSL3_MT_ENCRYPTED_EXTENSIONS
+			case SSL3_MT_ENCRYPTED_EXTENSIONS:
+				str_details1 = ", EncryptedExtensions";
+				break;
+#endif
+
 			case SSL3_MT_CERTIFICATE:
 				str_details1 = ", Certificate";
 				break;
@@ -1184,31 +1558,52 @@ void tls_session_information(tls_session_t *tls_session)
 			case SSL3_MT_FINISHED:
 				str_details1 = ", Finished";
 				break;
+
+#ifdef SSL3_MT_KEY_UPDATE
+			case SSL3_MT_KEY_UPDATE:
+				str_content_type = "KeyUpdate";
+				break;
+#endif
+
+			default:
+				snprintf(alert_buf, sizeof(alert_buf), ", type=%d", tls_session->info.handshake_type);
+				str_details1 = alert_buf;
+				break;
 			}
 		}
 	}
 
 	snprintf(tls_session->info.info_description,
 		 sizeof(tls_session->info.info_description),
-		 "%s %s%s [length %04lx]%s%s\n",
+		 "%s %s%s%s%s",
 		 str_write_p, str_version, str_content_type,
-		 (unsigned long)tls_session->info.record_len,
 		 str_details1, str_details2);
 
-	request = SSL_get_ex_data(tls_session->ssl, FR_TLS_EX_INDEX_REQUEST);
-	if (!request) return;
+	/*
+	 *	Cache the TLS session information in the session-state
+	 *	list, so it can be accessed by Post-Auth-Type
+	 *	Client-Lost { ... }
+	 */
+	vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_SESSION_INFORMATION, 0);
+	if (vp) {
+		fr_pair_value_strcpy(vp, tls_session->info.info_description);
+		fr_pair_add(&request->state, vp);
+	}
 
 	RDEBUG2("%s", tls_session->info.info_description);
+
+	if (FROM_CLIENT && details) RDEBUG2("(TLS) The client is informing us that %s.", details);
 }
 
 static CONF_PARSER cache_config[] = {
 	{ "enable", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, session_cache_enable), "no" },
 
-	{ "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, session_timeout), "24" },
+	{ "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, session_lifetime), "24" },
 	{ "name", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_id_name), NULL },
 
 	{ "max_entries", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, session_cache_size), "255" },
 	{ "persist_dir", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_cache_path), NULL },
+	{ "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_cache_server), NULL },
 	CONF_PARSER_TERMINATOR
 };
 
@@ -1256,6 +1651,7 @@ static CONF_PARSER tls_server_config[] = {
 #ifdef X509_V_FLAG_CRL_CHECK_ALL
 	{ "check_all_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, check_all_crl), "no" },
 #endif
+	{ "ca_path_reload_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, ca_path_reload_interval), "0" },
 	{ "allow_expired_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, allow_expired_crl), NULL },
 	{ "check_cert_cn", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_cn), NULL },
 	{ "cipher_list", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, cipher_list), NULL },
@@ -1263,6 +1659,10 @@ static CONF_PARSER tls_server_config[] = {
 	{ "check_cert_issuer", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_issuer), NULL },
 	{ "require_client_cert", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, require_client_cert), NULL },
 
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+	{ "reject_unknown_intermediate_ca", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disallow_untrusted), .dflt = "no", },
+#endif
+
 #if OPENSSL_VERSION_NUMBER >= 0x0090800fL
 #ifndef OPENSSL_NO_ECDH
 	{ "ecdh_curve", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, ecdh_curve), "prime256v1" },
@@ -1281,9 +1681,19 @@ static CONF_PARSER tls_server_config[] = {
 	{ "disable_tlsv1_2", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_2), NULL },
 #endif
 
-	{ "tls_max_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_max_version), "" },
+	{ "tls_max_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_max_version), NULL },
+
+	{ "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_min_version),
+#if defined(TLS1_2_VERSION)
+	  "1.2"
+#elif defined(TLS1_1_VERSION)
+	  "1.1"
+#else
+	  "1.0"
+#endif
+	},
 
-	{ "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_min_version), "1.0" },
+	{ "realm_dir", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, realm_dir), NULL },
 
 	{ "cache", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) cache_config },
 
@@ -1312,6 +1722,7 @@ static CONF_PARSER tls_client_config[] = {
 	{ "check_cert_cn", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_cn), NULL },
 	{ "cipher_list", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, cipher_list), NULL },
 	{ "check_cert_issuer", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_issuer), NULL },
+	{ "ca_path_reload_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, ca_path_reload_interval), "0" },
 
 #if OPENSSL_VERSION_NUMBER >= 0x0090800fL
 #ifndef OPENSSL_NO_ECDH
@@ -1331,9 +1742,19 @@ static CONF_PARSER tls_client_config[] = {
 	{ "disable_tlsv1_2", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_2), NULL },
 #endif
 
-	{ "tls_max_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_max_version), "" },
+	{ "tls_max_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_max_version), NULL },
 
-	{ "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_min_version), "1.0" },
+	{ "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_min_version),
+#if defined(TLS1_2_VERSION)
+	  "1.2"
+#elif defined(TLS1_1_VERSION)
+	  "1.1"
+#else
+	  "1.0"
+#endif
+	},
+
+	{ "hostname", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, client_hostname), NULL },
 
 	CONF_PARSER_TERMINATOR
 };
@@ -1347,7 +1768,44 @@ static int load_dh_params(SSL_CTX *ctx, char *file)
 	DH *dh = NULL;
 	BIO *bio;
 
-	if (!file) return 0;
+	/*
+	 * Prior to trying to load the file, check what OpenSSL will do with it.
+	 *
+	 * Certain downstreams (such as RHEL) will ignore user-provided dhparams
+	 * in FIPS mode, unless the specified parameters are FIPS-approved.
+	 * However, since OpenSSL >= 1.1.1 will automatically select parameters
+	 * anyways, there's no point in attempting to load them.
+	 *
+	 * Change suggested by @t8m
+	 */
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+	if (FIPS_mode() > 0) {
+		WARN(LOG_PREFIX ": Ignoring user-selected DH parameters in FIPS mode. Using defaults.");
+		file = NULL;
+	}
+
+	/*
+	 *	No dh file, set auto context.
+	 */
+	if (!file) {
+		if (!SSL_CTX_set_dh_auto(ctx, 1)) {
+			ERROR(LOG_PREFIX ": Unable to set DH parameters");
+			return -1;
+		}
+
+		return 0;
+	}
+
+	WARN(LOG_PREFIX ": Setting DH parameters from %s - this is no longer necessary.", file);
+	WARN(LOG_PREFIX ": You should comment out the 'dh_file' configuration item.");
+
+#else
+	if (!file) {
+		WARN(LOG_PREFIX ": Cannot set DH parameters.  DH cipher suites may not work.");
+		return 0;
+	}
+#endif
+
 
 	if ((bio = BIO_new_file(file, "r")) == NULL) {
 		ERROR(LOG_PREFIX ": Unable to open DH file - %s", file);
@@ -1422,7 +1880,7 @@ static int cbtls_new_session(SSL *ssl, SSL_SESSION *sess)
 
 	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
 	if (!conf) {
-		RWDEBUG("Failed to find TLS configuration in session");
+		RWDEBUG("(TLS) Failed to find TLS configuration in session");
 		return 0;
 	}
 
@@ -1439,7 +1897,7 @@ static int cbtls_new_session(SSL *ssl, SSL_SESSION *sess)
 		blob_len = i2d_SSL_SESSION(sess, NULL);
 		if (blob_len < 1) {
 			/* something went wrong */
-			if (request) RWDEBUG("Session serialisation failed, couldn't determine required buffer length");
+			if (request) RWDEBUG("(TLS) Session serialisation failed, could not determine required buffer length");
 			return 0;
 		}
 
@@ -1447,14 +1905,14 @@ static int cbtls_new_session(SSL *ssl, SSL_SESSION *sess)
 		/* alloc and convert to ASN.1 */
 		sess_blob = malloc(blob_len);
 		if (!sess_blob) {
-			RWDEBUG("Session serialisation failed, couldn't allocate buffer (%d bytes)", blob_len);
+			RWDEBUG("(TLS) Session serialisation failed, couldn't allocate buffer (%d bytes)", blob_len);
 			return 0;
 		}
 		/* openssl mutates &p */
 		p = sess_blob;
 		rv = i2d_SSL_SESSION(sess, &p);
 		if (rv != blob_len) {
-			if (request) RWDEBUG("Session serialisation failed");
+			if (request) RWDEBUG("(TLS) Session serialisation failed");
 			goto error;
 		}
 
@@ -1463,7 +1921,7 @@ static int cbtls_new_session(SSL *ssl, SSL_SESSION *sess)
 			 conf->session_cache_path, FR_DIR_SEP, buffer);
 		fd = open(filename, O_RDWR|O_CREAT|O_EXCL, S_IWUSR);
 		if (fd < 0) {
-			if (request) RERROR("Session serialisation failed, failed opening session file %s: %s",
+			if (request) RERROR("(TLS) Session serialisation failed, failed opening session file %s: %s",
 					    filename, fr_syserror(errno));
 			goto error;
 		}
@@ -1486,7 +1944,7 @@ static int cbtls_new_session(SSL *ssl, SSL_SESSION *sess)
 		while (todo > 0) {
 			rv = write(fd, p, todo);
 			if (rv < 1) {
-				if (request) RWDEBUG("Failed writing session: %s", fr_syserror(errno));
+				if (request) RWDEBUG("(TLS) Failed writing session: %s", fr_syserror(errno));
 				close(fd);
 				goto error;
 			}
@@ -1494,7 +1952,7 @@ static int cbtls_new_session(SSL *ssl, SSL_SESSION *sess)
 			todo -= rv;
 		}
 		close(fd);
-		if (request) RWDEBUG("Wrote session %s to %s (%d bytes)", buffer, filename, blob_len);
+		if (request) RWDEBUG("(TLS) Wrote session %s to %s (%d bytes)", buffer, filename, blob_len);
 	}
 
 error:
@@ -1595,7 +2053,7 @@ static SSL_SESSION *cbtls_get_session(SSL *ssl, const unsigned char *data, int l
 
 	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
 	if (!conf) {
-		RWDEBUG("Failed to find TLS configuration in session");
+		RWDEBUG("(TLS) Failed to find TLS configuration in session");
 		return NULL;
 	}
 
@@ -1617,20 +2075,20 @@ static SSL_SESSION *cbtls_get_session(SSL *ssl, const unsigned char *data, int l
 		snprintf(filename, sizeof(filename), "%s%c%s.asn1", conf->session_cache_path, FR_DIR_SEP, buffer);
 		fd = open(filename, O_RDONLY);
 		if (fd < 0) {
-			RWDEBUG("No persisted session file %s: %s", filename, fr_syserror(errno));
+			RWDEBUG("(TLS) No persisted session file %s: %s", filename, fr_syserror(errno));
 			goto error;
 		}
 
 		rv = fstat(fd, &st);
 		if (rv < 0) {
-			RWDEBUG("Failed stating persisted session file %s: %s", filename, fr_syserror(errno));
+			RWDEBUG("(TLS) Failed stating persisted session file %s: %s", filename, fr_syserror(errno));
 			close(fd);
 			goto error;
 		}
 
 		sess_data = talloc_array(NULL, unsigned char, st.st_size);
 		if (!sess_data) {
-			RWDEBUG("Failed allocating buffer for persisted session (%d bytes)", (int) st.st_size);
+			RWDEBUG("(TLS) Failed allocating buffer for persisted session (%d bytes)", (int) st.st_size);
 			close(fd);
 			goto error;
 		}
@@ -1640,7 +2098,7 @@ static SSL_SESSION *cbtls_get_session(SSL *ssl, const unsigned char *data, int l
 		while (todo > 0) {
 			rv = read(fd, q, todo);
 			if (rv < 1) {
-				RWDEBUG("Failed reading persisted session: %s", fr_syserror(errno));
+				RWDEBUG("(TLS) Failed reading persisted session: %s", fr_syserror(errno));
 				close(fd);
 				goto error;
 			}
@@ -1664,7 +2122,7 @@ static SSL_SESSION *cbtls_get_session(SSL *ssl, const unsigned char *data, int l
 		memcpy(&o, &p, sizeof(o));
 		sess = d2i_SSL_SESSION(NULL, o, st.st_size);
 		if (!sess) {
-			RWDEBUG("Failed loading persisted session: %s", ERR_error_string(ERR_get_error(), NULL));
+			RWDEBUG("(TLS) Failed loading persisted session: %s", ERR_error_string(ERR_get_error(), NULL));
 			goto error;
 		}
 
@@ -1674,7 +2132,7 @@ static SSL_SESSION *cbtls_get_session(SSL *ssl, const unsigned char *data, int l
 		rv = pairlist_read(talloc_ctx, filename, &pairlist, 1);
 		if (rv < 0) {
 			/* not safe to un-persist a session w/o VPs */
-			RWDEBUG("Failed loading persisted VPs for session %s", buffer);
+			RWDEBUG("(TLS) Failed loading persisted VPs for session %s", buffer);
 			SSL_SESSION_free(sess);
 			sess = NULL;
 			goto error;
@@ -1708,12 +2166,27 @@ static SSL_SESSION *cbtls_get_session(SSL *ssl, const unsigned char *data, int l
 			if (vp) {
 				if ((request->timestamp + vp->vp_integer) > expires) {
 					vp->vp_integer = expires - request->timestamp;
-					RWDEBUG2("Updating Session-Timeout to %u, due to impending certificate expiration",
+					RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration",
 						 vp->vp_integer);
 				}
 			}
 		}
 
+		/*
+		 *	Resumption MUST use the same EAP type as from
+		 *	the original packet.
+		 */
+		vp = fr_pair_find_by_num(pairlist->reply, PW_EAP_TYPE, 0, TAG_ANY);
+		if (vp) {
+			VALUE_PAIR *type = fr_pair_find_by_num(request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY);
+
+			if (type && (type->vp_integer != vp->vp_integer)) {
+				REDEBUG("Resumption has changed EAP types for session %s", buffer);
+				REDEBUG("Rejecting session due to protocol violations");
+				goto error;
+			}
+		}
+
 		/* move the cached VPs into the session */
 		fr_pair_list_mcopy_by_num(talloc_ctx, &vps, &pairlist->reply, 0, 0, TAG_ANY);
 
@@ -1733,34 +2206,378 @@ error:
 	return sess;
 }
 
-#ifdef HAVE_OPENSSL_OCSP_H
-
-/** Extract components of OCSP responser URL from a certificate
- *
- * @param[in] cert to extract URL from.
- * @param[out] host_out Portion of the URL (must be freed with free()).
- * @param[out] port_out Port portion of the URL (must be freed with free()).
- * @param[out] path_out Path portion of the URL (must be freed with free()).
- * @param[out] is_https Whether the responder should be contacted using https.
- * @return
- *	- 0 if no valid URL is contained in the certificate.
- *	- 1 if a URL was found and parsed.
- *	- -1 if at least one URL was found, but none could be parsed.
- */
-static int ocsp_parse_cert_url(X509 *cert, char **host_out, char **port_out,
-			       char **path_out, int *is_https)
+static size_t tls_session_id_binary(SSL_SESSION *ssn, uint8_t *buffer, size_t bufsize)
 {
-	int			i;
-	bool			found_uri = false;
+#if OPENSSL_VERSION_NUMBER < 0x10001000L
+	size_t size;
 
-	AUTHORITY_INFO_ACCESS	*aia;
-	ACCESS_DESCRIPTION	*ad;
+	size = ssn->session_id_length;
+	if (size > bufsize) size = bufsize;
 
-	aia = X509_get_ext_d2i(cert, NID_info_access, NULL, NULL);
+	memcpy(buffer, ssn->session_id, size);
+	return size;
+#else
+	unsigned int size;
+	uint8_t const *p;
 
-	for (i = 0; i < sk_ACCESS_DESCRIPTION_num(aia); i++) {
-		ad = sk_ACCESS_DESCRIPTION_value(aia, i);
-		if (OBJ_obj2nid(ad->method) != NID_ad_OCSP) continue;
+	p = SSL_SESSION_get_id(ssn, &size);
+	if (size > bufsize) size = bufsize;
+
+	memcpy(buffer, p, size);
+	return size;
+#endif
+}
+
+/*
+ *	From TLS-Cache-Method
+ *
+ *	All of the save / clear / load callbacks are done with any
+ *	OpenSSL locks *unlocked*.  So says the OpenSSL code.
+ */
+#define CACHE_SAVE (1)
+#define CACHE_LOAD (2)
+#define CACHE_CLEAR (3)
+#define CACHE_REFRESH (4)
+
+static REQUEST *cache_init_fake_request(fr_tls_server_conf_t const *conf, SSL_SESSION *sess, SSL *ssl,
+					uint8_t const *data, size_t size)
+{
+	VALUE_PAIR		*vp;
+	REQUEST			*fake, *request = NULL;
+	uint8_t			buffer[MAX_SESSION_SIZE];
+
+	if (sess) {
+		size = tls_session_id_binary(sess, buffer, sizeof(buffer));
+		data = buffer;
+	}
+
+	/*
+	 *	We get called essentially at random by OpenSSL, with
+	 *	no information other than the session ID.  As a
+	 *	result, we have to manually set up our own request.
+	 */
+	if (ssl) request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+
+	if (request) {
+		fake = request_alloc_fake(request);
+	} else {
+		fake = request_alloc(NULL);
+		fake->packet = rad_alloc(fake, false);
+		fake->reply = rad_alloc(fake, false);
+	}
+
+	vp = fr_pair_afrom_num(fake->packet, PW_TLS_SESSION_ID, 0);
+	if (!vp) {
+		talloc_free(fake);
+		return NULL;
+	}
+
+	fr_pair_value_memcpy(vp, data, size);
+	fr_pair_add(&fake->packet->vps, vp);
+
+	fake->server = conf->session_cache_server;
+
+	return fake;
+}
+
+/*
+ *	Clear cached data
+ */
+static void cbtls_cache_clear(SSL_CTX *ctx, SSL_SESSION *sess)
+{
+	fr_tls_server_conf_t	*conf;
+	REQUEST			*fake;
+
+	conf = (fr_tls_server_conf_t *)SSL_CTX_get_app_data(ctx);
+	if (!conf) {
+		DEBUG(LOG_PREFIX ": Failed to find TLS configuration in session");
+		return;
+	}
+
+	/*
+	 *	Find the SSL ID from the session, and delete it.
+	 *
+	 *	Don't bother with any parent request.  We're in a
+	 *	timer callback, and there is no request available.
+	 */
+	fake = cache_init_fake_request(conf, sess, NULL, NULL, 0);
+	if (!fake) return;
+
+	/*
+	 *	Use &request:TLS-Session-Id to clear the cache entry.
+	 */
+	(void) process_post_auth(CACHE_CLEAR, fake);
+	talloc_free(fake);
+	return;
+}
+
+/*
+ *	OpenSSL calls this function in order to save the session
+ *	BEFORE it has sent the final TLS success.  So our process here
+ *	is to say "yes, we saved it", and then do the *actual* saving
+ *	after the TLS success has been sent.
+ */
+static int cbtls_cache_save(UNUSED SSL *ssl, UNUSED SSL_SESSION *sess)
+{
+	return 0;
+}
+
+static int cbtls_cache_save_vps(SSL *ssl, SSL_SESSION *sess, VALUE_PAIR *vps)
+{
+	fr_tls_server_conf_t	*conf;
+	VALUE_PAIR		*vp;
+	REQUEST			*fake = NULL;
+	size_t			size, rv;
+	uint8_t			*p, *sess_blob = NULL;
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) return 0;
+
+	/*
+	 *	Find the SSL ID from the session, and save it.
+	 *
+	 *	Save anything from the parent request.
+	 */
+	fake = cache_init_fake_request(conf, sess, ssl, NULL, 0);
+	if (!fake) return 0;
+
+	/* find out what length data we need */
+	size = i2d_SSL_SESSION(sess, NULL);
+	if (size < 1) return 0;
+
+	/* Do not convert to TALLOC - it's passed to OpenSSL */
+	/* alloc and convert to ASN.1 */
+	MEM(sess_blob = malloc(size));
+
+	/* openssl mutates &p */
+	p = sess_blob;
+	rv = i2d_SSL_SESSION(sess, &p);
+	if (rv != size) goto error;
+
+	vp = fr_pair_afrom_num(fake->state_ctx, PW_TLS_SESSION_DATA, 0);
+	if (!vp) goto error;
+
+	fr_pair_value_memcpy(vp, sess_blob, size);
+	fr_pair_add(&fake->state, vp);
+
+	if (vps) fr_pair_add(&fake->reply->vps, fr_pair_list_copy(fake->reply, vps));
+
+	/*
+	 *	Use &request:TLS-Session-Id to save the
+	 *	&session-state:TLS-Session-Data values.
+	 *
+	 *	The current &reply: list is the list of VPs which
+	 *	should be cached.
+	 *
+	 *	Any other attributes which need to be saved can be
+	 *	read from the &outer.reply: list.
+	 */
+	(void) process_post_auth(CACHE_SAVE, fake);
+
+error:
+	if (fake) talloc_free(fake);
+	free(sess_blob);
+
+	return 0;
+}
+
+static int cbtls_cache_refresh(SSL *ssl, SSL_SESSION *sess)
+{
+	fr_tls_server_conf_t	*conf;
+	REQUEST			*fake = NULL;
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) return 0;
+
+	/*
+	 *	Find the SSL ID from the session, and save it.
+	 *
+	 *	Save anything from the parent request.
+	 */
+	fake = cache_init_fake_request(conf, sess, ssl, NULL, 0);
+	if (!fake) return 0;
+	/*
+	 *	Use &request:TLS-Session-Id to update the cache
+	 *	entry so that it doesn't not expire.
+	 */
+	(void) process_post_auth(CACHE_REFRESH, fake);
+
+	talloc_free(fake);
+
+	return 0;
+}
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+static SSL_SESSION *cbtls_cache_load(SSL *ssl, unsigned char *data, int len, int *copy)
+#else
+static SSL_SESSION *cbtls_cache_load(SSL *ssl, const unsigned char *data, int len, int *copy)
+#endif
+{
+	fr_tls_server_conf_t	*conf;
+	size_t			size;
+	uint8_t const  		*p;
+	VALUE_PAIR		*vp, *vps;
+	TALLOC_CTX		*talloc_ctx;
+	SSL_SESSION		*sess = NULL;
+	REQUEST			*fake = NULL;
+	REQUEST			*request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+	char			buffer[2 * MAX_SESSION_SIZE + 1];
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) return NULL;
+
+	rad_assert(request);
+
+	size = len;
+	if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE;
+
+	if (fr_debug_lvl > 1) {
+		fr_bin2hex(buffer, data, size);
+		RDEBUG2("Peer requested cached session: %s", buffer);
+	}
+
+	*copy = 0;
+
+	/*
+	 *	Take the given SSL ID, and create a fake request.
+	 *
+	 *	Don't bother parenting it from another request.  We do
+	 *	this for a number of reasons.
+	 *
+	 *	One is that rest of the code expects that the VPs will
+	 *	be added to fr_tls_ex_index_vps.  So we don't want to
+	 *	be poking the request directly, as that will result in
+	 *	a change of behavior.
+	 *
+	 *	The larger reason is that we do _not_ want to actually
+	 *	update the reply, until such time as we know that the
+	 *	user has been authenticated.
+	 */
+	fake = cache_init_fake_request(conf, NULL, NULL, data, size);
+	if (!fake) return 0;
+
+	/*
+	 *	Use &request:TLS-Session-Id to load the cached
+	 *	session.
+	 *
+	 *	The "cache load { ...}" section should put the reply
+	 *	attributes into the &reply: list, and the
+	 *	&session-state:TLS-Session-Data attribute.
+	 *
+	 *	Why?  Because v4 does it that way, and there aren't
+	 *	really good reasons for doing it differently.
+	 */
+	(void) process_post_auth(CACHE_LOAD, fake);
+
+	/*
+	 *	Enforce client certificate expiration.
+	 */
+	vp = fr_pair_find_by_num(fake->reply->vps, PW_TLS_CLIENT_CERT_EXPIRATION, 0, TAG_ANY);
+	if (vp) {
+		time_t expires;
+
+		if (ocsp_asn1time_to_epoch(&expires, vp->vp_strvalue) < 0) {
+			RDEBUG2("Failed getting certificate expiration, removing cache entry for session %s - %s", buffer, fr_strerror());
+			SSL_SESSION_free(sess);
+			sess = NULL;
+			goto error;
+		}
+
+		if (expires <= request->timestamp) {
+			RDEBUG2("Certificate has expired, removing cache entry for session %s", buffer);
+			SSL_SESSION_free(sess);
+			sess = NULL;
+			goto error;
+		}
+
+		/*
+		 *	Account for Session-Timeout, if it's available.
+		 */
+		vp = fr_pair_find_by_num(request->reply->vps, PW_SESSION_TIMEOUT, 0, TAG_ANY);
+		if (vp) {
+			if ((request->timestamp + vp->vp_integer) > expires) {
+				vp->vp_integer = expires - request->timestamp;
+				RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration",
+					 vp->vp_integer);
+			}
+		}
+	}
+
+	/*
+	 *	Try to de-serialize the session data.
+	 */
+	vp = fr_pair_find_by_num(fake->state, PW_TLS_SESSION_DATA, 0, TAG_ANY);
+	if (!vp) {
+		RWDEBUG("(TLS) Failed to find TLS-Session-Data in 'session-state' list for session %s", buffer);
+		goto error;
+	}
+
+	/*
+	 *	OpenSSL mutates what's passed in, so we assign sess_data to q,
+	 *	so the value of q gets mutated, and not the value of sess_data.
+	 *
+	 *	We then need a pointer to hold &q, but it can't be const, because
+	 *	clang complains about lack of consting in nested pointer types.
+	 *
+	 *	So we memcpy the value of that pointer, to one that
+	 *	does have a const, which we then pass into d2i_SSL_SESSION *sigh*.
+	 */
+	p = vp->vp_octets;
+	sess = d2i_SSL_SESSION(NULL, &p, vp->vp_length);
+	if (!sess) {
+		RWDEBUG("(TLS) Failed loading persisted session: %s", ERR_error_string(ERR_get_error(), NULL));
+		goto error;
+	}
+
+	talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC);
+	vps = NULL;
+
+	/* move the cached VPs into the session */
+	fr_pair_list_mcopy_by_num(talloc_ctx, &vps, &fake->reply->vps, 0, 0, TAG_ANY);
+
+	SSL_SESSION_set_ex_data(sess, fr_tls_ex_index_vps, vps);
+	RDEBUG("Successfully restored session %s", buffer);
+	rdebug_pair_list(L_DBG_LVL_2, request, vps, "reply:");
+
+	/*
+	 *	The "restore VPs from OpenSSL cache" code is
+	 *	now in eaptls_process()
+	 */
+
+error:
+	if (fake) talloc_free(fake);
+
+	return sess;
+}
+
+#ifdef HAVE_OPENSSL_OCSP_H
+
+/** Extract components of OCSP responser URL from a certificate
+ *
+ * @param[in] cert to extract URL from.
+ * @param[out] host_out Portion of the URL (must be freed with free()).
+ * @param[out] port_out Port portion of the URL (must be freed with free()).
+ * @param[out] path_out Path portion of the URL (must be freed with free()).
+ * @param[out] is_https Whether the responder should be contacted using https.
+ * @return
+ *	- 0 if no valid URL is contained in the certificate.
+ *	- 1 if a URL was found and parsed.
+ *	- -1 if at least one URL was found, but none could be parsed.
+ */
+static int ocsp_parse_cert_url(X509 *cert, char **host_out, char **port_out,
+			       char **path_out, int *is_https)
+{
+	int			i;
+	bool			found_uri = false;
+
+	AUTHORITY_INFO_ACCESS	*aia;
+	ACCESS_DESCRIPTION	*ad;
+
+	aia = X509_get_ext_d2i(cert, NID_info_access, NULL, NULL);
+
+	for (i = 0; i < sk_ACCESS_DESCRIPTION_num(aia); i++) {
+		ad = sk_ACCESS_DESCRIPTION_value(aia, i);
+		if (OBJ_obj2nid(ad->method) != NID_ad_OCSP) continue;
 		if (ad->location->type != GEN_URI) continue;
 		found_uri = true;
 
@@ -1811,7 +2628,7 @@ static ocsp_status_t ocsp_check(REQUEST *request, X509_STORE *store, X509 *issue
 	VALUE_PAIR	*vp;
 
 	if (issuer_cert == NULL) {
-		RWDEBUG("Could not get issuer certificate");
+		RWDEBUG("(TLS) Could not get issuer certificate");
 		goto skipped;
 	}
 
@@ -1836,7 +2653,7 @@ static ocsp_status_t ocsp_check(REQUEST *request, X509_STORE *store, X509 *issue
 		/* Reading the libssl src, they do a strdup on the URL, so it could of been const *sigh* */
 		OCSP_parse_url(url, &host, &port, &path, &use_ssl);
 		if (!host || !port || !path) {
-			RWDEBUG("ocsp: Host or port or path missing from configured URL \"%s\".  Not doing OCSP", url);
+			RWDEBUG("(TLS) ocsp: Host or port or path missing from configured URL \"%s\".  Not doing OCSP", url);
 			goto skipped;
 		}
 	} else {
@@ -1845,15 +2662,15 @@ static ocsp_status_t ocsp_check(REQUEST *request, X509_STORE *store, X509 *issue
 		ret = ocsp_parse_cert_url(client_cert, &host, &port, &path, &use_ssl);
 		switch (ret) {
 		case -1:
-			RWDEBUG("ocsp: Invalid URL in certificate.  Not doing OCSP");
+			RWDEBUG("(TLS) ocsp: Invalid URL in certificate.  Not doing OCSP");
 			break;
 
 		case 0:
 			if (conf->ocsp_url) {
-				RWDEBUG("ocsp: No OCSP URL in certificate, falling back to configured URL");
+				RWDEBUG("(TLS) ocsp: No OCSP URL in certificate, falling back to configured URL");
 				goto use_ocsp_url;
 			}
-			RWDEBUG("ocsp: No OCSP URL in certificate.  Not doing OCSP");
+			RWDEBUG("(TLS) ocsp: No OCSP URL in certificate.  Not doing OCSP");
 			goto skipped;
 
 		case 1:
@@ -1865,7 +2682,7 @@ static ocsp_status_t ocsp_check(REQUEST *request, X509_STORE *store, X509 *issue
 
 	/* Check host and port length are sane, then create Host: HTTP header */
 	if ((strlen(host) + strlen(port) + 2) > sizeof(hostheader)) {
-		RWDEBUG("ocsp: Host and port too long");
+		RWDEBUG("(TLS) ocsp: Host and port too long");
 		goto skipped;
 	}
 	snprintf(hostheader, sizeof(hostheader), "%s:%s", host, port);
@@ -2038,15 +2855,15 @@ ocsp_end:
 		vp = pair_make_request("TLS-OCSP-Cert-Valid", NULL, T_OP_SET);
 		vp->vp_integer = 2;	/* skipped */
 		if (conf->ocsp_softfail) {
-			RWDEBUG("ocsp: Unable to check certificate, assuming it's valid");
-			RWDEBUG("ocsp: This may be insecure");
+			RWDEBUG("(TLS) ocsp: Unable to check certificate, assuming it's valid");
+			RWDEBUG("(TLS) ocsp: This may be insecure");
 
 			/* Remove OpenSSL errors from queue or handshake will fail */
 			while (ERR_get_error());
 
 			ocsp_status = OCSP_STATUS_SKIPPED;
 		} else {
-			REDEBUG("ocsp: Unable to check certificate, failing");
+			REDEBUG("(TLS) ocsp: Unable to check certificate, failing");
 			ocsp_status = OCSP_STATUS_FAILED;
 		}
 		break;
@@ -2054,7 +2871,7 @@ ocsp_end:
 	default:
 		vp = pair_make_request("TLS-OCSP-Cert-Valid", NULL, T_OP_SET);
 		vp->vp_integer = 0;	/* no */
-		REDEBUG("ocsp: Certificate has been expired/revoked");
+		REDEBUG("(TLS) ocsp: Certificate has been expired/revoked");
 		break;
 	}
 
@@ -2087,6 +2904,10 @@ static char const *cert_attr_names[9][2] = {
 #define FR_TLS_SAN_UPN          (7)
 #define FR_TLS_VALID_SINCE	(8)
 
+static const char *cert_names[2] = {
+	"client", "server",
+};
+
 /*
  *	Before trusting a certificate, you must make sure that the
  *	certificate is 'valid'. There are several steps that your
@@ -2183,8 +3004,8 @@ int cbtls_verify(int ok, X509_STORE_CTX *ctx)
 	buf[0] = '\0';
 	sn = X509_get_serialNumber(client_cert);
 
-	RDEBUG2("TLS - Creating attributes from certificate OIDs");
-	RINDENT();
+	RDEBUG2("(TLS) Creating attributes from %s certificate", cert_names[lookup]);
+ 	RINDENT();
 
 	/*
 	 *	For this next bit, we create the attributes *only* if
@@ -2328,8 +3149,14 @@ int cbtls_verify(int ok, X509_STORE_CTX *ctx)
 
 	if (!my_ok) {
 		char const *p = X509_verify_cert_error_string(err);
-		RERROR("SSL says error %d : %s", err, p);
+		RERROR("(TLS) OpenSSL says error %d : %s", err, p);
 		REXDENT();
+
+		/*
+		 *	Copy certs even on failure so that they can be logged.
+		 */
+		if (certs && request) fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, *certs));
+
 		return my_ok;
 	}
 
@@ -2405,7 +3232,6 @@ int cbtls_verify(int ok, X509_STORE_CTX *ctx)
 				fr_bin2hex(value + 2, srcp, asn1len);
 			}
 
-
 			vp = fr_pair_make(talloc_ctx, certs, attribute, value, T_OP_ADD);
 			if (!vp) {
 				RDEBUG3("Skipping %s += '%s'.  Please check that both the "
@@ -2446,20 +3272,28 @@ int cbtls_verify(int ok, X509_STORE_CTX *ctx)
 
 	switch (X509_STORE_CTX_get_error(ctx)) {
 	case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
-		RERROR("issuer=%s", issuer);
+		RERROR("(TLS) unable to get issuer certificate for issuer=%s", issuer);
 		break;
 
 	case X509_V_ERR_CERT_NOT_YET_VALID:
+		RERROR("(TLS) Failed with certificate not yet valid.");
+		break;
+
 	case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
-		RERROR("notBefore=");
+		RERROR("(TLS) Failed with error in certificate 'not before' field.");
 #if 0
 		ASN1_TIME_print(bio_err, X509_get_notBefore(ctx->current_cert));
 #endif
 		break;
 
 	case X509_V_ERR_CERT_HAS_EXPIRED:
+		RERROR("(TLS) Failed with certificate has expired.");
+		break;
+
 	case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
-		RERROR("notAfter=");
+		RERROR("(TLS) Failed with err in certificate 'no after' field..");
+		break;
+
 #if 0
 		ASN1_TIME_print(bio_err, X509_get_notAfter(ctx->current_cert));
 #endif
@@ -2471,12 +3305,49 @@ int cbtls_verify(int ok, X509_STORE_CTX *ctx)
 	 *	checks.
 	 */
 	if (depth == 0) {
+		tls_session_t *ssn = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_SSN);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+		STACK_OF(X509)* untrusted = NULL;
+#endif
+
+		rad_assert(ssn != NULL);
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+		/*
+		 *	See if there are any untrusted certificates.
+		 *	If so, complain about them.
+		 */
+		untrusted = X509_STORE_CTX_get0_untrusted(ctx);
+		if (untrusted) {
+			if (conf->disallow_untrusted || RDEBUG_ENABLED2) {
+				int  i;
+
+				WARN("Certificate chain - %i cert(s) untrusted",
+				     X509_STORE_CTX_get_num_untrusted(ctx));
+				for (i = sk_X509_num(untrusted); i > 0 ; i--) {
+					X509 *this_cert = sk_X509_value(untrusted, i - 1);
+
+					X509_NAME_oneline(X509_get_subject_name(this_cert), subject, sizeof(subject));
+					subject[sizeof(subject) - 1] = '\0';
+
+					WARN("(TLS) untrusted certificate with depth [%i] subject name %s",
+					     i - 1, subject);
+				}
+			}
+
+			if (conf->disallow_untrusted) {
+				AUTH(LOG_PREFIX ": There are untrusted certificates in the certificate chain.  Rejecting.");
+				my_ok = 0;
+			}
+		}
+#endif
+
 		/*
 		 *	If the conf tells us to, check cert issuer
 		 *	against the specified value and fail
 		 *	verification if they don't match.
 		 */
-		if (conf->check_cert_issuer &&
+		if (my_ok && conf->check_cert_issuer &&
 		    (strcmp(issuer, conf->check_cert_issuer) != 0)) {
 			AUTH(LOG_PREFIX ": Certificate issuer (%s) does not match specified value (%s)!",
 			     issuer, conf->check_cert_issuer);
@@ -2595,45 +3466,54 @@ int cbtls_verify(int ok, X509_STORE_CTX *ctx)
 			unlink(filename);
 			break;
 		}
+
+		/*
+		 *	Track that we've verified the client certificate.
+		 */
+		ssn->client_cert_ok = (my_ok == 1);
 	} /* depth == 0 */
 
+	/*
+	 *	Copy certs to request even on failure, so that the
+	 *	user can log them.
+	 */
 	if (certs && request && !my_ok) {
 		fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, *certs));
 	}
 
 	if (RDEBUG_ENABLED3) {
-		RDEBUG3("chain-depth   : %d", depth);
-		RDEBUG3("error         : %d", err);
+		RDEBUG3("(TLS) chain-depth   : %d", depth);
+		RDEBUG3("(TLS) error         : %d", err);
 
 		if (identity) RDEBUG3("identity      : %s", *identity);
-		RDEBUG3("common name   : %s", common_name);
-		RDEBUG3("subject       : %s", subject);
-		RDEBUG3("issuer        : %s", issuer);
-		RDEBUG3("verify return : %d", my_ok);
+		RDEBUG3("(TLS) common name   : %s", common_name);
+		RDEBUG3("(TLS) subject       : %s", subject);
+		RDEBUG3("(TLS) issuer        : %s", issuer);
+		RDEBUG3("(TLS) verify return : %d", my_ok);
 	}
 
 	return (my_ok != 0);
 }
 
 
-#ifdef HAVE_OPENSSL_OCSP_H
 /*
- * 	Create Global X509 revocation store and use it to verify
- * 	OCSP responses
+ * 	Configure a X509 CA store to verify OCSP or client repsonses
  *
  * 	- Load the trusted CAs
  * 	- Load the trusted issuer certificates
+ *	- Configure CRLs check if needed
  */
-static X509_STORE *init_revocation_store(fr_tls_server_conf_t *conf)
+X509_STORE *fr_init_x509_store(fr_tls_server_conf_t *conf)
 {
-	X509_STORE *store = NULL;
+	X509_STORE *store = X509_STORE_new();
 
-	store = X509_STORE_new();
+	if (store == NULL) return NULL;
 
 	/* Load the CAs we trust */
 	if (conf->ca_file || conf->ca_path)
 		if (!X509_STORE_load_locations(store, conf->ca_file, conf->ca_path)) {
 			tls_error_log(NULL, "Error reading Trusted root CA list \"%s\"", conf->ca_file);
+			X509_STORE_free(store);
 			return NULL;
 		}
 
@@ -2647,36 +3527,58 @@ static X509_STORE *init_revocation_store(fr_tls_server_conf_t *conf)
 #endif
 	return store;
 }
-#endif	/* HAVE_OPENSSL_OCSP_H */
 
 #if OPENSSL_VERSION_NUMBER >= 0x0090800fL
 #ifndef OPENSSL_NO_ECDH
 static int set_ecdh_curve(SSL_CTX *ctx, char const *ecdh_curve, bool disable_single_dh_use)
 {
-	int      nid;
-	EC_KEY  *ecdh;
-
-	if (!ecdh_curve || !*ecdh_curve) return 0;
-
-	nid = OBJ_sn2nid(ecdh_curve);
-	if (!nid) {
-		ERROR(LOG_PREFIX ": Unknown ecdh_curve \"%s\"", ecdh_curve);
-		return -1;
+	if (!disable_single_dh_use) {
+		SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE);
 	}
 
-	ecdh = EC_KEY_new_by_curve_name(nid);
-	if (!ecdh) {
-		ERROR(LOG_PREFIX ": Unable to create new curve \"%s\"", ecdh_curve);
-		return -1;
-	}
+	if (!ecdh_curve) return 0;
 
-	SSL_CTX_set_tmp_ecdh(ctx, ecdh);
+#if OPENSSL_VERSION_NUMBER >= 0x1000200fL
+	/*
+	 *	A colon-separated list of curves.
+	 */
+	if (*ecdh_curve) {
+		char *list;
 
-	if (!disable_single_dh_use) {
-		SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE);
+		memcpy(&list, &ecdh_curve, sizeof(list)); /* const issues */
+
+		if (SSL_CTX_set1_curves_list(ctx, list) == 0) {
+			ERROR(LOG_PREFIX ": Unknown ecdh_curve \"%s\"", ecdh_curve);
+			return -1;
+		}
 	}
 
-	EC_KEY_free(ecdh);
+	(void) SSL_CTX_set_ecdh_auto(ctx, 1);
+#else
+	/*
+	 *	Use APIs for older versions of OpenSSL.
+	 */
+	{
+		int      nid;
+		EC_KEY  *ecdh;
+
+		nid = OBJ_sn2nid(ecdh_curve);
+		if (!nid) {
+			ERROR(LOG_PREFIX ": Unknown ecdh_curve \"%s\"", ecdh_curve);
+			return -1;
+		}
+
+		ecdh = EC_KEY_new_by_curve_name(nid);
+		if (!ecdh) {
+			ERROR(LOG_PREFIX ": Unable to create new curve \"%s\"", ecdh_curve);
+			return -1;
+		}
+
+		SSL_CTX_set_tmp_ecdh(ctx, ecdh);
+
+		EC_KEY_free(ecdh);
+	}
+#endif
 
 	return 0;
 }
@@ -2708,10 +3610,32 @@ int tls_global_init(bool spawn_flag, bool check)
 	 *	and we don't want to have tls.c depend on globals.
 	 */
 	if (spawn_flag && !check && (tls_mutexes_init() < 0)) {
-		ERROR("FATAL: Failed to set up SSL mutexes");
+		ERROR("(TLS) FATAL: Failed to set up SSL mutexes");
 		return -1;
 	}
 
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+	/*
+	 *	Load the default provider for most algorithms
+	 */
+	openssl_default_provider = OSSL_PROVIDER_load(NULL, "default");
+	if (!openssl_default_provider) {
+		ERROR("(TLS) Failed loading default provider");
+		return -1;
+	}
+
+	/*
+	 *	Needed for MD4
+	 *
+	 *	https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Legacy-Algorithms
+	 */
+	openssl_legacy_provider = OSSL_PROVIDER_load(NULL, "legacy");
+	if (!openssl_legacy_provider) {
+		ERROR("(TLS) Failed loading legacy provider");
+		return -1;
+	}
+#endif
+
 	return 0;
 }
 
@@ -2777,6 +3701,19 @@ void tls_global_cleanup(void)
 #ifndef OPENSSL_NO_ENGINE
 	ENGINE_cleanup();
 #endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+	if (openssl_default_provider && !OSSL_PROVIDER_unload(openssl_default_provider)) {
+		ERROR("Failed unloading default provider");
+	}
+	openssl_default_provider = NULL;
+
+	if (openssl_legacy_provider && !OSSL_PROVIDER_unload(openssl_legacy_provider)) {
+		ERROR("Failed unloading legacy provider");
+	}
+	openssl_legacy_provider = NULL;
+#endif
+
 	CONF_modules_unload(1);
 	ERR_free_strings();
 	EVP_cleanup();
@@ -2797,9 +3734,6 @@ static const FR_NAME_NUMBER version2int[] = {
 #endif
 #ifdef TLS1_3_VERSION
 	{ "1.3",    TLS1_3_VERSION },
-#endif
-#ifdef TLS1_4_VERSION
-	{ "1.4",    TLS1_4_VERSION },
 #endif
 	{ NULL, 0 }
 };
@@ -2816,18 +3750,18 @@ static const FR_NAME_NUMBER version2int[] = {
  * - Load the Private key & the certificate
  * - Set the Context options & Verify options
  */
-SSL_CTX *tls_init_ctx(fr_tls_server_conf_t *conf, int client)
+SSL_CTX *tls_init_ctx(fr_tls_server_conf_t *conf, int client, char const *chain_file, char const *private_key_file)
 {
 	SSL_CTX		*ctx;
 	X509_STORE	*certstore;
 	int		verify_mode = SSL_VERIFY_NONE;
-	int		ctx_options = 0;
-	int		ctx_tls_versions = 0;
+	int		ctx_options = 0, ctx_available = 0;
 	int		type;
 #ifdef CHECK_FOR_PSK_CERTS
 	bool		psk_and_certs = false;
 #endif
-	bool		insecure_tls_version = false;
+	int		min_version;
+	int		max_version;
 
 	/*
 	 *	SHA256 is in all versions of OpenSSL, but isn't
@@ -2840,7 +3774,7 @@ SSL_CTX *tls_init_ctx(fr_tls_server_conf_t *conf, int client)
 
 	ctx = SSL_CTX_new(SSLv23_method()); /* which is really "all known SSL / TLS methods".  Idiots. */
 	if (!ctx) {
-		tls_error_log(NULL, "Failed creating TLS context");
+		tls_error_log(NULL, "Failed creating OpenSSL context");
 		return NULL;
 	}
 
@@ -3033,39 +3967,55 @@ SSL_CTX *tls_init_ctx(fr_tls_server_conf_t *conf, int client)
 	 *	the cert chain needs to be given in PEM from
 	 *	openSSL.org
 	 */
-	if (!conf->certificate_file) goto load_ca;
+	if (!chain_file) chain_file = conf->certificate_file;
+	if (!chain_file) goto load_ca;
 
 	if (type == SSL_FILETYPE_PEM) {
-		if (!(SSL_CTX_use_certificate_chain_file(ctx, conf->certificate_file))) {
+		if (!(SSL_CTX_use_certificate_chain_file(ctx, chain_file))) {
 			tls_error_log(NULL, "Failed reading certificate file \"%s\"",
-				      conf->certificate_file);
+				      chain_file);
 			return NULL;
 		}
 
-	} else if (!(SSL_CTX_use_certificate_file(ctx, conf->certificate_file, type))) {
+	} else if (!(SSL_CTX_use_certificate_file(ctx, chain_file, type))) {
 		tls_error_log(NULL, "Failed reading certificate file \"%s\"",
-			      conf->certificate_file);
+			      chain_file);
 		return NULL;
 	}
 
-	/* Load the CAs we trust */
 load_ca:
+	/*
+	 *	Load the CAs we trust and configure CRL checks if needed
+	 */
 #if defined(X509_V_FLAG_PARTIAL_CHAIN)
 	X509_STORE_set_flags(SSL_CTX_get_cert_store(ctx), X509_V_FLAG_PARTIAL_CHAIN);
 #endif
 	if (conf->ca_file || conf->ca_path) {
-		if (!SSL_CTX_load_verify_locations(ctx, conf->ca_file, conf->ca_path)) {
-			tls_error_log(NULL, "Failed reading Trusted root CA list \"%s\"",
-				      conf->ca_file);
-			return NULL;
-		}
+		if ((certstore = fr_init_x509_store(conf)) == NULL ) return NULL;
+		SSL_CTX_set_cert_store(ctx, certstore);
 	}
+
 	if (conf->ca_file && *conf->ca_file) SSL_CTX_set_client_CA_list(ctx, SSL_load_client_CA_file(conf->ca_file));
 
-	if (conf->private_key_file) {
-		if (!(SSL_CTX_use_PrivateKey_file(ctx, conf->private_key_file, type))) {
+	conf->ca_path_last_reload = time(NULL);
+	conf->old_x509_store = NULL;
+
+	/*
+	 * Disable reloading of cert store if we're not using CA path
+	 */
+	if (!conf->ca_path) conf->ca_path_reload_interval = 0;
+
+	if (conf->ca_path_reload_interval > 0 && conf->ca_path_reload_interval < 300) {
+		DEBUG2("ca_path_reload_interval is set too low, reset it to 300");
+		conf->ca_path_reload_interval = 300;
+	}
+
+	/* Load private key */
+	if (!private_key_file) private_key_file = conf->private_key_file;
+	if (private_key_file) {
+		if (!(SSL_CTX_use_PrivateKey_file(ctx, private_key_file, type))) {
 			tls_error_log(NULL, "Failed reading private key file \"%s\"",
-				      conf->private_key_file);
+				      private_key_file);
 			return NULL;
 		}
 
@@ -3088,6 +4038,18 @@ post_ca:
 	ctx_options |= SSL_OP_NO_SSLv2;
 	ctx_options |= SSL_OP_NO_SSLv3;
 
+	/*
+	 *	If set then dummy Change Cipher Spec (CCS) messages are sent in
+	 *	TLSv1.3. This has the effect of making TLSv1.3 look more like TLSv1.2
+	 *	so that middleboxes that do not understand TLSv1.3 will not drop
+	 *	the connection. This isn't needed for EAP-TLS, so we disable it.
+	 *
+	 *	EAP (hopefully) does not have middlebox deployments
+	 */
+#ifdef SSL_OP_ENABLE_MIDDLEBOX_COMPAT
+	ctx_options &= ~SSL_OP_ENABLE_MIDDLEBOX_COMPAT;
+#endif
+
 	/*
 	 *	SSL_CTX_set_(min|max)_proto_version was included in OpenSSL 1.1.0
 	 *
@@ -3095,168 +4057,250 @@ post_ca:
 	 *	below, so we don't need to check for them explicitly.
 	 *
 	 *	TLS1_3_VERSION is available in OpenSSL 1.1.1.
-	 *
-	 *	TLS1_4_VERSION in speculative.
 	 */
-	{
-		int min_version = 0;
-		int max_version = 0;
 
+	/*
+	 *	Get the max version from the configuration files.
+	 */
+	if (conf->tls_max_version && *conf->tls_max_version) {
+		max_version = fr_str2int(version2int, conf->tls_max_version, 0);
+		if (!max_version) {
+			ERROR("Invalid value for tls_max_version '%s'", conf->tls_max_version);
+			return NULL;
+		}
+	} else {
 		/*
-		 *	Get the max version.
+		 *	Pick the maximum version available at compile
+		 *	time.
 		 */
-		if (conf->tls_max_version && *conf->tls_max_version) {
-			max_version = fr_str2int(version2int, conf->tls_max_version, 0);
-			if (!max_version) {
-				ERROR("Invalid value for tls_max_version '%s'", conf->tls_max_version);
-				return NULL;
-			}
-		} else {
-			/*
-			 *	Pick the maximum one we know about.
-			 */
-#ifdef TLS1_4_VERSION
-			max_version = TLS1_2_VERSION; /* NOT a typo! EAP methods for TLS 1.4 are NOT finished */
-#elif defined(TLS1_3_VERSION)
-			max_version = TLS1_2_VERSION; /* NOT a typo! EAP methods for TLS 1.3 are NOT finished */
+#if defined(TLS1_3_VERSION)
+		max_version = TLS1_2_VERSION; /* yes, we only use TLS 1.3 if it's EXPLICITELY ENABLED */
 #elif defined(TLS1_2_VERSION)
-			max_version = TLS1_2_VERSION;
+		max_version = TLS1_2_VERSION;
 #elif defined(TLS1_1_VERSION)
-			max_version = TLS1_1_VERSION;
+		max_version = TLS1_1_VERSION;
 #else
-			max_version = TLS1_VERSION;
+		max_version = TLS1_VERSION;
 #endif
-		}
+	}
 
+	/*
+	 *	Get the min version from the configuration files.
+	 */
+	if (conf->tls_min_version && *conf->tls_min_version) {
+		min_version = fr_str2int(version2int, conf->tls_min_version, 0);
+		if (!min_version) {
+			ERROR("Unknown or unsupported value for tls_min_version '%s'", conf->tls_min_version);
+			return NULL;
+		}
+	} else {
 		/*
-		 *	Set these for the rest of the code.
+		 *	Allow TLS 1.0.  It is horribly insecure, but
+		 *	some systems still use it.
 		 */
+		min_version = TLS1_VERSION;
+	}
+
+	/*
+	 *	Compare the two.
+	 */
+	if ((min_version > max_version) || (max_version < min_version)) {
+		ERROR("tls_min_version '%s' must be <= tls_max_version '%s'",
+		      conf->tls_min_version, conf->tls_max_version);
+		return NULL;
+	}
+
+#ifdef CHECK_FOR_PSK_CERTS
+	/*
+	 *	Disable TLS 1.3 when using PSKs and certs.
+	 *	This doesn't work.
+	 *
+	 *	It's best to disable the offending
+	 *	configuration and warn about it.  The
+	 *	alternative is to have the admin wonder why it
+	 *	doesn't work.
+	 *
+	 *	Note that the admin can over-ride this by
+	 *	setting "min_version = max_version = 1.3"
+	 */
+	if (psk_and_certs &&
+	    (min_version < TLS1_3_VERSION) && (max_version >= TLS1_3_VERSION)) {
+		max_version = TLS1_2_VERSION;
+		radlog(L_DBG | L_WARN, "Disabling TLS 1.3 due to PSK and certificates being configured simultaneously.  This is not supported by the standards.");
+	}
+#endif
+
+	/*
+	 *	No one should be using TLS 1.0 or TLS 1.1 any more
+	 *
+	 *	If TLS1.2 isn't defined by OpenSSL, then we _know_
+	 *	it's an insecure version of OpenSSL.
+	 */
 #ifdef TLS1_2_VERSION
-		if (max_version < TLS1_2_VERSION) {
-			conf->disable_tlsv1_2 = true;
-		}
+	if (max_version < TLS1_2_VERSION)
 #endif
-#ifdef TLS1_1_VERSION
-		if (max_version < TLS1_1_VERSION) {
-			conf->disable_tlsv1_1 = true;
+	{
+		if (rad_debug_lvl) {
+			WARN(LOG_PREFIX ": The configuration allows TLS 1.0 and/or TLS 1.1.  We STRONGLY recommned using only TLS 1.2 for security");
+			WARN(LOG_PREFIX ": Please set: tls_min_version = '1.2'");
 		}
-#endif
+	}
 
-		/*
-		 *	Get the min version.
-		 */
-		if (conf->tls_min_version && *conf->tls_min_version) {
-			min_version = fr_str2int(version2int, conf->tls_min_version, 0);
-			if (!min_version) {
-				ERROR("Unknown or unsupported value for tls_min_version '%s'", conf->tls_min_version);
-				return NULL;
-			}
-		} else {
-			min_version = TLS1_VERSION;
+#ifdef SSL_OP_NO_TLSv1
+	/*
+	 *	Check min / max against the old-style "disable" flag.
+	 */
+	if (conf->disable_tlsv1) {
+		if (min_version == TLS1_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1' is set, but 'min_version = 1.0'.  These cannot both be true.");
+			return NULL;
 		}
-
-		/*
-		 *	Compare the two.
-		 */
-		if (min_version > max_version) {
-			ERROR("tls_min_version '%s' must be <= tls_max_version '%s'",
-			      conf->tls_min_version, conf->tls_max_version);
+		if (max_version == TLS1_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1' is set, but 'max_version = 1.0'.  These cannot both be true.");
 			return NULL;
 		}
+		ctx_options |= SSL_OP_NO_TLSv1;
+	}
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-#ifdef CHECK_FOR_PSK_CERTS
-		/*
-		 *	Disable TLS 1.3 when using PSKs and certs.
-		 *	This doesn't work.
-		 *
-		 *	It's best to disable the offending
-		 *	configuration and warn about it.  The
-		 *	alternative is to have the admin wonder why it
-		 *	doesn't work.
-		 *
-		 *	Note that the admin can over-ride this by
-		 *	setting "min_version = max_version = 1.3"
-		 */
-		if (psk_and_certs &&
-		    (min_version < TLS1_3_VERSION) && (max_version >= TLS1_3_VERSION)) {
-			max_version = TLS1_2_VERSION;
-			radlog(L_DBG | L_WARN, "Disabling TLS 1.3 due to PSK and certificates being configured simultaneously.  This is not supported by the standards.");
-		}
+	if (min_version > TLS1_VERSION) ctx_options |= SSL_OP_NO_TLSv1;
+
+	ctx_available |= SSL_OP_NO_TLSv1;
 #endif
 
-		if (!SSL_CTX_set_max_proto_version(ctx, max_version)) {
-			ERROR("Failed setting TLS maximum version");
+#ifdef SSL_OP_NO_TLSv1_1
+	/*
+	 *	Check min / max against the old-style "disable" flag.
+	 */
+	if (conf->disable_tlsv1_1) {
+		if (min_version <= TLS1_1_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'min_version <= 1.1'.  These cannot both be true.");
 			return NULL;
 		}
-
-		if (!SSL_CTX_set_min_proto_version(ctx, min_version)) {
-			ERROR("Failed setting TLS minimum version");
+		if (max_version == TLS1_1_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'max_version = 1.1'.  These cannot both be true.");
 			return NULL;
 		}
+		ctx_options |= SSL_OP_NO_TLSv1_1;
+	}
 
-		/*
-		 *	No one should be using TLS 1.0 or TLS 1.1 any more
-		 */
-		if (min_version < TLS1_2_VERSION) insecure_tls_version = true;
-#else  /* OpenSSL version < 1.1.0 */
+	if (min_version > TLS1_1_VERSION) ctx_options |= SSL_OP_NO_TLSv1_1;
+	if (max_version < TLS1_1_VERSION) ctx_options |= SSL_OP_NO_TLSv1_1;
 
-#ifdef SSL_OP_NO_TLSv1
-		insecure_tls_version |= (conf->disable_tlsv1 == false);
+	ctx_available |= SSL_OP_NO_TLSv1_1;
 #endif
-#ifdef SSL_OP_NO_TLSv1_1
-		insecure_tls_version |= (conf->disable_tlsv1_1 == false);
-#endif
-#endif	/* OpenSSL version ? 1.1.0 */
 
-		if (rad_debug_lvl && insecure_tls_version) {
-			WARN("The configuration allows TLS 1.0 and/or TLS 1.1.  We STRONGLY recommned using only TLS 1.2 for security");
-			WARN("Please set: tls_min_version = \"1.2\"");
+#ifdef SSL_OP_NO_TLSv1_2
+	/*
+	 *	Check min / max against the old-style "disable" flag.
+	 */
+	if (conf->disable_tlsv1_2) {
+		if (min_version <= TLS1_2_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1_2' is set, but 'min_version <= 1.2'.  These cannot both be true.");
+			return NULL;
+		}
+		if (max_version == TLS1_2_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'max_version = 1.2'.  These cannot both be true.");
+			return NULL;
 		}
+		ctx_options |= SSL_OP_NO_TLSv1_2;
 	}
+	ctx_available |= SSL_OP_NO_TLSv1_2;
+
+	if (min_version > TLS1_2_VERSION) ctx_options |= SSL_OP_NO_TLSv1_2;
+	if (max_version < TLS1_2_VERSION) ctx_options |= SSL_OP_NO_TLSv1_2;
+#endif
+
+#ifdef SSL_OP_NO_TLSv1_3
+	ctx_available |= SSL_OP_NO_TLSv1_3;
+	if (min_version > TLS1_3_VERSION) ctx_options |= SSL_OP_NO_TLSv1_3;
+	if (max_version < TLS1_3_VERSION) ctx_options |= SSL_OP_NO_TLSv1_3;
+#endif
 
 	/*
-	 *	For historical config compatibility, we also allow
-	 *	these, but complain if the admin uses them.
+	 *	Set the cipher list if we were told to do so.  We do
+	 *	this before setting min/max TLS version.  In a sane
+	 *	world, OpenSSL would error out if we set the max TLS
+	 *	version to something which was unsupported by the
+	 *	current security level.  However, this is OpenSSL.  If
+	 *	you set conflicting options, it doesn't give an error.
+	 *	Instead, it just picks something to do.
 	 */
-#ifdef SSL_OP_NO_TLSv1
-	if (conf->disable_tlsv1) {
-		ctx_options |= SSL_OP_NO_TLSv1;
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-		WARN("Please use tls_min_version and tls_max_version instead of disable_tlsv1");
-#endif
+	if (conf->cipher_list) {
+		if (!SSL_CTX_set_cipher_list(ctx, conf->cipher_list)) {
+			tls_error_log(NULL, "Failed setting cipher list");
+			return NULL;
+		}
 	}
 
-	ctx_tls_versions |= SSL_OP_NO_TLSv1;
-#endif
-#ifdef SSL_OP_NO_TLSv1_1
-	if (conf->disable_tlsv1_1) {
-		ctx_options |= SSL_OP_NO_TLSv1_1;
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-		WARN("Please use tls_min_version and tls_max_version instead of disable_tlsv1_2");
+	/*
+	 *	Tell OpenSSL PRETTY PLEASE MAY WE USE TLS 1.1.
+	 *
+	 *	Because saying "use TLS 1.1" isn't enough.  We have to
+	 *	send it flowers and cake.
+	 */
+	if (min_version <= TLS1_1_VERSION) {
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+		int seclevel = SSL_CTX_get_security_level(ctx);
+		int required;;
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+		required = 0;
+#else
+		required = 1;
 #endif
-	}
 
-	ctx_tls_versions |= SSL_OP_NO_TLSv1_1;
+		if (seclevel != required) {
+			WARN(LOG_PREFIX ": In order to use TLS 1.0 and/or TLS 1.1, you likely need to set: cipher_list = \"DEFAULT@SECLEVEL=%d\"", required);
+		}
+
+#else
+		/*
+		 *	No API to get the security level.  Just guess based on the string in the cipher_list.
+		 */
+		if (conf->cipher_list &&
+		    !strstr(conf->cipher_list, "DEFAULT@SECLEVEL=1")) {
+			WARN(LOG_PREFIX ": In order to use TLS 1.0 and/or TLS 1.1, you likely need to set: cipher_list = \"DEFAULT@SECLEVEL=1\"");
+		}
 #endif
-#ifdef SSL_OP_NO_TLSv1_2
+	}
 
-	if (conf->disable_tlsv1_2) {
-		ctx_options |= SSL_OP_NO_TLSv1_2;
 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
-		WARN("Please use tls_min_version and tls_max_version instead of disable_tlsv1_2");
-#endif
+	if (conf->disable_tlsv1) {
+		WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1'");
+	}
+	if (conf->disable_tlsv1_1) {
+		WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1_1'");
+	}
+	if (conf->disable_tlsv1_2) {
+		WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1_2'");
 	}
 
-	ctx_tls_versions |= SSL_OP_NO_TLSv1_2;
+	ctx_options &= ~(ctx_available); /* clear these flags, as they're not needed. */
 
-#endif
+	if (!SSL_CTX_set_max_proto_version(ctx, max_version)) {
+		ERROR("Failed setting TLS maximum version");
+		return NULL;
+	}
+
+	if (!SSL_CTX_set_min_proto_version(ctx, min_version)) {
+		ERROR("Failed setting TLS minimum version");
+		return NULL;
+	}
+#endif	/* OpenSSL version < 1.1.0 */
 
-	if ((ctx_options & ctx_tls_versions) == ctx_tls_versions) {
+	if ((ctx_options & ctx_available) == ctx_available) {
 		ERROR(LOG_PREFIX ": You have disabled all available TLS versions.  EAP will not work");
 		return NULL;
 	}
 
+	/*
+	 *	Cache min / max TLS version so that we can
+	 *	programatically disable TLS 1.3 for TTLS, PEAP, and
+	 *	FAST.
+	 */
+	conf->min_version = min_version;
+	conf->max_version = max_version;
+
 #ifdef SSL_OP_NO_TICKET
 	ctx_options |= SSL_OP_NO_TICKET;
 #endif
@@ -3291,6 +4335,19 @@ post_ca:
 
 	SSL_CTX_set_options(ctx, ctx_options);
 
+	/*
+	 *	TLS 1.3 introduces the concept of early data (also known as zero
+	 *	round trip data or 0-RTT data). Early data allows a client to send
+	 *	data to a server in the first round trip of a connection, without
+	 *	waiting for the TLS handshake to complete if the client has spoken
+	 *	to the same server recently. This doesn't work for EAP, so we
+	 *	disable early data.
+	 *
+	 */
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+	SSL_CTX_set_max_early_data(ctx, 0);
+#endif
+
 	/*
 	 *	TODO: Set the RSA & DH
 	 *	SSL_CTX_set_tmp_rsa_callback(ctx, cbtls_rsa);
@@ -3336,12 +4393,21 @@ post_ca:
 		/*
 		 *	Cache sessions on disk if requested.
 		 */
-		if (conf->session_cache_path) {
+		if (conf->session_cache_path && *conf->session_cache_path) {
 			SSL_CTX_sess_set_new_cb(ctx, cbtls_new_session);
 			SSL_CTX_sess_set_get_cb(ctx, cbtls_get_session);
 			SSL_CTX_sess_set_remove_cb(ctx, cbtls_remove_session);
 		}
 
+		/*
+		 *	Or run the cache through a virtual server.
+		 */
+		if (conf->session_cache_server && *conf->session_cache_server) {
+			SSL_CTX_sess_set_new_cb(ctx, cbtls_cache_save);
+			SSL_CTX_sess_set_get_cb(ctx, cbtls_cache_load);
+			SSL_CTX_sess_set_remove_cb(ctx, cbtls_cache_clear);
+		}
+
 		SSL_CTX_set_quiet_shutdown(ctx, 1);
 		if (fr_tls_ex_index_vps < 0)
 			fr_tls_ex_index_vps = SSL_SESSION_get_ex_new_index(0, NULL, NULL, NULL, NULL);
@@ -3359,6 +4425,17 @@ post_ca:
 		}
 		X509_STORE_set_flags(certstore, X509_V_FLAG_CRL_CHECK);
 
+#ifdef X509_V_FLAG_USE_DELTAS
+		/*
+		 *	If set, delta CRLs (if present) are used to
+		 *	determine certificate status. If not set
+		 *	deltas are ignored.
+		 *
+		 *	So it's safe to always set this flag.
+		 */
+		X509_STORE_set_flags(certstore, X509_V_FLAG_USE_DELTAS);
+#endif
+
 #ifdef X509_V_FLAG_CRL_CHECK_ALL
 		if (conf->check_all_crl)
 			X509_STORE_set_flags(certstore, X509_V_FLAG_CRL_CHECK_ALL);
@@ -3389,16 +4466,6 @@ post_ca:
 	}
 #endif
 
-	/*
-	 * Set the cipher list if we were told to
-	 */
-	if (conf->cipher_list) {
-		if (!SSL_CTX_set_cipher_list(ctx, conf->cipher_list)) {
-			tls_error_log(NULL, "Failed setting cipher list");
-			return NULL;
-		}
-	}
-
 	/*
 	 *	Setup session caching
 	 */
@@ -3424,9 +4491,9 @@ post_ca:
 					       (unsigned int) strlen(conf->session_context_id));
 
 		/*
-		 *	Our timeout is in hours, this is in seconds.
+		 *	Our lifetime is in hours, this is in seconds.
 		 */
-		SSL_CTX_set_timeout(ctx, conf->session_timeout * 3600);
+		SSL_CTX_set_timeout(ctx, conf->session_lifetime * 3600);
 
 		/*
 		 *	Set the maximum number of entries in the
@@ -3468,11 +4535,15 @@ static int _tls_server_conf_free(fr_tls_server_conf_t *conf)
 
 	if (conf->cache_ht) fr_hash_table_free(conf->cache_ht);
 
+	pthread_mutex_destroy(&conf->mutex);
+
 #ifdef HAVE_OPENSSL_OCSP_H
 	if (conf->ocsp_store) X509_STORE_free(conf->ocsp_store);
 	conf->ocsp_store = NULL;
 #endif
 
+	if (conf->realms) fr_hash_table_free(conf->realms);
+
 #ifndef NDEBUG
 	memset(conf, 0, sizeof(*conf));
 #endif
@@ -3505,9 +4576,109 @@ static int store_cmp(void const *a, void const *b)
 	DICT_ATTR const *one = a;
 	DICT_ATTR const *two = b;
 
-	return one - two;
+	return (one < two) - (one > two);
+}
+
+static uint32_t realm_hash(void const *data)
+{
+	fr_realm_ctx_t const *r = data;
+
+	return fr_hash_string(r->name);
+}
+
+static int realm_cmp(void const *a, void const *b)
+{
+	fr_realm_ctx_t const *one = a;
+	fr_realm_ctx_t const *two = b;
+
+	return strcmp(one->name, two->name);
 }
 
+static void realm_free(void *data)
+{
+	fr_realm_ctx_t *r = data;
+
+	SSL_CTX_free(r->ctx);
+}
+
+static int tls_realms_load(fr_tls_server_conf_t *conf)
+{
+	fr_hash_table_t *ht;
+	DIR		*dir;
+	struct dirent	*dp;
+	char		buffer[PATH_MAX];
+	char		buffer2[PATH_MAX];
+
+	ht = fr_hash_table_create(realm_hash, realm_cmp, realm_free);
+	if (!ht) return -1;
+
+	dir = opendir(conf->realm_dir);
+	if (!dir) {
+		ERROR("Error reading directory %s: %s", conf->realm_dir, fr_syserror(errno));
+	error:
+		if (dir) closedir(dir);
+		fr_hash_table_free(ht);
+		return -1;
+	}
+
+	/*
+	 *	Read only the PEM files
+	 */
+	while ((dp = readdir(dir)) != NULL) {
+		char *p;
+		struct stat stat_buf;
+		SSL_CTX *ctx;
+		fr_realm_ctx_t *r;
+		char const *private_key_file = buffer;
+
+		if (dp->d_name[0] == '.') continue;
+
+		p = strrchr(dp->d_name, '.');
+		if (!p) continue;
+
+		if (memcmp(p, ".pem", 5) != 0) continue; /* must END in .pem */
+
+		snprintf(buffer, sizeof(buffer), "%s/%s", conf->realm_dir, dp->d_name); /* ignore directories */
+		if ((stat(buffer, &stat_buf) != 0) ||
+		    S_ISDIR(stat_buf.st_mode)) continue;
+
+		strcpy(buffer2, buffer);
+		p = strchr(buffer2, '.'); /* which must be there... */
+		if (!p) continue;
+
+		/*
+		 *	If there's a key file, then use that.
+		 *	Otherwise assume that the private key is in
+		 *	the chain file.
+		 */
+		strcpy(p, ".key");
+		if (stat(buffer2, &stat_buf) != 0) private_key_file = buffer2;
+
+		ctx = tls_init_ctx(conf, 1, buffer, private_key_file);
+		if (!ctx) goto error;
+
+		r = talloc_zero(conf, fr_realm_ctx_t);
+		if (!r) {
+			SSL_CTX_free(ctx);
+			goto error;
+		}
+
+		r->name = talloc_strdup(r, buffer);
+		r->ctx = ctx;
+
+		if (fr_hash_table_insert(ht, r) < 0) {
+			ERROR("Failed inserting certificate file %s into hash table", buffer);
+			goto error;
+		}
+	}
+
+	conf->realms = ht;
+	closedir(dir);
+
+	return 0;
+}
+
+
 fr_tls_server_conf_t *tls_server_conf_parse(CONF_SECTION *cs)
 {
 	fr_tls_server_conf_t *conf;
@@ -3535,6 +4706,16 @@ fr_tls_server_conf_t *tls_server_conf_parse(CONF_SECTION *cs)
 	 */
 	if (conf->fragment_size < 100) conf->fragment_size = 100;
 
+	/*
+	 *	Disallow sessions of more than 7 days, as per RFC
+	 *	8446.
+	 *
+	 *	Note that we also enforce this on TLS 1.2, etc.
+	 *	Because there's just no reason to have month-long TLS
+	 *	sessions.
+	 */
+	if (conf->session_lifetime > (7 * 24)) conf->session_lifetime = 7 * 24;
+
 	/*
 	 *	Only check for certificate things if we don't have a
 	 *	PSK query.
@@ -3563,10 +4744,15 @@ fr_tls_server_conf_t *tls_server_conf_parse(CONF_SECTION *cs)
 		}
 	}
 
+	/*
+	 *	Initialize configuration mutex
+	 */
+	pthread_mutex_init(&conf->mutex, NULL);
+
 	/*
 	 *	Initialize TLS
 	 */
-	conf->ctx = tls_init_ctx(conf, 0);
+	conf->ctx = tls_init_ctx(conf, 0, NULL, NULL);
 	if (conf->ctx == NULL) {
 		goto error;
 	}
@@ -3633,10 +4819,11 @@ skip_list:
 	 * 	Initialize OCSP Revocation Store
 	 */
 	if (conf->ocsp_enable) {
-		conf->ocsp_store = init_revocation_store(conf);
+		conf->ocsp_store = fr_init_x509_store(conf);
 		if (conf->ocsp_store == NULL) goto error;
 	}
 #endif /*HAVE_OPENSSL_OCSP_H*/
+
 	{
 		char *dh_file;
 
@@ -3655,7 +4842,7 @@ skip_list:
 	}
 
 	if (conf->verify_client_cert_cmd && !conf->verify_tmp_dir) {
-		ERROR(LOG_PREFIX ": You MUST set the verify directory in order to use verify_client_cmd");
+		ERROR(LOG_PREFIX ": You MUST set the 'tmpdir' directory in order to use '%s' cmd", conf->verify_client_cert_cmd);
 		goto error;
 	}
 
@@ -3663,12 +4850,17 @@ skip_list:
 	/*
 	 *	OpenSSL 1.0.1f and 1.0.1g get the MS-MPPE keys wrong.
 	 */
-#if (OPENSSL_VERSION_NUMBER >= 0x10010060L) && (OPENSSL_VERSION_NUMBER < 0x10010060L)
+#if (OPENSSL_VERSION_NUMBER >= 0x1010106L) && (OPENSSL_VERSION_NUMBER <= 0x1010107L)
 	conf->disable_tlsv1_2 = true;
 	WARN(LOG_PREFIX ": Disabling TLSv1.2 due to OpenSSL bugs");
 #endif
 #endif
 
+	/*
+	 *	Load certificates and private keys from the realm directory.
+	 */
+	if (conf->realm_dir && (tls_realms_load(conf) < 0)) goto error;
+
 	/*
 	 *	Cache conf in cs in case we're asked to parse this again.
 	 */
@@ -3703,7 +4895,7 @@ fr_tls_server_conf_t *tls_client_conf_parse(CONF_SECTION *cs)
 	/*
 	 *	Initialize TLS
 	 */
-	conf->ctx = tls_init_ctx(conf, 1);
+	conf->ctx = tls_init_ctx(conf, 1, NULL, NULL);
 	if (conf->ctx == NULL) {
 		goto error;
 	}
@@ -3755,7 +4947,7 @@ int tls_success(tls_session_t *ssn, REQUEST *request)
 		 *	not allowed,
 		 */
 		if (SSL_session_reused(ssn->ssl)) {
-			RDEBUG("Forcibly stopping session resumption as it is not allowed");
+			RDEBUG("(TLS) cache - Forcibly stopping session resumption as it is administratively disabled.");
 			return -1;
 		}
 
@@ -3763,12 +4955,14 @@ int tls_success(tls_session_t *ssn, REQUEST *request)
 	 *	Else resumption IS allowed, so we store the
 	 *	user data in the cache.
 	 */
-	} else if (!SSL_session_reused(ssn->ssl)) {
+	} else if ((!SSL_session_reused(ssn->ssl)) || ssn->session_not_resumed) {
 		VALUE_PAIR **certs;
 		char buffer[2 * MAX_SESSION_SIZE + 1];
 
 		tls_session_id(ssn->ssl_session, buffer, MAX_SESSION_SIZE);
 
+		RDEBUG("(TLS) cache - Setting up attributes for session resumption");
+
 		vp = fr_pair_list_copy_by_num(talloc_ctx, request->reply->vps, PW_USER_NAME, 0, TAG_ANY);
 		if (vp) fr_pair_add(&vps, vp);
 
@@ -3778,6 +4972,9 @@ int tls_success(tls_session_t *ssn, REQUEST *request)
 		vp = fr_pair_list_copy_by_num(talloc_ctx, request->packet->vps, PW_STRIPPED_USER_DOMAIN, 0, TAG_ANY);
 		if (vp) fr_pair_add(&vps, vp);
 
+		vp = fr_pair_list_copy_by_num(talloc_ctx, request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY);
+		if (vp) fr_pair_add(&vps, vp);
+
 		vp = fr_pair_list_copy_by_num(talloc_ctx, request->reply->vps, PW_CHARGEABLE_USER_IDENTITY, 0, TAG_ANY);
 		if (vp) fr_pair_add(&vps, vp);
 
@@ -3836,7 +5033,7 @@ int tls_success(tls_session_t *ssn, REQUEST *request)
 				if (vp) {
 					if ((request->timestamp + vp->vp_integer) > expires) {
 						vp->vp_integer = expires - request->timestamp;
-						RWDEBUG2("Updating Session-Timeout to %u, due to impending certificate expiration",
+						RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration",
 							 vp->vp_integer);
 					}
 				}
@@ -3858,7 +5055,7 @@ int tls_success(tls_session_t *ssn, REQUEST *request)
 					 FR_DIR_SEP, buffer);
 				vp_file = fopen(filename, "w");
 				if (vp_file == NULL) {
-					RWDEBUG("Could not write session VPs to persistent cache: %s",
+					RWDEBUG("(TLS) Could not write session VPs to persistent cache: %s",
 						fr_syserror(errno));
 				} else {
 					VALUE_PAIR *prev = NULL;
@@ -3889,6 +5086,10 @@ int tls_success(tls_session_t *ssn, REQUEST *request)
 					fprintf(vp_file, "\n");
 					fclose(vp_file);
 				}
+
+			} else if (conf->session_cache_server) {
+				cbtls_cache_save_vps(ssn->ssl, ssn->ssl_session, vps);
+
 			} else {
 				RDEBUG("Failed to find 'persist_dir' in TLS configuration.  Session will not be cached on disk.");
 			}
@@ -3901,15 +5102,27 @@ int tls_success(tls_session_t *ssn, REQUEST *request)
 	 *	Else the session WAS allowed.  Copy the cached reply.
 	 */
 	} else {
-		char buffer[2 * MAX_SESSION_SIZE + 1];
-
-		tls_session_id(ssn->ssl_session, buffer, MAX_SESSION_SIZE);
+		RDEBUG("(TLS) cache - Refreshing entry for session resumption");
 
 		/*
 		 *	The "restore VPs from OpenSSL cache" code is
 		 *	now in eaptls_process()
 		 */
 		if (conf->session_cache_path) {
+			char buffer[2 * MAX_SESSION_SIZE + 1];
+
+#if OPENSSL_VERSION_NUMBER >= 0x10001000L
+#ifdef TLS1_3_VERSION
+			/*
+			 *	OpenSSL frees the underlying session out from
+			 *	under us in TLS 1.3.
+			 */
+			if (SSL_version(ssn->ssl) == TLS1_3_VERSION) ssn->ssl_session = SSL_get_session(ssn->ssl);
+#endif
+#endif
+
+			tls_session_id(ssn->ssl_session, buffer, MAX_SESSION_SIZE);
+
 			/* "touch" the cached session/vp file */
 			char filename[3 * MAX_SESSION_SIZE + 1];
 
@@ -3921,6 +5134,10 @@ int tls_success(tls_session_t *ssn, REQUEST *request)
 			utime(filename, NULL);
 		}
 
+		if (conf->session_cache_server) {
+			cbtls_cache_refresh(ssn->ssl, ssn->ssl_session);
+		}
+
 		/*
 		 *	Mark the request as resumed.
 		 */
@@ -3953,49 +5170,69 @@ fr_tls_status_t tls_application_data(tls_session_t *ssn, REQUEST *request)
 		err = BIO_write(ssn->into_ssl, ssn->dirty_in.data,
 				ssn->dirty_in.used);
 		if (err != (int) ssn->dirty_in.used) {
+			REDEBUG("(TLS) Failed writing %zd bytes to SSL BIO: %d", ssn->dirty_in.used, err);
 			record_init(&ssn->dirty_in);
-			RDEBUG("Failed writing %zd bytes to SSL BIO: %d", ssn->dirty_in.used, err);
 			return FR_TLS_FAIL;
 		}
+
+		record_init(&ssn->dirty_in);
 	}
 
 	/*
-	 *      Clear the dirty buffer now that we are done with it
-	 *      and init the clean_out buffer to store decrypted data
+	 *	tls_handshake_recv() may read application data.  So
+	 *	don't touch clean_out.  But only if the BIO_write()
+	 *	above didn't do anything.
 	 */
-	record_init(&ssn->dirty_in);
-	record_init(&ssn->clean_out);
+	else if (ssn->clean_out.used > 0) {
+		RDEBUG("(TLS) We already have %zd bytes of application data, processing it.",
+		       (ssn->clean_out.used));
+		goto add_certs;
+	}
 
 	/*
 	 *      Read (and decrypt) the tunneled data from the
 	 *      SSL session, and put it into the decrypted
 	 *      data buffer.
 	 */
-	err = SSL_read(ssn->ssl, ssn->clean_out.data, sizeof(ssn->clean_out.data));
+	err = SSL_read(ssn->ssl, ssn->clean_out.data + ssn->clean_out.used,
+		       sizeof(ssn->clean_out.data) - ssn->clean_out.used);
 	if (err <= 0) {
 		int code;
 
-		RDEBUG("SSL_read Error");
+		RDEBUG3("(TLS) SSL_read Error");
 
 		code = SSL_get_error(ssn->ssl, err);
 		switch (code) {
 		case SSL_ERROR_WANT_READ:
-			RDEBUG("Error in fragmentation logic: SSL_WANT_READ");
+			if (ssn->clean_out.used > 0) { /* just process what application data we have */
+				err = 0;
+				break;
+			}
+
+			RDEBUG("(TLS) OpenSSL says that it needs to read more data.");
 			return FR_TLS_MORE_FRAGMENTS;
 
 		case SSL_ERROR_WANT_WRITE:
-			RDEBUG("Error in fragmentation logic: SSL_WANT_WRITE");
+			if (ssn->clean_out.used > 0) { /* just process what application data we have */
+				err = 0;
+				break;
+			}
+
+			REDEBUG("(TLS) Error in fragmentation logic: SSL_WANT_WRITE");
 			return FR_TLS_FAIL;
 
 		case SSL_ERROR_NONE:
-			RDEBUG2("No application data received.  Assuming handshake is continuing...");
+			RDEBUG2("(TLS) No application data received.  Assuming handshake is continuing...");
 			err = 0;
 			break;
 
+		case SSL_ERROR_ZERO_RETURN:
+			RDEBUG2("(TLS) Other end closed the TLS tunnel.");
+			return FR_TLS_FAIL;
+
 		default:
-			REDEBUG("Error in fragmentation logic");
-			tls_error_io_log(request, ssn, err,
-					 "Failed in " STRINGIFY(__FUNCTION__) " (SSL_read)");
+			REDEBUG("(TLS) Error in fragmentation logic - code %d", code);
+			tls_error_io_log(request, ssn, err, "Failed reading application data from OpenSSL");
 			return FR_TLS_FAIL;
 		}
 	}
@@ -4003,8 +5240,9 @@ fr_tls_status_t tls_application_data(tls_session_t *ssn, REQUEST *request)
 	/*
 	 *	Passed all checks, successfully decrypted data
 	 */
-	ssn->clean_out.used = err;
+	ssn->clean_out.used += err;
 
+add_certs:
 	/*
 	 *	Add the certificates to intermediate packets, so that
 	 *	the inner tunnel policies can use them.
@@ -4026,27 +5264,27 @@ fr_tls_status_t tls_application_data(tls_session_t *ssn, REQUEST *request)
 fr_tls_status_t tls_ack_handler(tls_session_t *ssn, REQUEST *request)
 {
 	if (ssn == NULL){
-		REDEBUG("Unexpected ACK received:  No ongoing SSL session");
+		REDEBUG("(TLS) Unexpected ACK received:  No ongoing SSL session");
 		return FR_TLS_INVALID;
 	}
 	if (!ssn->info.initialized) {
-		RDEBUG("No SSL info available.  Waiting for more SSL data");
+		RDEBUG("(TLS) No SSL info available.  Waiting for more SSL data");
 		return FR_TLS_REQUEST;
 	}
 
 	if ((ssn->info.content_type == handshake) && (ssn->info.origin == 0)) {
-		REDEBUG("Unexpected ACK received:  We sent no previous messages");
+		REDEBUG("(TLS) Unexpected ACK received:  We sent no previous messages");
 		return FR_TLS_INVALID;
 	}
 
 	switch (ssn->info.content_type) {
 	case alert:
-		RDEBUG2("Peer ACKed our alert");
+		RDEBUG2("(TLS) Peer ACKed our alert");
 		return FR_TLS_FAIL;
 
 	case handshake:
 		if ((ssn->is_init_finished) && (ssn->dirty_out.used == 0)) {
-			RDEBUG2("Peer ACKed our handshake fragment.  handshake is finished");
+			RDEBUG2("(TLS) Peer ACKed our handshake fragment.  handshake is finished");
 
 			/*
 			 *	From now on all the content is
@@ -4057,12 +5295,12 @@ fr_tls_status_t tls_ack_handler(tls_session_t *ssn, REQUEST *request)
 			return FR_TLS_SUCCESS;
 		} /* else more data to send */
 
-		RDEBUG2("Peer ACKed our handshake fragment");
+		RDEBUG2("(TLS) Peer ACKed our handshake fragment");
 		/* Fragmentation handler, send next fragment */
 		return FR_TLS_REQUEST;
 
 	case application_data:
-		RDEBUG2("Peer ACKed our application data fragment");
+		RDEBUG2("(TLS) Peer ACKed our application data fragment");
 		return FR_TLS_REQUEST;
 
 		/*
@@ -4070,7 +5308,7 @@ fr_tls_status_t tls_ack_handler(tls_session_t *ssn, REQUEST *request)
 		 *	to the default section below.
 		 */
 	default:
-		REDEBUG("Invalid ACK received: %d", ssn->info.content_type);
+		REDEBUG("(TLS) Invalid ACK received: %d", ssn->info.content_type);
 		return FR_TLS_INVALID;
 	}
 }
diff --git a/src/main/tls_listen.c b/src/main/tls_listen.c
index 0eed87b64f..c3c40d17ea 100644
--- a/src/main/tls_listen.c
+++ b/src/main/tls_listen.c
@@ -81,7 +81,7 @@ static void tls_socket_close(rad_listen_t *listener)
 	/*
 	 *	Tell the event handler that an FD has disappeared.
 	 */
-	DEBUG("Client has closed connection");
+	DEBUG("(TLS) Client has closed connection");
 	radius_update_listener(listener);
 
 	/*
@@ -102,11 +102,11 @@ static int CC_HINT(nonnull) tls_socket_write(rad_listen_t *listener, REQUEST *re
 	p = sock->ssn->dirty_out.data;
 
 	while (p < (sock->ssn->dirty_out.data + sock->ssn->dirty_out.used)) {
-		RDEBUG3("Writing to socket %d", request->packet->sockfd);
-		rcode = write(request->packet->sockfd, p,
+		RDEBUG3("(TLS) Writing to socket %d", listener->fd);
+		rcode = write(listener->fd, p,
 			      (sock->ssn->dirty_out.data + sock->ssn->dirty_out.used) - p);
 		if (rcode <= 0) {
-			RDEBUG("Error writing to TLS socket: %s", fr_syserror(errno));
+			RDEBUG("(TLS) Error writing to socket: %s", fr_syserror(errno));
 
 			tls_socket_close(listener);
 			return 0;
@@ -118,8 +118,6 @@ static int CC_HINT(nonnull) tls_socket_write(rad_listen_t *listener, REQUEST *re
 
 	return 1;
 }
-
-
 static int tls_socket_recv(rad_listen_t *listener)
 {
 	bool doing_init = false;
@@ -165,7 +163,7 @@ static int tls_socket_recv(rad_listen_t *listener)
 		rad_assert(sock->ssn == NULL);
 
 		sock->ssn = tls_new_session(sock, listener->tls, sock->request,
-					    listener->tls->require_client_cert);
+					    listener->tls->require_client_cert, true);
 		if (!sock->ssn) {
 			TALLOC_FREE(sock->request);
 			sock->packet = NULL;
@@ -175,6 +173,7 @@ static int tls_socket_recv(rad_listen_t *listener)
 		SSL_set_ex_data(sock->ssn->ssl, FR_TLS_EX_INDEX_REQUEST, (void *)request);
 		SSL_set_ex_data(sock->ssn->ssl, fr_tls_ex_index_certs, (void *) &sock->certs);
 		SSL_set_ex_data(sock->ssn->ssl, FR_TLS_EX_INDEX_TALLOC, sock);
+		sock->ssn->quick_session_tickets = true; /* we don't have inner-tunnel authentication */
 
 		doing_init = true;
 	}
@@ -186,7 +185,18 @@ static int tls_socket_recv(rad_listen_t *listener)
 
 	request = sock->request;
 
-	RDEBUG3("Reading from socket %d", request->packet->sockfd);
+	if (sock->state == LISTEN_TLS_SETUP) {
+		RDEBUG3("(TLS) Setting connection state to RUNNING");
+		sock->state = LISTEN_TLS_RUNNING;
+
+		if (sock->ssn->clean_out.used < 20) {
+			goto get_application_data;
+		}
+
+		goto read_application_data;
+	}
+
+	RDEBUG3("(TLS) Reading from socket %d", request->packet->sockfd);
 	PTHREAD_MUTEX_LOCK(&sock->mutex);
 
 	/*
@@ -195,9 +205,9 @@ static int tls_socket_recv(rad_listen_t *listener)
 	 *	the socket.
 	 */
 	if (SSL_pending(sock->ssn->ssl)) {
-		RDEBUG3("Reading pending buffered data");
+		RDEBUG3("(TLS) Reading pending buffered data");
 		sock->ssn->dirty_in.used = 0;
-		goto get_application_data;
+		goto check_for_setup;
 	}
 
 	rcode = read(request->packet->sockfd,
@@ -205,14 +215,14 @@ static int tls_socket_recv(rad_listen_t *listener)
 		     sizeof(sock->ssn->dirty_in.data));
 	if ((rcode < 0) && (errno == ECONNRESET)) {
 	do_close:
-		DEBUG("Closing TLS socket from client port %u", sock->other_port);
+		DEBUG("(TLS) Closing socket from client port %u", sock->other_port);
 		tls_socket_close(listener);
 		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
 		return 0;
 	}
 
 	if (rcode < 0) {
-		RDEBUG("Error reading TLS socket: %s", fr_syserror(errno));
+		RDEBUG("(TLS) Error reading socket: %s", fr_syserror(errno));
 		goto do_close;
 	}
 
@@ -222,23 +232,23 @@ static int tls_socket_recv(rad_listen_t *listener)
 	if (rcode == 0) goto do_close;
 
 	sock->ssn->dirty_in.used = rcode;
-
 	dump_hex("READ FROM SSL", sock->ssn->dirty_in.data, sock->ssn->dirty_in.used);
 
 	/*
 	 *	Catch attempts to use non-SSL.
 	 */
 	if (doing_init && (sock->ssn->dirty_in.data[0] != handshake)) {
-		RDEBUG("Non-TLS data sent to TLS socket: closing");
+		RDEBUG("(TLS) Non-TLS data sent to TLS socket: closing");
 		goto do_close;
 	}
 
 	/*
 	 *	If we need to do more initialization, do that here.
 	 */
+check_for_setup:
 	if (!sock->ssn->is_init_finished) {
 		if (!tls_handshake_recv(request, sock->ssn)) {
-			RDEBUG("FAILED in TLS handshake receive");
+			RDEBUG("(TLS) Failed in TLS handshake receive");
 			goto do_close;
 		}
 
@@ -252,10 +262,55 @@ static int tls_socket_recv(rad_listen_t *listener)
 		}
 
 		/*
-		 *	FIXME: Run the request through a virtual
-		 *	server in order to see if we like the
-		 *	certificate presented by the client.
+		 *      If SSL handshake still isn't finished, then there
+		 *      is more data to read.  Release the mutex and
+		 *      return so this function will be called again
+		 */
+		if (!SSL_is_init_finished(sock->ssn->ssl)) {
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return 0;
+		}
+	}
+
+	/*
+	 *	Run the request through a virtual server in
+	 *	order to see if we like the certificate
+	 *	presented by the client.
+	 */
+	if (sock->state == LISTEN_TLS_INIT) {
+		if (!SSL_is_init_finished(sock->ssn->ssl)) {
+			RDEBUG("(TLS) OpenSSL says that the TLS session is still negotiating, but there's no more data to send!");
+			goto do_close;
+		}
+
+		sock->ssn->is_init_finished = true;
+		if (!listener->check_client_connections) {
+			sock->state = LISTEN_TLS_RUNNING;
+			goto get_application_data;
+		}
+
+		request->packet->vps = fr_pair_list_copy(request->packet, sock->certs);
+
+		/*
+		 *	Fake out a Status-Server packet, which
+		 *	does NOT have a Message-Authenticator,
+		 *	or any other contents.
 		 */
+		request->packet->code = PW_CODE_STATUS_SERVER;
+		request->packet->data = talloc_zero_array(request->packet, uint8_t, 20);
+		request->packet->data[0] = PW_CODE_STATUS_SERVER;
+		request->packet->data[3] = 20;
+		sock->state = LISTEN_TLS_CHECKING;
+		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+		/*
+		 *	Don't read from the socket until the request
+		 *	returns.
+		 */
+		listener->status = RAD_LISTEN_STATUS_PAUSE;
+		radius_update_listener(listener);
+
+		return 1;
 	}
 
 	/*
@@ -263,7 +318,7 @@ static int tls_socket_recv(rad_listen_t *listener)
 	 */
 get_application_data:
 	status = tls_application_data(sock->ssn, request);
-	RDEBUG("Application data status %d", status);
+	RDEBUG3("(TLS) Application data status %d", status);
 
 	if (status == FR_TLS_MORE_FRAGMENTS) {
 		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
@@ -275,6 +330,16 @@ get_application_data:
 		return 0;
 	}
 
+	/*
+	 *	Hold application data if we're not yet in the RUNNING
+	 *	state.
+	 */
+	if (sock->state != LISTEN_TLS_RUNNING) {
+		RDEBUG3("(TLS) Holding application data until setup is complete");
+		return 0;
+	}
+
+read_application_data:
 	/*
 	 *	We now have a bunch of application data.
 	 */
@@ -286,7 +351,7 @@ get_application_data:
 	 */
 	if ((sock->ssn->clean_out.used < 20) ||
 	    (((sock->ssn->clean_out.data[2] << 8) | sock->ssn->clean_out.data[3]) != (int) sock->ssn->clean_out.used)) {
-		RDEBUG("Received bad packet: Length %zd contents %d",
+		RDEBUG("(TLS) Received bad packet: Length %zd contents %d",
 		       sock->ssn->clean_out.used,
 		       (sock->ssn->clean_out.data[2] << 8) | sock->ssn->clean_out.data[3]);
 		goto do_close;
@@ -301,7 +366,7 @@ get_application_data:
 
 	if (!rad_packet_ok(packet, 0, NULL)) {
 		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
-		DEBUG("Closing TLS socket from client");
+		DEBUG("(TLS) Closing TLS socket from client");
 		PTHREAD_MUTEX_LOCK(&sock->mutex);
 		tls_socket_close(listener);
 		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
@@ -315,7 +380,7 @@ get_application_data:
 		char host_ipaddr[128];
 
 		if (is_radius_code(packet->code)) {
-			RDEBUG("tls_recv: %s packet from host %s port %d, id=%d, length=%d",
+			RDEBUG("(TLS): %s packet from host %s port %d, id=%d, length=%d",
 			       fr_packet_codes[packet->code],
 			       inet_ntop(packet->src_ipaddr.af,
 					 &packet->src_ipaddr.ipaddr,
@@ -323,7 +388,7 @@ get_application_data:
 			       packet->src_port,
 			       packet->id, (int) packet->data_len);
 		} else {
-			RDEBUG("tls_recv: Packet from host %s port %d code=%d, id=%d, length=%d",
+			RDEBUG("(TLS): Packet from host %s port %d code=%d, id=%d, length=%d",
 			       inet_ntop(packet->src_ipaddr.af,
 					 &packet->src_ipaddr.ipaddr,
 					 host_ipaddr, sizeof(host_ipaddr)),
@@ -359,6 +424,7 @@ redo:
 	rad_assert(client != NULL);
 
 	packet = talloc_steal(NULL, sock->packet);
+	sock->request->packet = NULL;
 	sock->packet = NULL;
 
 	/*
@@ -391,8 +457,26 @@ redo:
 		break;
 #endif
 
+#ifdef WITH_COA
+	case PW_CODE_COA_REQUEST:
+		if (listener->type != RAD_LISTEN_COA) goto bad_packet;
+		FR_STATS_INC(coa, total_requests);
+		fun = rad_coa_recv;
+		break;
+
+	case PW_CODE_DISCONNECT_REQUEST:
+		if (listener->type != RAD_LISTEN_COA) goto bad_packet;
+		FR_STATS_INC(dsc, total_requests);
+		fun = rad_coa_recv;
+		break;
+#endif
+
 	case PW_CODE_STATUS_SERVER:
-		if (!main_config.status_server) {
+		if (!main_config.status_server
+#ifdef WITH_TLS
+		    && !listener->check_client_connections
+#endif
+			) {
 			FR_STATS_INC(auth, total_unknown_types);
 			WARN("Ignoring Status-Server request due to security configuration");
 			rad_free(&packet);
@@ -405,7 +489,7 @@ redo:
 	bad_packet:
 		FR_STATS_INC(auth, total_unknown_types);
 
-		DEBUG("Invalid packet code %d sent from client %s port %d : IGNORED",
+		DEBUG("(TLS) Invalid packet code %d sent from client %s port %d : IGNORED",
 		      packet->code, client->shortname, packet->src_port);
 		rad_free(&packet);
 		return 0;
@@ -432,7 +516,7 @@ redo:
 		int peek = SSL_peek(sock->ssn->ssl, buf, 1);
 
 		if (peek > 0) {
-			DEBUG("more TLS records after dual_tls_recv");
+			DEBUG("(TLS) more TLS records after dual_tls_recv");
 			goto redo;
 		}
 	}
@@ -455,6 +539,34 @@ int dual_tls_send(rad_listen_t *listener, REQUEST *request)
 
 	if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0;
 
+	/*
+	 *	See if the policies allowed this connection.
+	 */
+	if (sock->state == LISTEN_TLS_CHECKING) {
+		if (request->reply->code != PW_CODE_ACCESS_ACCEPT) {
+			listener->status = RAD_LISTEN_STATUS_EOL;
+			listener->tls = NULL; /* parent owns this! */
+
+			/*
+			 *	Tell the event handler that an FD has disappeared.
+			 */
+			radius_update_listener(listener);
+			return 0;
+		}
+
+		/*
+		 *	Resume reading from the listener.
+		 */
+		listener->status = RAD_LISTEN_STATUS_RESUME;
+		radius_update_listener(listener);
+
+		rad_assert(sock->request->packet != request->packet);
+
+		sock->state = LISTEN_TLS_SETUP;
+		(void) dual_tls_recv(listener);
+		return 0;
+	}
+
 	/*
 	 *	Accounting reject's are silently dropped.
 	 *
@@ -727,6 +839,15 @@ int proxy_tls_recv(rad_listen_t *listener)
 		break;
 #endif
 
+#ifdef WITH_COA
+	case PW_CODE_COA_ACK:
+	case PW_CODE_COA_NAK:
+	case PW_CODE_DISCONNECT_ACK:
+	case PW_CODE_DISCONNECT_NAK:
+		break;
+
+#endif
+
 	default:
 		/*
 		 *	FIXME: Update MIB for packet types?
@@ -765,8 +886,8 @@ int proxy_tls_send(rad_listen_t *listener, REQUEST *request)
 	 *	if there's no packet, encode it here.
 	 */
 	if (!request->proxy->data) {
-		request->proxy_listener->encode(request->proxy_listener,
-						request);
+		request->proxy_listener->proxy_encode(request->proxy_listener,
+						      request);
 	}
 
 	if (!sock->ssn->connected) {
diff --git a/src/modules/proto_dhcp/rlm_dhcp.c b/src/modules/proto_dhcp/rlm_dhcp.c
index 9fed166e62..1cd73ff246 100644
--- a/src/modules/proto_dhcp/rlm_dhcp.c
+++ b/src/modules/proto_dhcp/rlm_dhcp.c
@@ -97,7 +97,7 @@ static ssize_t dhcp_options_xlat(UNUSED void *instance, REQUEST *request,
 			decoded++;
 		}
 
-		fr_pair_list_move(request->packet, &(request->packet->vps), &head);
+		fr_pair_list_move(request->packet, &(request->packet->vps), &head, T_OP_ADD);
 
 		/* Free any unmoved pairs */
 		fr_pair_list_free(&head);
diff --git a/src/modules/rlm_eap/libeap/eap_tls.c b/src/modules/rlm_eap/libeap/eap_tls.c
index 83e7252fa8..2f37663df1 100644
--- a/src/modules/rlm_eap/libeap/eap_tls.c
+++ b/src/modules/rlm_eap/libeap/eap_tls.c
@@ -1,3 +1,4 @@
+
 /*
  * eap_tls.c
  *
@@ -61,7 +62,7 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
  *
  *	Fragment length is Framed-MTU - 4.
  */
-tls_session_t *eaptls_session(eap_handler_t *handler, fr_tls_server_conf_t *tls_conf, bool client_cert)
+tls_session_t *eaptls_session(eap_handler_t *handler, fr_tls_server_conf_t *tls_conf, bool client_cert, bool allow_tls13)
 {
 	tls_session_t	*ssn;
 	REQUEST		*request = handler->request;
@@ -75,7 +76,7 @@ tls_session_t *eaptls_session(eap_handler_t *handler, fr_tls_server_conf_t *tls_
 	 *	in Opaque.  So that we can use these data structures
 	 *	when we get the response
 	 */
-	ssn = tls_new_session(handler, tls_conf, request, client_cert);
+	ssn = tls_new_session(handler, tls_conf, request, client_cert, allow_tls13);
 	if (!ssn) {
 		return NULL;
 	}
@@ -139,7 +140,7 @@ int eaptls_start(EAP_DS *eap_ds, int peap_flag)
  */
 int eaptls_success(eap_handler_t *handler, int peap_flag)
 {
-	EAPTLS_PACKET	reply;
+	EAPTLS_PACKET reply;
 	REQUEST *request = handler->request;
 	tls_session_t *tls_session = handler->opaque;
 
@@ -160,15 +161,42 @@ int eaptls_success(eap_handler_t *handler, int peap_flag)
 	/*
 	 *	Automatically generate MPPE keying material.
 	 */
-	if (tls_session->prf_label) {
-		eaptls_gen_mppe_keys(handler->request,
-				     tls_session->ssl, tls_session->prf_label);
+	if (tls_session->label) {
+		uint8_t const *context = NULL;
+		size_t context_size = 0;
+#ifdef TLS1_3_VERSION
+		uint8_t const context_tls13[] = { handler->type };
+#endif
+
+		switch (SSL_version(tls_session->ssl)) {
+#ifdef TLS1_3_VERSION
+		case TLS1_3_VERSION:
+			context = context_tls13;
+			context_size = sizeof(context_tls13);
+			tls_session->label = "EXPORTER_EAP_TLS_Key_Material";
+			break;
+#endif
+		case TLS1_2_VERSION:
+		case TLS1_1_VERSION:
+		case TLS1_VERSION:
+			break;
+		case SSL2_VERSION:
+		case SSL3_VERSION:
+		default:
+			/* Should never happen */
+			rad_assert(0);
+			return 0;
+			break;
+		}
+		eaptls_gen_mppe_keys(request,
+				     tls_session->ssl, tls_session->label,
+				     context, context_size);
 	} else if (handler->type != PW_EAP_FAST) {
-		RWDEBUG("Not adding MPPE keys because there is no PRF label");
+		RWDEBUG("(TLS) EAP Not adding MPPE keys because there is no PRF label");
 	}
 
-	eaptls_gen_eap_key(handler->request->reply, tls_session->ssl,
-			   handler->type);
+	eaptls_gen_eap_key(handler);
+
 	return 1;
 }
 
@@ -291,7 +319,7 @@ static int eaptls_send_ack(eap_handler_t *handler, int peap_flag)
 	EAPTLS_PACKET 	reply;
 	REQUEST		*request = handler->request;
 
-	RDEBUG2("ACKing Peer's TLS record fragment");
+	RDEBUG2("(TLS) EAP ACKing fragment, the peer should send more data.");
 	reply.code = FR_TLS_ACK;
 	reply.length = TLS_HEADER_LEN + 1/*flags*/;
 	reply.flags = peap_flag;
@@ -343,7 +371,7 @@ static fr_tls_status_t eaptls_verify(eap_handler_t *handler)
 		/*
 		 *	First output the flags (for debugging)
 		 */
-		RDEBUG3("Peer sent flags %c%c%c",
+		RDEBUG3("(TLS) EAP Peer sent flags %c%c%c",
 			TLS_START(eaptls_packet->flags) ? 'S' : '-',
 			TLS_MORE_FRAGMENTS(eaptls_packet->flags) ? 'M' : '-',
 			TLS_LENGTH_INCLUDED(eaptls_packet->flags) ? 'L' : '-');
@@ -364,7 +392,7 @@ static fr_tls_status_t eaptls_verify(eap_handler_t *handler)
 		if (prev_eap_ds && (prev_eap_ds->request->id == eap_ds->response->id)) {
 			return tls_ack_handler(handler->opaque, request);
 		} else {
-			REDEBUG("Received Invalid TLS ACK");
+			REDEBUG("(TLS) EAP Received Unexpected ACK - rejection the connection");
 			return FR_TLS_INVALID;
 		}
 	}
@@ -373,7 +401,7 @@ static fr_tls_status_t eaptls_verify(eap_handler_t *handler)
 	 *	We send TLS_START, but do not receive it.
 	 */
 	if (TLS_START(eaptls_packet->flags)) {
-		REDEBUG("Peer sent EAP-TLS Start message (only the server is allowed to do this)");
+		REDEBUG("(TLS) EAP Peer sent EAP-TLS Start message (only the server is allowed to do this)");
 		return FR_TLS_INVALID;
 	}
 
@@ -400,11 +428,11 @@ static fr_tls_status_t eaptls_verify(eap_handler_t *handler)
 		size_t total_len = eaptls_packet->data[2] * 256 | eaptls_packet->data[3];
 
 		if (frag_len > total_len) {
-			RWDEBUG("TLS fragment length (%zu bytes) greater than TLS record length (%zu bytes)", frag_len,
+			RWDEBUG("(TLS) EAP Fragment length (%zu bytes) is greater than TLS record length (%zu bytes)", frag_len,
 				total_len);
 		}
 
-		RDEBUG2("Peer indicated complete TLS record size will be %zu bytes", total_len);
+		RDEBUG2("(TLS) EAP Peer says that the final record size will be %zu bytes", total_len);
 		if (TLS_MORE_FRAGMENTS(eaptls_packet->flags)) {
 			/*
 			 *	The supplicant is free to send fragments of wildly varying
@@ -415,7 +443,7 @@ static fr_tls_status_t eaptls_verify(eap_handler_t *handler)
 			 *	as they won't contain the length field.
 			 */
 			if (frag_len + 4) {	/* check for wrap, else clang scan gets excited */
-				RDEBUG2("Expecting %i TLS record fragments",
+				RDEBUG2("(TLS) EAP Expecting %i fragments",
 					(int)((((total_len - frag_len) + ((frag_len + 4) - 1)) / (frag_len + 4)) + 1));
 			}
 
@@ -427,24 +455,24 @@ static fr_tls_status_t eaptls_verify(eap_handler_t *handler)
 			 */
 			if (!prev_eap_ds || (!prev_eap_ds->response) || (!eaptls_prev) ||
 			    !TLS_MORE_FRAGMENTS(eaptls_prev->flags)) {
-			    	RDEBUG2("Got first TLS record fragment (%zu bytes).  Peer indicated more fragments "
-			    		"to follow", frag_len);
+			    	RDEBUG2("(TLS) EAP Got first TLS fragment (%zu bytes).  Peer says more fragments "
+			    		"will follow", frag_len);
 			    	tls_session->tls_record_in_total_len = total_len;
 			    	tls_session->tls_record_in_recvd_len = frag_len;
 
 				return FR_TLS_FIRST_FRAGMENT;
 			}
 
-			RDEBUG2("Got additional TLS record fragment with length (%zu bytes).  "
-				"Peer indicated more fragments to follow", frag_len);
+			RDEBUG2("(TLS) EAP Got additional fragment with length (%zu bytes).  "
+				"Peer says more fragments will follow", frag_len);
 
 			/*
 			 *	Check we've not exceeded the originally indicated TLS record size.
 			 */
 			tls_session->tls_record_in_recvd_len += frag_len;
 			if (tls_session->tls_record_in_recvd_len > tls_session->tls_record_in_total_len) {
-				RWDEBUG("Total received TLS record fragments (%zu bytes), exceeds "
-					"total TLS record length (%zu bytes)", frag_len, total_len);
+				RWDEBUG("(TLS) EAP Total received fragments (%zu bytes), exceeds "
+					"total data length (%zu bytes)", frag_len, total_len);
 			}
 
 			return FR_TLS_MORE_FRAGMENTS_WITH_LENGTH;
@@ -455,13 +483,13 @@ static fr_tls_status_t eaptls_verify(eap_handler_t *handler)
 		 *	value of the four octet TLS length field.
 		 */
 		if (total_len != frag_len) {
-			RWDEBUG("Peer indicated no more fragments, but TLS record length (%zu bytes) "
-				"does not match EAP-TLS data length (%zu bytes)", total_len, frag_len);
+			RWDEBUG("(TLS) EAP Peer says no more fragments, but expected data length (%zu bytes) "
+				"does not match expected data length (%zu bytes)", total_len, frag_len);
 		}
 
 		tls_session->tls_record_in_total_len = total_len;
 		tls_session->tls_record_in_recvd_len = frag_len;
-		RDEBUG2("Got complete TLS record (%zu bytes)", frag_len);
+		RDEBUG2("(TLS) EAP Got all data (%zu bytes)", frag_len);
 		return FR_TLS_LENGTH_INCLUDED;
 	}
 
@@ -470,22 +498,22 @@ static fr_tls_status_t eaptls_verify(eap_handler_t *handler)
 	 *	this must be the final record fragment
 	 */
 	if ((eaptls_prev && TLS_MORE_FRAGMENTS(eaptls_prev->flags)) && !TLS_MORE_FRAGMENTS(eaptls_packet->flags)) {
-		RDEBUG2("Got final TLS record fragment (%zu bytes)", frag_len);
+		RDEBUG2("(TLS) EAP Got final fragment (%zu bytes)", frag_len);
 		tls_session->tls_record_in_recvd_len += frag_len;
 		if (tls_session->tls_record_in_recvd_len != tls_session->tls_record_in_total_len) {
-			RWDEBUG("Total received TLS record fragments (%zu bytes), does not equal indicated "
-				"TLS record length (%zu bytes)",
+			RWDEBUG("(TLS) EAP Total received record fragments (%zu bytes), does not equal expected "
+				"expected data length (%zu bytes)",
 				tls_session->tls_record_in_recvd_len, tls_session->tls_record_in_total_len);
 		}
 	}
 
 	if (TLS_MORE_FRAGMENTS(eaptls_packet->flags)) {
-		RDEBUG2("Got additional TLS record fragment (%zu bytes).  Peer indicated more fragments to follow",
+		RDEBUG2("(TLS) EAP Got additional fragment (%zu bytes).  Peer says more fragments will follow",
 			frag_len);
 		tls_session->tls_record_in_recvd_len += frag_len;
 		if (tls_session->tls_record_in_recvd_len > tls_session->tls_record_in_total_len) {
-			RWDEBUG("Total received TLS record fragments (%zu bytes), exceeds "
-				"indicated TLS record length (%zu bytes)",
+			RWDEBUG("(TLS) EAP Total received fragments (%zu bytes), exceeds "
+				"expected length (%zu bytes)",
 				tls_session->tls_record_in_recvd_len, tls_session->tls_record_in_total_len);
 		}
 		return FR_TLS_MORE_FRAGMENTS;
@@ -576,7 +604,7 @@ static EAPTLS_PACKET *eaptls_extract(REQUEST *request, EAP_DS *eap_ds, fr_tls_st
 	 */
 	if (TLS_LENGTH_INCLUDED(tlspacket->flags) &&
 	    (tlspacket->length < 5)) { /* flags + TLS message length */
-		REDEBUG("Invalid EAP-TLS packet received:  Length bit is set, "
+		REDEBUG("(TLS) EAP Invalid packet received: Length bit is set,"
 			"but packet too short to contain length field");
 		talloc_free(tlspacket);
 		return NULL;
@@ -593,8 +621,8 @@ static EAPTLS_PACKET *eaptls_extract(REQUEST *request, EAP_DS *eap_ds, fr_tls_st
 		memcpy(&data_len, &eap_ds->response->type.data[1], 4);
 		data_len = ntohl(data_len);
 		if (data_len > MAX_RECORD_SIZE) {
-			REDEBUG("Reassembled TLS record will be %u bytes, "
-				"greater than our maximum record size (" STRINGIFY(MAX_RECORD_SIZE) " bytes)",
+			REDEBUG("(TLS) EAP Reassembled data will be %u bytes, "
+				"greater than the size that we can handle (" STRINGIFY(MAX_RECORD_SIZE) " bytes)",
 				data_len);
 			talloc_free(tlspacket);
 			return NULL;
@@ -615,7 +643,7 @@ static EAPTLS_PACKET *eaptls_extract(REQUEST *request, EAP_DS *eap_ds, fr_tls_st
 	case FR_TLS_LENGTH_INCLUDED:
 	case FR_TLS_MORE_FRAGMENTS_WITH_LENGTH:
 		if (tlspacket->length < 5) { /* flags + TLS message length */
-			REDEBUG("Invalid EAP-TLS packet received: Expected length, got none");
+			REDEBUG("(TLS) EAP Invalid packet received: Expected length, got none");
 			talloc_free(tlspacket);
 			return NULL;
 		}
@@ -648,7 +676,7 @@ static EAPTLS_PACKET *eaptls_extract(REQUEST *request, EAP_DS *eap_ds, fr_tls_st
 		break;
 
 	default:
-		REDEBUG("Invalid EAP-TLS packet received");
+		REDEBUG("(TLS) EAP Invalid packet received");
 		talloc_free(tlspacket);
 		return NULL;
 	}
@@ -719,11 +747,37 @@ static fr_tls_status_t eaptls_operation(fr_tls_status_t status, eap_handler_t *h
 	 *	is required then send another request.
 	 */
 	if (!tls_handshake_recv(handler->request, tls_session)) {
-		REDEBUG("TLS receive handshake failed during operation");
+		REDEBUG("(TLS) EAP Receive handshake failed during operation");
 		tls_fail(tls_session);
 		return FR_TLS_FAIL;
 	}
 
+#ifdef TLS1_3_VERSION
+	/*
+	 *	https://tools.ietf.org/html/draft-ietf-emu-eap-tls13#section-2.5
+	 *
+	 *	We need to signal the other end that TLS negotiation
+	 *	is done.  We can't send a zero-length application data
+	 *	message, so we send application data which is one byte
+	 *	of zero.
+	 *
+	 *	Note this is only done for when there is no application
+	 *	data to be sent. So this is done always for EAP-TLS but
+	 *	notibly not for PEAP even on resumption.
+	 */
+	if ((SSL_version(tls_session->ssl) == TLS1_3_VERSION) &&
+	    (tls_session->client_cert_ok || tls_session->authentication_success || SSL_session_reused(tls_session->ssl))) {
+		if ((handler->type == PW_EAP_TLS) || SSL_session_reused(tls_session->ssl)) {
+			tls_session->authentication_success = true;
+
+			RDEBUG("(TLS) EAP Sending final Commitment Message.");
+			tls_session->record_plus(&tls_session->clean_in, "\0", 1);
+		}
+
+		tls_handshake_send(request, tls_session);
+	}
+#endif
+
 	/*
 	 *	FIXME: return success/fail.
 	 *
@@ -737,23 +791,28 @@ static fr_tls_status_t eaptls_operation(fr_tls_status_t status, eap_handler_t *h
 	/*
 	 *	If there is no data to send i.e
 	 *	dirty_out.used <=0 and if the SSL
-	 *	handshake is finished, then return a
-	 *	EPTLS_SUCCESS
+	 *	handshake is finished.
 	 */
+	if (tls_session->is_init_finished) return FR_TLS_SUCCESS;
 
-	if (tls_session->is_init_finished) {
+	/*
+	 *	If session is established, skip round-trip and
+	 *	try to process any inner tunnel data if present.
+	 *
+	 *	This occurs for EAP-TTLS/PAP with TLSv1.3.
+	 */
+	if (!tls_session->is_init_finished && SSL_is_init_finished(tls_session->ssl)) {
 		/*
-		 *	Init is finished.  The rest is
-		 *	application data.
+		 *	Don't set is_init_finished, as that causes the
+		 *	rest of the code to make too many assumptions.
 		 */
-		tls_session->info.content_type = application_data;
-		return FR_TLS_SUCCESS;
+		return FR_TLS_OK;
 	}
 
 	/*
 	 *	Who knows what happened...
 	 */
-	REDEBUG("TLS failed during operation");
+	REDEBUG("(TLS) Cannot continue, as the peer is misbehaving.");
 	return FR_TLS_FAIL;
 }
 
@@ -794,7 +853,7 @@ fr_tls_status_t eaptls_process(eap_handler_t *handler)
 
 	if (!request) return FR_TLS_FAIL;
 
-	RDEBUG2("Continuing EAP-TLS");
+	RDEBUG3("(TLS) EAP Continuing ...");
 
 	SSL_set_ex_data(tls_session->ssl, FR_TLS_EX_INDEX_REQUEST, request);
 
@@ -807,9 +866,9 @@ fr_tls_status_t eaptls_process(eap_handler_t *handler)
 	 */
 	status = eaptls_verify(handler);
 	if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) {
-		REDEBUG("[eaptls verify] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		REDEBUG("(TLS) EAP Verification failed with %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	} else {
-		RDEBUG2("[eaptls verify] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		RDEBUG3("(TLS) EAP Verification says %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	}
 
 	switch (status) {
@@ -840,7 +899,7 @@ fr_tls_status_t eaptls_process(eap_handler_t *handler)
 	 *	data" phase.
 	 */
 	case FR_TLS_OK:
-		RDEBUG2("Done initial handshake");
+		RDEBUG2("(TLS) EAP Done initial handshake");
 
 	/*
 	 *	Get the rest of the fragments.
@@ -856,6 +915,7 @@ fr_tls_status_t eaptls_process(eap_handler_t *handler)
 	 *	Extract the TLS packet from the buffer.
 	 */
 	if ((tlspacket = eaptls_extract(request, handler->eap_ds, status)) == NULL) {
+		REDEBUG("(TLS) EAP Failed extracting TLS packet from EAP-Message");
 		status = FR_TLS_FAIL;
 		goto done;
 	}
@@ -873,7 +933,7 @@ fr_tls_status_t eaptls_process(eap_handler_t *handler)
 	if (tlspacket->dlen !=
 	    (tls_session->record_plus)(&tls_session->dirty_in, tlspacket->data, tlspacket->dlen)) {
 		talloc_free(tlspacket);
-		REDEBUG("Exceeded maximum record size");
+		REDEBUG("(TLS) EAP Exceeded maximum record size");
 		status = FR_TLS_FAIL;
 		goto done;
 	}
@@ -902,7 +962,7 @@ fr_tls_status_t eaptls_process(eap_handler_t *handler)
 			 *	Send the ACK.
 			 */
 			eaptls_send_ack(handler, tls_session->peap_flag);
-			RDEBUG2("Init is done, but tunneled data is fragmented");
+			RDEBUG2("(TLS) EAP Init is done, but tunneled data is fragmented");
 			status = FR_TLS_HANDLED;
 			goto done;
 		}
@@ -928,13 +988,13 @@ fr_tls_status_t eaptls_process(eap_handler_t *handler)
 
 		vps = SSL_SESSION_get_ex_data(tls_session->ssl_session, fr_tls_ex_index_vps);
 		if (!vps) {
-			RWDEBUG("No information in cached session %s", buffer);
+			RWDEBUG("(TLS) EAP No information in cached session %s", buffer);
 		} else {
 			vp_cursor_t cursor;
 			VALUE_PAIR *vp;
 			fr_tls_server_conf_t *conf;
 
-			RDEBUG("Adding cached attributes from session %s", buffer);
+			RDEBUG("(TLS) EAP Adding cached attributes from session %s", buffer);
 
 			conf = (fr_tls_server_conf_t *)SSL_get_ex_data(tls_session->ssl, FR_TLS_EX_INDEX_CONF);
 			rad_assert(conf != NULL);
@@ -970,7 +1030,19 @@ fr_tls_status_t eaptls_process(eap_handler_t *handler)
 						rdebug_pair(L_DBG_LVL_2, request, vp, "&request:");
 						fr_pair_add(&request->packet->vps, fr_pair_copy(request->packet, vp));
 					}
+
+				} else if ((vp->da->vendor == 0) &&
+					   (vp->da->attr == PW_EAP_TYPE)) {
+					/*
+					 *	EAP-Type gets added to
+					 *	the control list, so
+					 *	that we can sanity check it.
+					 */
+					rdebug_pair(L_DBG_LVL_2, request, vp, "&control:");
+					fr_pair_add(&request->config, fr_pair_copy(request, vp));
+
 				} else {
+
 					rdebug_pair(L_DBG_LVL_2, request, vp, "&reply:");
 					fr_pair_add(&request->reply->vps, fr_pair_copy(request->reply, vp));
 				}
diff --git a/src/modules/rlm_eap/libeap/eap_tls.h b/src/modules/rlm_eap/libeap/eap_tls.h
index 73c7fdd53b..8e5fc773d6 100644
--- a/src/modules/rlm_eap/libeap/eap_tls.h
+++ b/src/modules/rlm_eap/libeap/eap_tls.h
@@ -62,11 +62,11 @@ int	eaptls_fail(eap_handler_t *handler, int peap_flag) CC_HINT(nonnull);
 int	eaptls_request(EAP_DS *eap_ds, tls_session_t *ssn) CC_HINT(nonnull);
 
 
-void			T_PRF(unsigned char const *secret, unsigned int secret_len, char const *prf_label, unsigned char const *seed,  unsigned int seed_len, unsigned char *out, unsigned int out_len) CC_HINT(nonnull(1,3,6));
-void	eaptls_gen_mppe_keys(REQUEST *request, SSL *s, char const *prf_label);
+void	T_PRF(unsigned char const *secret, unsigned int secret_len, char const *prf_label, unsigned char const *seed,  unsigned int seed_len, unsigned char *out, unsigned int out_len) CC_HINT(nonnull(1,3,6));
+void	eaptls_gen_mppe_keys(REQUEST *request, SSL *s, char const *label, uint8_t const *context, size_t context_size);
 void	eapttls_gen_challenge(SSL *s, uint8_t *buffer, size_t size);
-void	eaptls_gen_eap_key(RADIUS_PACKET *packet, SSL *s, uint32_t header);
-void			eap_fast_tls_gen_challenge(SSL *ssl, uint8_t *buffer, uint8_t *scratch, size_t size, char const *prf_label) CC_HINT(nonnull);
+void	eaptls_gen_eap_key(eap_handler_t *handler);
+void	eap_fast_tls_gen_challenge(SSL *ssl, int version, uint8_t *buffer, size_t size, char const *prf_label) CC_HINT(nonnull);
 
 #define BUFFER_SIZE 1024
 
@@ -100,7 +100,7 @@ typedef struct tls_packet {
 /* EAP-TLS framework */
 EAPTLS_PACKET	*eaptls_alloc(void);
 void		eaptls_free(EAPTLS_PACKET **eaptls_packet_ptr);
-tls_session_t	*eaptls_session(eap_handler_t *handler, fr_tls_server_conf_t *tls_conf, bool client_cert);
+tls_session_t	*eaptls_session(eap_handler_t *handler, fr_tls_server_conf_t *tls_conf, bool client_cert, bool allow_tls13);
 int		eaptls_start(EAP_DS *eap_ds, int peap);
 int		eaptls_compose(EAP_DS *eap_ds, EAPTLS_PACKET *reply);
 
diff --git a/src/modules/rlm_eap/libeap/mppe_keys.c b/src/modules/rlm_eap/libeap/mppe_keys.c
index 3a9e864104..385441c62f 100644
--- a/src/modules/rlm_eap/libeap/mppe_keys.c
+++ b/src/modules/rlm_eap/libeap/mppe_keys.c
@@ -26,11 +26,16 @@ RCSID("$Id$")
 USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 
 #include "eap_tls.h"
+#include <openssl/ssl.h>
 #include <openssl/hmac.h>
+#include <freeradius-devel/openssl3.h>
 
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+#include <openssl/provider.h>
+#endif
 
 /*
- * TLS PRF from RFC 2246
+ *	TLS P_hash from RFC 2246/5246 section 5
  */
 static void P_hash(EVP_MD const *evp_md,
 		   unsigned char const *secret, unsigned int secret_len,
@@ -38,23 +43,18 @@ static void P_hash(EVP_MD const *evp_md,
 		   unsigned char *out, unsigned int out_len)
 {
 	HMAC_CTX *ctx_a, *ctx_out;
-	unsigned char a[HMAC_MAX_MD_CBLOCK];
-	unsigned int size;
+	unsigned char a[EVP_MAX_MD_SIZE];
+	unsigned int size = EVP_MAX_MD_SIZE;
+	unsigned int digest_len;
 
 	ctx_a = HMAC_CTX_new();
 	ctx_out = HMAC_CTX_new();
-#ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW
-	HMAC_CTX_set_flags(ctx_a, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
-	HMAC_CTX_set_flags(ctx_out, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
-#endif
 	HMAC_Init_ex(ctx_a, secret, secret_len, evp_md, NULL);
 	HMAC_Init_ex(ctx_out, secret, secret_len, evp_md, NULL);
 
-	size = HMAC_size(ctx_out);
-
 	/* Calculate A(1) */
 	HMAC_Update(ctx_a, seed, seed_len);
-	HMAC_Final(ctx_a, a, NULL);
+	HMAC_Final(ctx_a, a, &size);
 
 	while (1) {
 		/* Calculate next part of output */
@@ -63,13 +63,15 @@ static void P_hash(EVP_MD const *evp_md,
 
 		/* Check if last part */
 		if (out_len < size) {
-			HMAC_Final(ctx_out, a, NULL);
+			digest_len = EVP_MAX_MD_SIZE;
+			HMAC_Final(ctx_out, a, &digest_len);
 			memcpy(out, a, out_len);
 			break;
 		}
 
 		/* Place digest in output buffer */
-		HMAC_Final(ctx_out, out, NULL);
+		digest_len = EVP_MAX_MD_SIZE;
+		HMAC_Final(ctx_out, out, &digest_len);
 		HMAC_Init_ex(ctx_out, NULL, 0, NULL, NULL);
 		out += size;
 		out_len -= size;
@@ -77,7 +79,8 @@ static void P_hash(EVP_MD const *evp_md,
 		/* Calculate next A(i) */
 		HMAC_Init_ex(ctx_a, NULL, 0, NULL, NULL);
 		HMAC_Update(ctx_a, a, size);
-		HMAC_Final(ctx_a, a, NULL);
+		digest_len = EVP_MAX_MD_SIZE;
+		HMAC_Final(ctx_a, a, &digest_len);
 	}
 
 	HMAC_CTX_free(ctx_a);
@@ -85,6 +88,82 @@ static void P_hash(EVP_MD const *evp_md,
 	memset(a, 0, sizeof(a));
 }
 
+/*
+ *	TLS PRF from RFC 2246 section 5
+ */
+static void PRF(unsigned char const *secret, unsigned int secret_len,
+		unsigned char const *seed,   unsigned int seed_len,
+		unsigned char *out, unsigned int out_len)
+{
+	uint8_t buf[out_len + (out_len % SHA_DIGEST_LENGTH)];
+	unsigned int i;
+
+	unsigned int len = (secret_len + 1) / 2;
+	uint8_t const *s1 = secret;
+	uint8_t const *s2 = secret + (secret_len - len);
+
+	EVP_MD const *md5 = NULL;
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+	EVP_MD *md5_to_free = NULL;
+
+	/*
+	 *	If we are using OpenSSL >= 3.0 and FIPS mode is
+	 *	enabled, we need to load the default provider in a
+	 *	standalone context in order to access MD5.
+	 */
+	OSSL_LIB_CTX	*libctx = NULL;
+	OSSL_PROVIDER	*default_provider = NULL;
+
+	if (EVP_default_properties_is_fips_enabled(NULL)) {
+		libctx = OSSL_LIB_CTX_new();
+		default_provider = OSSL_PROVIDER_load(libctx, "default");
+
+		if (!default_provider) {
+			ERROR("Failed loading OpenSSL default provider.");
+			return;
+		}
+
+		md5_to_free = EVP_MD_fetch(libctx, "MD5", NULL);
+		if (!md5_to_free) {
+			ERROR("Failed loading OpenSSL MD5 function.");
+			return;
+		}
+
+		md5 = md5_to_free;
+	} else {
+		md5 = EVP_md5();
+	}
+#else
+	md5 = EVP_md5();
+#endif
+
+	P_hash(md5, s1, len, seed, seed_len, out, out_len);
+	P_hash(EVP_sha1(), s2, len, seed, seed_len, buf, out_len);
+
+	for (i = 0; i < out_len; i++) {
+		out[i] ^= buf[i];
+	}
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+	if (libctx) {
+		OSSL_PROVIDER_unload(default_provider);
+		OSSL_LIB_CTX_free(libctx);
+		EVP_MD_free(md5_to_free);
+	}
+#endif
+}
+
+/*
+ *	TLS 1.2 PRF from RFC 5246 section 5
+ */
+static void PRFv12(unsigned char const *secret, unsigned int secret_len,
+		   unsigned char const *seed,   unsigned int seed_len,
+		   unsigned char *out, unsigned int out_len)
+{
+	P_hash(EVP_sha256(), secret, secret_len, seed, seed_len, out, out_len);
+}
+
 /*  EAP-FAST Pseudo-Random Function (T-PRF): RFC 4851, Section 5.5 */
 void T_PRF(unsigned char const *secret, unsigned int secret_len,
 	   char const *prf_label,
@@ -128,60 +207,55 @@ void T_PRF(unsigned char const *secret, unsigned int secret_len,
 	talloc_free(buf);
 }
 
-static void PRF(unsigned char const *secret, unsigned int secret_len,
-		unsigned char const *seed,   unsigned int seed_len,
-		unsigned char *out, unsigned char *buf, unsigned int out_len)
-{
-	unsigned int i;
-	unsigned int len = (secret_len + 1) / 2;
-	uint8_t const *s1 = secret;
-	uint8_t const *s2 = secret + (secret_len - len);
-
-	P_hash(EVP_md5(),  s1, len, seed, seed_len, out, out_len);
-	P_hash(EVP_sha1(), s2, len, seed, seed_len, buf, out_len);
-
-	for (i=0; i < out_len; i++) {
-		out[i] ^= buf[i];
-	}
-}
-
 #define EAPTLS_MPPE_KEY_LEN     32
 
 /*
  *	Generate keys according to RFC 2716 and add to reply
  */
-void eaptls_gen_mppe_keys(REQUEST *request, SSL *s, char const *prf_label)
+void eaptls_gen_mppe_keys(REQUEST *request, SSL *s, char const *label, uint8_t const *context, UNUSED size_t context_size)
 {
 	uint8_t out[4 * EAPTLS_MPPE_KEY_LEN];
 	uint8_t *p;
-	size_t prf_size;
+	size_t len;
 
-	prf_size = strlen(prf_label);
+	len = strlen(label);
 
 #if OPENSSL_VERSION_NUMBER >= 0x10001000L
-	if (SSL_export_keying_material(s, out, sizeof(out), prf_label, prf_size, NULL, 0, 0) != 1) {
+	if (SSL_export_keying_material(s, out, sizeof(out), label, len, context, context_size, context != NULL) != 1) {
 		ERROR("Failed generating keying material");
 		return;
 	}
 #else
 	{
-		uint8_t seed[64 + (2 * SSL3_RANDOM_SIZE)];
+		uint8_t seed[64 + (2 * SSL3_RANDOM_SIZE) + (context ? 2 + context_size : 0)];
 		uint8_t buf[4 * EAPTLS_MPPE_KEY_LEN];
 
 		p = seed;
 
-		memcpy(p, prf_label, prf_size);
-		p += prf_size;
+		memcpy(p, label, len);
+		p += len;
 
 		memcpy(p, s->s3->client_random, SSL3_RANDOM_SIZE);
 		p += SSL3_RANDOM_SIZE;
-		prf_size += SSL3_RANDOM_SIZE;
+		len += SSL3_RANDOM_SIZE;
 
 		memcpy(p, s->s3->server_random, SSL3_RANDOM_SIZE);
-		prf_size += SSL3_RANDOM_SIZE;
+		p += SSL3_RANDOM_SIZE;
+		len += SSL3_RANDOM_SIZE;
+
+		if (context) {
+			/* cloned and reversed FR_PUT_LE16 */
+			p[0] = ((uint16_t) (context_size)) >> 8;
+			p[1] = ((uint16_t) (context_size)) & 0xff;
+			p += 2;
+			len += 2;
+			memcpy(p, context, context_size);
+			p += context_size;
+			len += context_size;
+		}
 
 		PRF(s->session->master_key, s->session->master_key_length,
-		    seed, prf_size, out, buf, sizeof(out));
+		    seed, len, out, buf, sizeof(out));
 	}
 #endif
 
@@ -195,7 +269,7 @@ void eaptls_gen_mppe_keys(REQUEST *request, SSL *s, char const *prf_label)
 }
 
 
-#define FR_TLS_PRF_CHALLENGE	"ttls challenge"
+#define FR_TLS_PRF_CHALLENGE		"ttls challenge"
 
 /*
  *	Generate the TTLS challenge
@@ -206,9 +280,10 @@ void eaptls_gen_mppe_keys(REQUEST *request, SSL *s, char const *prf_label)
 void eapttls_gen_challenge(SSL *s, uint8_t *buffer, size_t size)
 {
 #if OPENSSL_VERSION_NUMBER >= 0x10001000L
-	SSL_export_keying_material(s, buffer, size, FR_TLS_PRF_CHALLENGE,
-				   sizeof(FR_TLS_PRF_CHALLENGE) - 1, NULL, 0, 0);
-
+	if (SSL_export_keying_material(s, buffer, size, FR_TLS_PRF_CHALLENGE,
+				       sizeof(FR_TLS_PRF_CHALLENGE)-1, NULL, 0, 0) != 1) {
+		ERROR("Failed generating keying material");
+	}
 #else
 	uint8_t out[32], buf[32];
 	uint8_t seed[sizeof(FR_TLS_PRF_CHALLENGE)-1 + 2*SSL3_RANDOM_SIZE];
@@ -226,14 +301,20 @@ void eapttls_gen_challenge(SSL *s, uint8_t *buffer, size_t size)
 #endif
 }
 
+#define FR_TLS_EXPORTER_METHOD_ID	"EXPORTER_EAP_TLS_Method-Id"
+
 /*
  *	Actually generates EAP-Session-Id, which is an internal server
  *	attribute.  Not all systems want to send EAP-Key-Name.
  */
-void eaptls_gen_eap_key(RADIUS_PACKET *packet, SSL *ssl, uint32_t header)
+void eaptls_gen_eap_key(eap_handler_t *handler)
 {
+	RADIUS_PACKET *packet = handler->request->reply;
+	tls_session_t *tls_session = handler->opaque;
+	SSL *s = tls_session->ssl;
 	VALUE_PAIR *vp;
 	uint8_t *buff, *p;
+	uint8_t type = handler->type & 0xff;
 
 	vp = fr_pair_afrom_num(packet, PW_EAP_SESSION_ID, 0);
 	if (!vp) return;
@@ -241,11 +322,33 @@ void eaptls_gen_eap_key(RADIUS_PACKET *packet, SSL *ssl, uint32_t header)
 	vp->vp_length = 1 + 2 * SSL3_RANDOM_SIZE;
 	buff = p = talloc_array(vp, uint8_t, vp->vp_length);
 
-	*p++ = header & 0xff;
+	*p++ = type;
 
-	SSL_get_client_random(ssl, p, SSL3_RANDOM_SIZE);
-	p += SSL3_RANDOM_SIZE;
-	SSL_get_server_random(ssl, p, SSL3_RANDOM_SIZE);
+	switch (SSL_version(tls_session->ssl)) {
+	case TLS1_VERSION:
+	case TLS1_1_VERSION:
+	case TLS1_2_VERSION:
+		SSL_get_client_random(s, p, SSL3_RANDOM_SIZE);
+		p += SSL3_RANDOM_SIZE;
+		SSL_get_server_random(s, p, SSL3_RANDOM_SIZE);
+		break;
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#ifdef TLS1_3_VERSION
+	case TLS1_3_VERSION:
+#endif
+	default:
+	{
+		uint8_t const context[] = { type };
+
+		if (SSL_export_keying_material(s, p, 2 * SSL3_RANDOM_SIZE,
+					       FR_TLS_EXPORTER_METHOD_ID, sizeof(FR_TLS_EXPORTER_METHOD_ID)-1,
+					       context, sizeof(context), 1) != 1) {
+			ERROR("Failed generating keying material");
+			return;
+		}
+	}
+#endif
+	}
 
 	vp->vp_octets = buff;
 	fr_pair_add(&packet->vps, vp);
@@ -254,7 +357,7 @@ void eaptls_gen_eap_key(RADIUS_PACKET *packet, SSL *ssl, uint32_t header)
 /*
  *	Same as before, but for EAP-FAST the order of {server,client}_random is flipped
  */
-void eap_fast_tls_gen_challenge(SSL *s, uint8_t *buffer, uint8_t *scratch, size_t size, char const *prf_label)
+void eap_fast_tls_gen_challenge(SSL *s, int version, uint8_t *buffer, size_t size, char const *prf_label)
 {
 	uint8_t *p;
 	size_t len, master_key_len;
@@ -273,7 +376,9 @@ void eap_fast_tls_gen_challenge(SSL *s, uint8_t *buffer, uint8_t *scratch, size_
 	p += SSL3_RANDOM_SIZE;
 
 	master_key_len = SSL_SESSION_get_master_key(SSL_get_session(s), master_key, sizeof(master_key));
-	PRF(master_key, master_key_len, seed, p - seed, buffer, scratch, size);
-}
-
 
+	if (version == TLS1_2_VERSION)
+		PRFv12(master_key, master_key_len, seed, p - seed, buffer, size);
+	else
+		PRF(master_key, master_key_len, seed, p - seed, buffer, size);
+}
diff --git a/src/modules/rlm_eap/radeapclient.c b/src/modules/rlm_eap/radeapclient.c
index 553a6a6a57..cd504a8363 100644
--- a/src/modules/rlm_eap/radeapclient.c
+++ b/src/modules/rlm_eap/radeapclient.c
@@ -182,6 +182,14 @@ static void rc_get_port(PW_CODE type, uint16_t *port);
 static void rc_evprep_packet_timeout(rc_transaction_t *trans);
 static void rc_deallocate_id(rc_transaction_t *trans);
 
+/*
+ *	For cbtls_cache_*()
+ */
+rlm_rcode_t process_post_auth(UNUSED int postauth_type, UNUSED REQUEST *request)
+{
+	return RLM_MODULE_FAIL;
+}
+
 
 static void NEVER_RETURNS usage(void)
 {
diff --git a/src/modules/rlm_eap/types/rlm_eap_fast/eap_fast.c b/src/modules/rlm_eap/types/rlm_eap_fast/eap_fast.c
index b0953aa1d4..bbb5a03c95 100644
--- a/src/modules/rlm_eap/types/rlm_eap_fast/eap_fast.c
+++ b/src/modules/rlm_eap/types/rlm_eap_fast/eap_fast.c
@@ -61,33 +61,51 @@ static int openssl_get_keyblock_size(REQUEST *request, SSL *ssl)
 		return -1;
 
 	RDEBUG2("OpenSSL: keyblock size: key_len=%d MD_size=%d "
-		   "IV_len=%d", EVP_CIPHER_key_length(c), md_size,
-		   EVP_CIPHER_iv_length(c));
+		"IV_len=%d", EVP_CIPHER_key_length(c), md_size,
+		EVP_CIPHER_iv_length(c));
 	return 2 * (EVP_CIPHER_key_length(c) +
 		    md_size +
 		    EVP_CIPHER_iv_length(c));
 #else
 	const SSL_CIPHER *ssl_cipher;
 	int cipher, digest;
+	int mac_key_len, enc_key_len, fixed_iv_len;
 
 	ssl_cipher = SSL_get_current_cipher(ssl);
 	if (!ssl_cipher)
 		return -1;
 	cipher = SSL_CIPHER_get_cipher_nid(ssl_cipher);
 	digest = SSL_CIPHER_get_digest_nid(ssl_cipher);
-	RDEBUG2("OpenSSL: cipher nid %d digest nid %d", cipher, digest);
+	RDEBUG3("OpenSSL: cipher nid %d digest nid %d",
+		cipher, digest);
 	if (cipher < 0 || digest < 0)
 		return -1;
+	if (cipher == NID_undef) {
+		RDEBUG3("OpenSSL: no cipher in use?!");
+		return -1;
+	}
 	c = EVP_get_cipherbynid(cipher);
-	h = EVP_get_digestbynid(digest);
-	if (!c || !h)
+	if (!c)
 		return -1;
+	enc_key_len = EVP_CIPHER_key_length(c);
+	if (EVP_CIPHER_mode(c) == EVP_CIPH_GCM_MODE ||
+	    EVP_CIPHER_mode(c) == EVP_CIPH_CCM_MODE)
+		fixed_iv_len = 4; /* only part of IV from PRF */
+	else
+		fixed_iv_len = EVP_CIPHER_iv_length(c);
+	if (digest == NID_undef) {
+		RDEBUG3("OpenSSL: no digest in use (e.g., AEAD)");
+		mac_key_len = 0;
+	} else {
+		h = EVP_get_digestbynid(digest);
+		if (!h)
+			return -1;
+		mac_key_len = EVP_MD_size(h);
+	}
 
-	RDEBUG2("OpenSSL: keyblock size: key_len=%d MD_size=%d IV_len=%d",
-		   EVP_CIPHER_key_length(c), EVP_MD_size(h),
-		   EVP_CIPHER_iv_length(c));
-	return 2 * (EVP_CIPHER_key_length(c) + EVP_MD_size(h) +
-		    EVP_CIPHER_iv_length(c));
+	RDEBUG2("OpenSSL: keyblock size: mac_key_len=%d enc_key_len=%d fixed_iv_len=%d",
+		mac_key_len, enc_key_len, fixed_iv_len);
+	return 2 * (mac_key_len + enc_key_len + fixed_iv_len);
 #endif
 }
 
@@ -98,7 +116,6 @@ static void eap_fast_init_keys(REQUEST *request, tls_session_t *tls_session)
 {
 	eap_fast_tunnel_t *t = tls_session->opaque;
 	uint8_t *buf;
-	uint8_t *scratch;
 	size_t ksize;
 
 	RDEBUG2("Deriving EAP-FAST keys");
@@ -108,11 +125,10 @@ static void eap_fast_init_keys(REQUEST *request, tls_session_t *tls_session)
 	ksize = openssl_get_keyblock_size(request, tls_session->ssl);
 	rad_assert(ksize > 0);
 	buf = talloc_size(request, ksize + sizeof(*t->keyblock));
-	scratch = talloc_size(request, ksize + sizeof(*t->keyblock));
 
 	t->keyblock = talloc(t, eap_fast_keyblock_t);
 
-	eap_fast_tls_gen_challenge(tls_session->ssl, buf, scratch, ksize + sizeof(*t->keyblock), "key expansion");
+	eap_fast_tls_gen_challenge(tls_session->ssl, SSL_version(tls_session->ssl), buf, ksize + sizeof(*t->keyblock), "key expansion");
 	memcpy(t->keyblock, &buf[ksize], sizeof(*t->keyblock));
 	memset(buf, 0, ksize + sizeof(*t->keyblock));
 
@@ -123,7 +139,6 @@ static void eap_fast_init_keys(REQUEST *request, tls_session_t *tls_session)
 	t->imckc = 0;
 
 	talloc_free(buf);
-	talloc_free(scratch);
 }
 
 /**
@@ -256,6 +271,7 @@ static void eap_fast_send_pac_tunnel(REQUEST *request, tls_session_t *tls_sessio
 	dlen = eap_fast_encrypt((unsigned const char *)&opaque_plaintext, sizeof(opaque_plaintext),
 				    t->a_id, PAC_A_ID_LENGTH, t->pac_opaque_key, pac.opaque.iv,
 				    pac.opaque.data, pac.opaque.tag);
+	if (dlen < 0) return;
 
 	pac.opaque.hdr.type = htons(EAP_FAST_TLV_MANDATORY | PAC_INFO_PAC_OPAQUE);
 	pac.opaque.hdr.length = htons(sizeof(pac.opaque) - sizeof(pac.opaque.hdr) - sizeof(pac.opaque.data) + dlen);
@@ -765,6 +781,7 @@ static rlm_rcode_t CC_HINT(nonnull) process_reply( eap_handler_t *eap_session,
 	switch (reply->code) {
 	case PW_CODE_ACCESS_ACCEPT:
 		RDEBUG("Got tunneled Access-Accept");
+		tls_session->authentication_success = true;
 		rcode = RLM_MODULE_OK;
 
 		for (vp = fr_cursor_init(&cursor, &reply->vps); vp; vp = fr_cursor_next(&cursor)) {
@@ -1203,8 +1220,12 @@ PW_CODE eap_fast_process(eap_handler_t *eap_session, tls_session_t *tls_session)
 				t->mode = EAP_FAST_PROVISIONING_AUTH;
 			}
 
-			if (!t->pac.expires || t->pac.expired || t->pac.expires - time(NULL) < t->pac_lifetime * 0.6)
+			/*
+			 *	Send a new pac at ~0.6 times the lifetime.
+			 */
+			if (!t->pac.expires || t->pac.expired || t->pac.expires < (time(NULL) + (t->pac_lifetime >> 1) + (t->pac_lifetime >> 3))) {
 				t->pac.send = true;
+			}
 		}
 
 		eap_fast_init_keys(request, tls_session);
diff --git a/src/modules/rlm_eap/types/rlm_eap_fast/rlm_eap_fast.c b/src/modules/rlm_eap/types/rlm_eap_fast/rlm_eap_fast.c
index 2ce2dd0c3b..093dc868cd 100644
--- a/src/modules/rlm_eap/types/rlm_eap_fast/rlm_eap_fast.c
+++ b/src/modules/rlm_eap/types/rlm_eap_fast/rlm_eap_fast.c
@@ -131,6 +131,25 @@ static int mod_instantiate(CONF_SECTION *cs, void **instance)
 		return -1;
 	}
 
+#ifdef TLS1_3_VERSION
+	if (inst->tls_conf->min_version == TLS1_3_VERSION) {
+		ERROR("There are no standards for using TLS 1.3 with EAP-FAST.");
+		ERROR("You MUST enable TLS 1.2 for EAP-FAST to work.");
+		return -1;
+	}
+
+	if ((inst->tls_conf->max_version == TLS1_3_VERSION) ||
+	    (inst->tls_conf->min_version == TLS1_3_VERSION)) {
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+		WARN("!! There is no standard for using EAP-FAST with TLS 1.3");
+		WARN("!! Please set tls_max_version = \"1.2\"");
+		WARN("!! FreeRADIUS only supports TLS 1.3 for special builds of wpa_supplicant and Windows");
+		WARN("!! This limitation is likely to change in late 2021.");
+		WARN("!! If you are using this version of FreeRADIUS after 2021, you will probably need to upgrade");
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+	}
+#endif
+
 	rad_assert(PAC_A_ID_LENGTH == MD5_DIGEST_LENGTH);
 	FR_MD5_CTX ctx;
 	fr_md5_init(&ctx);
@@ -241,7 +260,7 @@ static int _session_ticket(SSL *s, uint8_t const *data, int len, void *arg)
 	DICT_ATTR const	*fast_da;
 	char const		*errmsg;
 	int			dlen, plen;
-	uint16_t		length;
+	int			length;
 	eap_fast_attr_pac_opaque_t const	*opaque = (eap_fast_attr_pac_opaque_t const *) data;
 	eap_fast_attr_pac_opaque_t		opaque_plaintext;
 
@@ -274,7 +293,7 @@ error:
 	 * so we have to use the length in the PAC-Opaque header
 	 */
 	length = ntohs(opaque->hdr.length);
-	if (len - sizeof(opaque->hdr) < length) {
+	if (len < (int) (length + sizeof(opaque->hdr))) {
 		errmsg = "PAC has bad length in header";
 		goto error;
 	}
@@ -293,7 +312,7 @@ error:
 	plen = eap_fast_decrypt(opaque->data, dlen, opaque->aad, PAC_A_ID_LENGTH,
 					(uint8_t const *) opaque->tag, t->pac_opaque_key, opaque->iv,
 					(uint8_t *)&opaque_plaintext);
-	if (plen == -1) {
+	if (plen < 0) {
 		errmsg = "PAC failed to decrypt";
 		goto error;
 	}
@@ -314,8 +333,8 @@ error:
 			break;
 		case PAC_INFO_PAC_LIFETIME:
 			rad_assert(t->pac.expires == 0);
-			t->pac.expires = vp->vp_integer;
-			t->pac.expired = (vp->vp_integer <= time(NULL));
+			t->pac.expires = vp->vp_integer + time(NULL);
+			t->pac.expired = false;
 			break;
 		case PAC_INFO_PAC_KEY:
 			rad_assert(t->pac.key == NULL);
@@ -392,7 +411,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 	if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) {
 		REDEBUG("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	} else {
-		RDEBUG2("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		RDEBUG3("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	}
 
 	/*
@@ -553,7 +572,13 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 	} else {
 		client_cert = inst->req_client_cert;
 	}
-	handler->opaque = tls_session = eaptls_session(handler, inst->tls_conf, client_cert);
+
+	/*
+	 *	Don't allow TLS 1.3 for us, even if it's allowed
+	 *	elsewhere.  We haven't implemented the necessary
+	 *	changes, so we don't allow it.
+	 */
+	handler->opaque = tls_session = eaptls_session(handler, inst->tls_conf, client_cert, false);
 
 	if (!tls_session) return 0;
 
@@ -566,16 +591,20 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 		}
 	}
 
-#ifdef SSL_OP_NO_TLSv1_2
-	/*
-	 *	Forcibly disable TLSv1.2
-	 *
-	 *	@fixme - TLSv1.2 uses a different PRF and
-	 *	SSL_export_keying_material("key expansion") is
-	 *	forbidden
-	 */
-	SSL_set_options(tls_session->ssl, SSL_OP_NO_TLSv1_2);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+	{
+		int i;
+		for (i = 0; ; i++) {
+			const char *cipher = SSL_get_cipher_list(tls_session->ssl, i);
+			if (!cipher) break;
+			if (!strstr(cipher, "ADH-")) continue;
+			RDEBUG("Setting security level to 0 to allow anonymous cipher suites");
+			SSL_set_security_level(tls_session->ssl, 0);
+			break;
+		}
+	}
 #endif
+
 #ifdef SSL_OP_NO_TLSv1_3
 	/*
 	 *	Forcibly disable TLSv1.3
@@ -599,6 +628,7 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 	rcode = eap_fast_tls_start(handler->eap_ds, tls_session);
 
 	if (rcode < 0) {
+	error:
 		talloc_free(tls_session);
 		return 0;
 	}
@@ -607,7 +637,7 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 
 	if (!SSL_set_session_ticket_ext_cb(tls_session->ssl, _session_ticket, tls_session)) {
 		RERROR("Failed setting SSL session ticket callback");
-		return 0;
+		goto error;
 	}
 
 	handler->stage = PROCESS;
diff --git a/src/modules/rlm_eap/types/rlm_eap_peap/peap.c b/src/modules/rlm_eap/types/rlm_eap_peap/peap.c
index deaf702d61..5647f613af 100644
--- a/src/modules/rlm_eap/types/rlm_eap_peap/peap.c
+++ b/src/modules/rlm_eap/types/rlm_eap_peap/peap.c
@@ -442,6 +442,7 @@ static rlm_rcode_t CC_HINT(nonnull) process_reply(eap_handler_t *handler, tls_se
 	switch (reply->code) {
 	case PW_CODE_ACCESS_ACCEPT:
 		RDEBUG2("Tunneled authentication was successful");
+		tls_session->authentication_success = true;
 		t->status = PEAP_STATUS_SENT_TLV_SUCCESS;
 		eappeap_success(handler, tls_session);
 		rcode = RLM_MODULE_HANDLED;
@@ -790,6 +791,7 @@ rlm_rcode_t eappeap_process(eap_handler_t *handler, tls_session_t *tls_session,
 			/* send an identity request */
 			t->session_resumption_state = PEAP_RESUMPTION_NO;
 			t->status = PEAP_STATUS_INNER_IDENTITY_REQ_SENT;
+			tls_session->session_not_resumed = true;
 			eappeap_identity(handler, tls_session);
 		}
 		return RLM_MODULE_HANDLED;
@@ -903,15 +905,15 @@ rlm_rcode_t eappeap_process(eap_handler_t *handler, tls_session_t *tls_session,
 
 		return RLM_MODULE_REJECT;
 
-		case PEAP_STATUS_PHASE2_INIT:
-			RDEBUG("In state machine in phase2 init?");
+	case PEAP_STATUS_PHASE2_INIT:
+		RDEBUG("In state machine in phase2 init?");
 
-		case PEAP_STATUS_PHASE2:
-			break;
+	case PEAP_STATUS_PHASE2:
+		break;
 
-		default:
-			REDEBUG("Unhandled state in peap");
-			return RLM_MODULE_REJECT;
+	default:
+		REDEBUG("Unhandled state in peap");
+		return RLM_MODULE_REJECT;
 	}
 
 	fake = request_alloc_fake(request);
@@ -1040,6 +1042,10 @@ rlm_rcode_t eappeap_process(eap_handler_t *handler, tls_session_t *tls_session,
 
 		if (vp) {
 			eap_tunnel_data_t *tunnel;
+			bool proxy_as_eap = t->proxy_tunneled_request_as_eap;
+			VALUE_PAIR *flag = fr_pair_find_by_num(fake->config, PW_PROXY_TUNNELED_REQUEST_AS_EAP, 0, TAG_ANY);
+
+			if (flag) proxy_as_eap = flag->vp_integer;
 
 			/*
 			 *	The tunneled request was NOT handled,
@@ -1056,7 +1062,7 @@ rlm_rcode_t eappeap_process(eap_handler_t *handler, tls_session_t *tls_session,
 			 *	Once the tunneled EAP session is ALMOST
 			 *	done, THEN we proxy it...
 			 */
-			if (!t->proxy_tunneled_request_as_eap) {
+			if (!proxy_as_eap) {
 				fake->options |= RAD_REQUEST_OPTION_PROXY_EAP;
 
 				/*
diff --git a/src/modules/rlm_eap/types/rlm_eap_peap/rlm_eap_peap.c b/src/modules/rlm_eap/types/rlm_eap_peap/rlm_eap_peap.c
index 3d23322663..d9f850cef2 100644
--- a/src/modules/rlm_eap/types/rlm_eap_peap/rlm_eap_peap.c
+++ b/src/modules/rlm_eap/types/rlm_eap_peap/rlm_eap_peap.c
@@ -192,7 +192,10 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 		client_cert = inst->req_client_cert;
 	}
 
-	ssn = eaptls_session(handler, inst->tls_conf, client_cert);
+	/*
+	 *	Allow TLS 1.3, it works.
+	 */
+	ssn = eaptls_session(handler, inst->tls_conf, client_cert, true);
 	if (!ssn) {
 		return 0;
 	}
@@ -200,9 +203,13 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 	handler->opaque = ((void *)ssn);
 
 	/*
-	 *	Set up type-specific information.
+	 *	Set the label to a fixed string.  For TLS 1.3, the
+	 *	label is the same for all TLS-based EAP methods.  If
+	 *	the client is using TLS 1.3, then eaptls_success()
+	 *	will over-ride this label with the correct label for
+	 *	TLS 1.3.
 	 */
-	ssn->prf_label = "client EAP encryption";
+	ssn->label = "client EAP encryption";
 
 	/*
 	 *	As it is a poorly designed protocol, PEAP uses
@@ -230,7 +237,7 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 	if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) {
 		REDEBUG("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	} else {
-		RDEBUG2("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		RDEBUG3("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	}
 	if (status == 0) return 0;
 
@@ -274,7 +281,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 	if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) {
 		REDEBUG("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	} else {
-		RDEBUG2("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		RDEBUG3("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	}
 
 	/*
@@ -311,6 +318,10 @@ static int mod_process(void *arg, eap_handler_t *handler)
 	 *	data.
 	 */
 	case FR_TLS_OK:
+                /*
+                 *	TLSv1.3 makes application data immediately avaliable
+                 */
+		if (tls_session->is_init_finished && (peap->status == PEAP_STATUS_INVALID)) peap->status = PEAP_STATUS_TUNNEL_ESTABLISHED;
 		break;
 
 		/*
diff --git a/src/modules/rlm_eap/types/rlm_eap_pwd/const_time.h b/src/modules/rlm_eap/types/rlm_eap_pwd/const_time.h
new file mode 100644
index 0000000000..b717dd51b3
--- /dev/null
+++ b/src/modules/rlm_eap/types/rlm_eap_pwd/const_time.h
@@ -0,0 +1,190 @@
+/*
+ * Helper functions for constant time operations
+ * Copyright (c) 2019, The Linux Foundation
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ *
+ * These helper functions can be used to implement logic that needs to minimize
+ * externally visible differences in execution path by avoiding use of branches,
+ * avoiding early termination or other time differences, and forcing same memory
+ * access pattern regardless of values.
+ */
+
+#ifndef CONST_TIME_H
+#define CONST_TIME_H
+
+
+#if defined(__clang__)
+#define NO_UBSAN_UINT_OVERFLOW                                        \
+	__attribute__((no_sanitize("unsigned-integer-overflow")))
+#else
+#define NO_UBSAN_UINT_OVERFLOW
+#endif
+
+/**
+ * const_time_fill_msb - Fill all bits with MSB value
+ * @param	val Input value
+ * @return	Value with all the bits set to the MSB of the input val
+ */
+static inline unsigned int const_time_fill_msb(unsigned int val)
+{
+	/* Move the MSB to LSB and multiple by -1 to fill in all bits. */
+	return (val >> (sizeof(val) * 8 - 1)) * ~0U;
+}
+
+
+/* @return	-1 if val is zero; 0 if val is not zero */
+static inline unsigned int const_time_is_zero(unsigned int val)
+	NO_UBSAN_UINT_OVERFLOW
+{
+	/* Set MSB to 1 for 0 and fill rest of bits with the MSB value */
+	return const_time_fill_msb(~val & (val - 1));
+}
+
+
+/* @return	-1 if a == b; 0 if a != b */
+static inline unsigned int const_time_eq(unsigned int a, unsigned int b)
+{
+	return const_time_is_zero(a ^ b);
+}
+
+
+/* @return	-1 if a == b; 0 if a != b */
+static inline unsigned char const_time_eq_u8(unsigned int a, unsigned int b)
+{
+	return (unsigned char) const_time_eq(a, b);
+}
+
+
+/**
+ * const_time_eq_bin - Constant time memory comparison
+ * @param	a First buffer to compare
+ * @param	b Second buffer to compare
+ * @param	len Number of octets to compare
+ * @return	-1 if buffers are equal, 0 if not
+ *
+ * This function is meant for comparing passwords or hash values where
+ * difference in execution time or memory access pattern could provide external
+ * observer information about the location of the difference in the memory
+ * buffers. The return value does not behave like memcmp(), i.e.,
+ * const_time_eq_bin() cannot be used to sort items into a defined order. Unlike
+ * memcmp(), the execution time of const_time_eq_bin() does not depend on the
+ * contents of the compared memory buffers, but only on the total compared
+ * length.
+ */
+static inline unsigned int const_time_eq_bin(const void *a, const void *b,
+					     size_t len)
+{
+	const unsigned char *aa = a;
+	const unsigned char *bb = b;
+	size_t i;
+	unsigned char res = 0;
+
+	for (i = 0; i < len; i++)
+		res |= aa[i] ^ bb[i];
+
+	return const_time_is_zero(res);
+}
+
+
+/**
+ * const_time_select - Constant time unsigned int selection
+ * @param	mask 0 (false) or -1 (true) to identify which value to select
+ * @param	true_val Value to select for the true case
+ * @param	false_val Value to select for the false case
+ * @return	true_val if mask == -1, false_val if mask == 0
+ */
+static inline unsigned int const_time_select(unsigned int mask,
+					     unsigned int true_val,
+					     unsigned int false_val)
+{
+	return (mask & true_val) | (~mask & false_val);
+}
+
+
+/**
+ * const_time_select_int - Constant time int selection
+ * @param	mask 0 (false) or -1 (true) to identify which value to select
+ * @param	true_val Value to select for the true case
+ * @param	false_val Value to select for the false case
+ * @return	true_val if mask == -1, false_val if mask == 0
+ */
+static inline int const_time_select_int(unsigned int mask, int true_val,
+					int false_val)
+{
+	return (int) const_time_select(mask, (unsigned int) true_val,
+				       (unsigned int) false_val);
+}
+
+
+/**
+ * const_time_select_u8 - Constant time u8 selection
+ * @param	mask 0 (false) or -1 (true) to identify which value to select
+ * @param	true_val Value to select for the true case
+ * @param	false_val Value to select for the false case
+ * @return	true_val if mask == -1, false_val if mask == 0
+ */
+static inline unsigned char const_time_select_u8(unsigned char mask, unsigned char true_val, unsigned char false_val)
+{
+	return (unsigned char) const_time_select(mask, true_val, false_val);
+}
+
+
+/**
+ * const_time_select_s8 - Constant time s8 selection
+ * @param	mask 0 (false) or -1 (true) to identify which value to select
+ * @param	true_val Value to select for the true case
+ * @param	false_val Value to select for the false case
+ * @return	true_val if mask == -1, false_val if mask == 0
+ */
+static inline char const_time_select_s8(char mask, char true_val, char false_val)
+{
+	return (char) const_time_select(mask, (unsigned int) true_val,
+				      (unsigned int) false_val);
+}
+
+
+/**
+ * const_time_select_bin - Constant time binary buffer selection copy
+ * @param	mask 0 (false) or -1 (true) to identify which value to copy
+ * @param	true_val Buffer to copy for the true case
+ * @param	false_val Buffer to copy for the false case
+ * @param	len Number of octets to copy
+ * @param	dst Destination buffer for the copy
+ *
+ * This function copies the specified buffer into the destination buffer using
+ * operations with identical memory access pattern regardless of which buffer
+ * is being copied.
+ */
+static inline void const_time_select_bin(unsigned char mask, const unsigned char *true_val,
+					 const unsigned char *false_val, size_t len,
+					 unsigned char *dst)
+{
+	size_t i;
+
+	for (i = 0; i < len; i++)
+		dst[i] = const_time_select_u8(mask, true_val[i], false_val[i]);
+}
+
+
+static inline int const_time_memcmp(const void *a, const void *b, size_t len)
+{
+	const unsigned char *aa = a;
+	const unsigned char *bb = b;
+	int diff, res = 0;
+	unsigned int mask;
+
+	if (len == 0)
+		return 0;
+	do {
+		len--;
+		diff = (int) aa[len] - (int) bb[len];
+		mask = const_time_is_zero((unsigned int) diff);
+		res = const_time_select_int(mask, res, diff);
+	} while (len);
+
+	return res;
+}
+
+#endif /* CONST_TIME_H */
diff --git a/src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.c b/src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.c
index d94851c3aa..26260527a5 100644
--- a/src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.c
+++ b/src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.c
@@ -1,7 +1,5 @@
-/*
- * Copyright (c) Dan Harkins, 2012
- *
- *  Copyright holder grants permission for redistribution and use in source
+/**
+ *  copyright holder grants permission for redistribution and use in source
  *  and binary forms, with or without modification, provided that the
  *  following conditions are met:
  *     1. Redistribution of source code must retain the above copyright
@@ -29,100 +27,237 @@
  * This license and distribution terms cannot be changed. In other words,
  * this code cannot simply be copied and put under a different distribution
  * license (including the GNU public license).
+ *
+ * @copyright (c) Dan Harkins, 2012
  */
 
 RCSID("$Id: d94851c3aa0fc31db9be2d01a4fb94c1a6c81e00 $")
 USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 
 #include "eap_pwd.h"
+#include "const_time.h"
+#include <freeradius-devel/openssl3.h>
 
-#include <freeradius-devel/radiusd.h>
-#include <freeradius-devel/modules.h>
+static uint8_t allzero[SHA256_DIGEST_LENGTH] = { 0x00 };
 
 /* The random function H(x) = HMAC-SHA256(0^32, x) */
-static void H_Init(HMAC_CTX *ctx)
+static void pwd_hmac_final(HMAC_CTX *hmac_ctx, uint8_t *digest)
 {
-	uint8_t allzero[SHA256_DIGEST_LENGTH];
+	unsigned int mdlen = SHA256_DIGEST_LENGTH;
+	HMAC_Final(hmac_ctx, digest, &mdlen);
+//	HMAC_CTX_reset(hmac_ctx);
+}
 
-	memset(allzero, 0, SHA256_DIGEST_LENGTH);
+/* a counter-based KDF based on NIST SP800-108 */
+static void eap_pwd_kdf(uint8_t *key, int keylen, char const *label,
+			int label_len, uint8_t *result, int result_bit_len)
+{
+	HMAC_CTX	*hmac_ctx;
+	uint8_t		digest[SHA256_DIGEST_LENGTH];
+	uint16_t	i, ctr, L;
+	int		result_byte_len, len = 0;
+	unsigned int	mdlen = SHA256_DIGEST_LENGTH;
+	uint8_t		mask = 0xff;
+
+	MEM(hmac_ctx = HMAC_CTX_new());
+	result_byte_len = (result_bit_len + 7) / 8;
+
+	ctr = 0;
+	L = htons(result_bit_len);
+	while (len < result_byte_len) {
+		ctr++; i = htons(ctr);
 
-	HMAC_Init_ex(ctx, allzero, SHA256_DIGEST_LENGTH, EVP_sha256(), NULL);
+		HMAC_Init_ex(hmac_ctx, key, keylen, EVP_sha256(), NULL);
+		if (ctr > 1) HMAC_Update(hmac_ctx, digest, mdlen);
+		HMAC_Update(hmac_ctx, (uint8_t *) &i, sizeof(uint16_t));
+		HMAC_Update(hmac_ctx, (uint8_t const *)label, label_len);
+		HMAC_Update(hmac_ctx, (uint8_t *) &L, sizeof(uint16_t));
+		HMAC_Final(hmac_ctx, digest, &mdlen);
+		if ((len + (int) mdlen) > result_byte_len) {
+			memcpy(result + len, digest, result_byte_len - len);
+		} else {
+			memcpy(result + len, digest, mdlen);
+		}
+		len += mdlen;
+//		HMAC_CTX_reset(hmac_ctx);
+	}
+
+	/* since we're expanding to a bit length, mask off the excess */
+	if (result_bit_len % 8) {
+		mask <<= (8 - (result_bit_len % 8));
+		result[result_byte_len - 1] &= mask;
+	}
+
+	HMAC_CTX_free(hmac_ctx);
 }
 
-static void H_Update(HMAC_CTX *ctx, uint8_t const *data, int len)
+static BIGNUM *consttime_BN (void)
 {
-	HMAC_Update(ctx, data, len);
+	BIGNUM *bn;
+
+	bn = BN_new();
+	if (bn) BN_set_flags(bn, BN_FLG_CONSTTIME);
+	return bn;
 }
 
-static void H_Final(HMAC_CTX *ctx, uint8_t *digest)
+/*
+ * compute the legendre symbol in constant time
+ */
+static int legendre(BIGNUM *a, BIGNUM *p, BN_CTX *bnctx)
 {
-	unsigned int mdlen = SHA256_DIGEST_LENGTH;
+	int		symbol;
+	unsigned int	mask;
+	BIGNUM		*res, *pm1over2;
+
+	pm1over2 = consttime_BN();
+	res = consttime_BN();
+
+	if (!BN_sub(pm1over2, p, BN_value_one()) ||
+	    !BN_rshift1(pm1over2, pm1over2) ||
+	    !BN_mod_exp_mont_consttime(res, a, pm1over2, p, bnctx, NULL)) {
+		BN_free(pm1over2);
+		BN_free(res);
+		return -2;
+	}
+
+	symbol = -1;
+	mask = const_time_eq(BN_is_word(res, 1), 1);
+	symbol = const_time_select_int(mask, 1, symbol);
+	mask = const_time_eq(BN_is_zero(res), 1);
+	symbol = const_time_select_int(mask, -1, symbol);
+
+	BN_free(pm1over2);
+	BN_free(res);
 
-	HMAC_Final(ctx, digest, &mdlen);
+	return symbol;
 }
 
-/* a counter-based KDF based on NIST SP800-108 */
-static int eap_pwd_kdf(uint8_t *key, int keylen, char const *label, int labellen, uint8_t *result, int resultbitlen)
+static void do_equation(EC_GROUP *group, BIGNUM *y2, BIGNUM *x, BN_CTX *bnctx)
 {
-	HMAC_CTX *hctx = NULL;
-	uint8_t digest[SHA256_DIGEST_LENGTH];
-	uint16_t i, ctr, L;
-	int resultbytelen, len = 0;
-	unsigned int mdlen = SHA256_DIGEST_LENGTH;
-	uint8_t mask = 0xff;
+	BIGNUM *p, *a, *b, *tmp1, *pm1;
 
-	hctx = HMAC_CTX_new();
-	if (hctx == NULL) {
-		DEBUG("failed allocating HMAC context");
-		return -1;
+	tmp1 = BN_new();
+	pm1 = BN_new();
+	p = BN_new();
+	a = BN_new();
+	b = BN_new();
+	EC_GROUP_get_curve(group, p, a, b, bnctx);
+
+	BN_sub(pm1, p, BN_value_one());
+
+	/*
+	 * y2 = x^3 + ax + b
+	 */
+	BN_mod_sqr(tmp1, x, p, bnctx);
+	BN_mod_mul(y2, tmp1, x, p, bnctx);
+	BN_mod_mul(tmp1, a, x, p, bnctx);
+	BN_mod_add_quick(y2, y2, tmp1, p);
+	BN_mod_add_quick(y2, y2, b, p);
+
+	BN_free(tmp1);
+	BN_free(pm1);
+	BN_free(p);
+	BN_free(a);
+	BN_free(b);
+
+	return;
+}
+
+static int is_quadratic_residue(BIGNUM *val, BIGNUM *p, BIGNUM *qr, BIGNUM *qnr, BN_CTX *bnctx)
+{
+	int offset, check, ret = 0;
+	BIGNUM *r = NULL, *pm1 = NULL, *res = NULL, *qr_or_qnr = NULL;
+	unsigned int mask;
+	unsigned char *qr_bin = NULL, *qnr_bin = NULL, *qr_or_qnr_bin = NULL;
+
+	if (((r = consttime_BN()) == NULL) ||
+	    ((res = consttime_BN()) == NULL) ||
+	    ((qr_or_qnr = consttime_BN()) == NULL) ||
+	    ((pm1 = consttime_BN()) == NULL)) {
+		ret = -2;
+		goto fail;
 	}
-	resultbytelen = (resultbitlen + 7)/8;
-	ctr = 0;
-	L = htons(resultbitlen);
-	while (len < resultbytelen) {
-		ctr++; i = htons(ctr);
-		HMAC_Init_ex(hctx, key, keylen, EVP_sha256(), NULL);
-		if (ctr > 1) {
-			HMAC_Update(hctx, digest, mdlen);
-		}
-		HMAC_Update(hctx, (uint8_t *) &i, sizeof(uint16_t));
-		HMAC_Update(hctx, (uint8_t const *)label, labellen);
-		HMAC_Update(hctx, (uint8_t *) &L, sizeof(uint16_t));
-		HMAC_Final(hctx, digest, &mdlen);
-		if ((len + (int) mdlen) > resultbytelen) {
-			memcpy(result + len, digest, resultbytelen - len);
-		} else {
-			memcpy(result + len, digest, mdlen);
-		}
-		len += mdlen;
+
+	if (((qr_bin = (unsigned char *)malloc(BN_num_bytes(p))) == NULL) ||
+	    ((qnr_bin = (unsigned char *)malloc(BN_num_bytes(p))) == NULL) ||
+	    ((qr_or_qnr_bin = (unsigned char *)malloc(BN_num_bytes(p))) == NULL)) {
+		ret = -2;
+		goto fail;
 	}
-	HMAC_CTX_free(hctx);
 
-	/* since we're expanding to a bit length, mask off the excess */
-	if (resultbitlen % 8) {
-		mask <<= (8 - (resultbitlen % 8));
-		result[resultbytelen - 1] &= mask;
+	/*
+	 * we select binary in constant time so make them binary
+	 */
+	memset(qr_bin, 0, BN_num_bytes(p));
+	memset(qnr_bin, 0, BN_num_bytes(p));
+	memset(qr_or_qnr_bin, 0, BN_num_bytes(p));
+
+	offset = BN_num_bytes(p) - BN_num_bytes(qr);
+	BN_bn2bin(qr, qr_bin + offset);
+
+	offset = BN_num_bytes(p) - BN_num_bytes(qnr);
+	BN_bn2bin(qnr, qnr_bin + offset);
+
+	/*
+	 * r = (random() mod p-1) + 1
+	 */
+	BN_sub(pm1, p, BN_value_one());
+	BN_rand_range(r, pm1);
+	BN_add(r, r, BN_value_one());
+
+	BN_copy(res, val);
+
+	/*
+	 * res = val * r * r which ensures res != val but has same quadratic residocity
+	 */
+	BN_mod_mul(res, res, r, p, bnctx);
+	BN_mod_mul(res, res, r, p, bnctx);
+
+	/*
+	 * if r is even (mask is -1) then multiply by qnr and our check is qnr
+	 * otherwise multiply by qr and our check is qr
+	 */
+	mask = const_time_is_zero(BN_is_odd(r));
+	const_time_select_bin(mask, qnr_bin, qr_bin, BN_num_bytes(p), qr_or_qnr_bin);
+	BN_bin2bn(qr_or_qnr_bin, BN_num_bytes(p), qr_or_qnr);
+	BN_mod_mul(res, res, qr_or_qnr, p, bnctx);
+	check = const_time_select_int(mask, -1, 1);
+
+	if ((ret = legendre(res, p, bnctx)) == -2) {
+		ret = -1;	/* just say no it's not */
+		goto fail;
 	}
+	mask = const_time_eq(ret, check);
+	ret = const_time_select_int(mask, 1, 0);
 
-	return 0;
+fail:
+	if (qr_bin != NULL) free(qr_bin);
+	if (qnr_bin != NULL) free(qnr_bin);
+	if (qr_or_qnr_bin != NULL) free(qr_or_qnr_bin);
+	BN_free(r);
+	BN_free(res);
+	BN_free(qr_or_qnr);
+	BN_free(pm1);
+
+	return ret;
 }
 
-int compute_password_element (pwd_session_t *session, uint16_t grp_num,
+int compute_password_element (REQUEST *request, pwd_session_t *session, uint16_t grp_num,
 			      char const *password, int password_len,
 			      char const *id_server, int id_server_len,
 			      char const *id_peer, int id_peer_len,
 			      uint32_t *token)
 {
-	BIGNUM *x_candidate = NULL, *rnd = NULL, *cofactor = NULL;
-	HMAC_CTX *ctx = NULL;
-	uint8_t pwe_digest[SHA256_DIGEST_LENGTH], *prfbuf = NULL, ctr;
-	int nid, is_odd, primebitlen, primebytelen, ret = 0;
-
-	ctx = HMAC_CTX_new();
-	if (ctx == NULL) {
-		DEBUG("failed allocating HMAC context");
-		goto fail;
-	}
+	BIGNUM		*x_candidate = NULL, *rnd = NULL, *y_sqrd = NULL, *qr = NULL, *qnr = NULL, *y1 = NULL, *y2 = NULL, *y = NULL, *exp = NULL;
+	EVP_MD_CTX	*hmac_ctx;
+	EVP_PKEY	*hmac_pkey;
+	uint8_t		pwe_digest[SHA256_DIGEST_LENGTH], *prfbuf = NULL, *xbuf = NULL, *pm1buf = NULL, *y1buf = NULL, *y2buf = NULL, *ybuf = NULL, ctr;
+	int		nid, is_odd, primebitlen, primebytelen, ret = 0, found = 0, mask;
+	int		save, i, rbits, qr_or_qnr, save_is_odd = 0, cmp;
+	unsigned int	skip;
+
+	MEM(hmac_ctx = EVP_MD_CTX_new());
+	MEM(hmac_pkey = EVP_PKEY_new_mac_key(EVP_PKEY_HMAC, NULL, allzero, sizeof(allzero)));
 
 	switch (grp_num) { /* from IANA registry for IKE D-H groups */
 	case 19:
@@ -159,17 +294,23 @@ int compute_password_element (pwd_session_t *session, uint16_t grp_num,
 		goto fail;
 	}
 
-	if (((rnd = BN_new()) == NULL) ||
-	    ((cofactor = BN_new()) == NULL) ||
+	if (((rnd = consttime_BN()) == NULL) ||
 	    ((session->pwe = EC_POINT_new(session->group)) == NULL) ||
-	    ((session->order = BN_new()) == NULL) ||
-	    ((session->prime = BN_new()) == NULL) ||
-	    ((x_candidate = BN_new()) == NULL)) {
+	    ((session->order = consttime_BN()) == NULL) ||
+	    ((session->prime = consttime_BN()) == NULL) ||
+	    ((qr = consttime_BN()) == NULL) ||
+	    ((qnr = consttime_BN()) == NULL) ||
+	    ((x_candidate = consttime_BN()) == NULL) ||
+	    ((y_sqrd = consttime_BN()) == NULL) ||
+	    ((y1 = consttime_BN()) == NULL) ||
+	    ((y2 = consttime_BN()) == NULL) ||
+	    ((y = consttime_BN()) == NULL) ||
+        ((exp = consttime_BN()) == NULL)) {
 		DEBUG("unable to create bignums");
 		goto fail;
 	}
 
-	if (!EC_GROUP_get_curve_GFp(session->group, session->prime, NULL, NULL, NULL)) {
+	if (!EC_GROUP_get_curve(session->group, session->prime, NULL, NULL, NULL)) {
 		DEBUG("unable to get prime for GFp curve");
 		goto fail;
 	}
@@ -179,46 +320,80 @@ int compute_password_element (pwd_session_t *session, uint16_t grp_num,
 		goto fail;
 	}
 
-	if (!EC_GROUP_get_cofactor(session->group, cofactor, NULL)) {
-		DEBUG("unable to get cofactor for curve");
-		goto fail;
-	}
-
 	primebitlen = BN_num_bits(session->prime);
 	primebytelen = BN_num_bytes(session->prime);
 	if ((prfbuf = talloc_zero_array(session, uint8_t, primebytelen)) == NULL) {
 		DEBUG("unable to alloc space for prf buffer");
 		goto fail;
 	}
+	if ((xbuf = talloc_zero_array(request, uint8_t, primebytelen)) == NULL) {
+		DEBUG("unable to alloc space for x buffer");
+		goto fail;
+	}
+	if ((pm1buf = talloc_zero_array(request, uint8_t, primebytelen)) == NULL) {
+		DEBUG("unable to alloc space for pm1 buffer");
+		goto fail;
+	}
+	if ((y1buf = talloc_zero_array(request, uint8_t, primebytelen)) == NULL) {
+		DEBUG("unable to alloc space for y1 buffer");
+		goto fail;
+	}
+	if ((y2buf = talloc_zero_array(request, uint8_t, primebytelen)) == NULL) {
+		DEBUG("unable to alloc space for y2 buffer");
+		goto fail;
+	}
+	if ((ybuf = talloc_zero_array(request, uint8_t, primebytelen)) == NULL) {
+		DEBUG("unable to alloc space for y buffer");
+		goto fail;
+	}
+
+
+	/*
+	* derive random quadradic residue and quadratic non-residue
+	*/
+	do {
+		BN_rand_range(qr, session->prime);
+	} while (legendre(qr, session->prime, session->bnctx) != 1);
+
+	do {
+		BN_rand_range(qnr, session->prime);
+	} while (legendre(qnr, session->prime, session->bnctx) != -1);
+
+	if (!BN_sub(rnd, session->prime, BN_value_one())) {
+		goto fail;
+	}
+	BN_bn2bin(rnd, pm1buf);
+
+	save_is_odd = 0;
+	found = 0;
+	memset(xbuf, 0, primebytelen);
 	ctr = 0;
-	while (1) {
-		if (ctr > 100) {
-			DEBUG("unable to find random point on curve for group %d, something's fishy", grp_num);
-			goto fail;
-		}
+	while (ctr < 40) {
 		ctr++;
 
 		/*
 		 * compute counter-mode password value and stretch to prime
-		 *    pwd-seed = H(token | peer-id | server-id | password |
-		 *		   counter)
+		 *	pwd-seed = H(token | peer-id | server-id | password |
+		 *		     counter)
 		 */
-		H_Init(ctx);
-		H_Update(ctx, (uint8_t *)token, sizeof(*token));
-		H_Update(ctx, (uint8_t const *)id_peer, id_peer_len);
-		H_Update(ctx, (uint8_t const *)id_server, id_server_len);
-		H_Update(ctx, (uint8_t const *)password, password_len);
-		H_Update(ctx, (uint8_t *)&ctr, sizeof(ctr));
-		H_Final(ctx, pwe_digest);
+		EVP_DigestSignInit(hmac_ctx, NULL, EVP_sha256(), NULL, hmac_pkey);
+		EVP_DigestSignUpdate(hmac_ctx, (uint8_t *)token, sizeof(*token));
+		EVP_DigestSignUpdate(hmac_ctx, (uint8_t const *)id_peer, id_peer_len);
+		EVP_DigestSignUpdate(hmac_ctx, (uint8_t const *)id_server, id_server_len);
+		EVP_DigestSignUpdate(hmac_ctx, (uint8_t const *)password, password_len);
+		EVP_DigestSignUpdate(hmac_ctx, (uint8_t *)&ctr, sizeof(ctr));
+
+		{
+			size_t mdlen = SHA256_DIGEST_LENGTH;
+
+			EVP_DigestSignFinal(hmac_ctx, pwe_digest, &mdlen);
+			EVP_MD_CTX_reset(hmac_ctx);
+		}
 
 		BN_bin2bn(pwe_digest, SHA256_DIGEST_LENGTH, rnd);
-		if (eap_pwd_kdf(pwe_digest, SHA256_DIGEST_LENGTH, "EAP-pwd Hunting And Pecking",
-			        strlen("EAP-pwd Hunting And Pecking"), prfbuf, primebitlen) != 0) {
-			DEBUG("key derivation function failed");
-			goto fail;
-		}
+		eap_pwd_kdf(pwe_digest, SHA256_DIGEST_LENGTH, "EAP-pwd Hunting And Pecking",
+			    strlen("EAP-pwd Hunting And Pecking"), prfbuf, primebitlen);
 
-		BN_bin2bn(prfbuf, primebytelen, x_candidate);
 		/*
 		 * eap_pwd_kdf() returns a string of bits 0..primebitlen but
 		 * BN_bin2bn will treat that string of bits as a big endian
@@ -226,49 +401,86 @@ int compute_password_element (pwd_session_t *session, uint16_t grp_num,
 		 * then excessive bits-- those _after_ primebitlen-- so now
 		 * we have to shift right the amount we masked off.
 		 */
-		if (primebitlen % 8) BN_rshift(x_candidate, x_candidate, (8 - (primebitlen % 8)));
-		if (BN_ucmp(x_candidate, session->prime) >= 0) continue;
+		if (primebitlen % 8) {
+			rbits = 8 - (primebitlen % 8);
+			for (i = primebytelen - 1; i > 0; i--) {
+				prfbuf[i] = (prfbuf[i - 1] << (8 - rbits)) | (prfbuf[i] >> rbits);
+			}
+			prfbuf[0] >>= rbits;
+		}
+		BN_bin2bn(prfbuf, primebytelen, x_candidate);
 
 		/*
-		 * need to unambiguously identify the solution, if there is
-		 * one...
-		 */
-		is_odd = BN_is_odd(rnd) ? 1 : 0;
+		* it would've been better if the spec reduced the candidate
+		* modulo the prime but it didn't. So if the candidate >= prime
+		* we need to skip it but still run through the operations below
+		*/
+		cmp = const_time_memcmp(pm1buf, prfbuf, primebytelen);
+		skip = const_time_fill_msb((unsigned int)cmp);
 
 		/*
-		 * solve the quadratic equation, if it's not solvable then we
-		 * don't have a point
-		 */
-		if (!EC_POINT_set_compressed_coordinates_GFp(session->group, session->pwe, x_candidate, is_odd, NULL)) {
-			continue;
-		}
+		* need to unambiguously identify the solution, if there is
+		* one..
+		*/
+		is_odd = BN_is_odd(rnd);
 
 		/*
-		 * If there's a solution to the equation then the point must be
-		 * on the curve so why check again explicitly? OpenSSL code
-		 * says this is required by X9.62. We're not X9.62 but it can't
-		 * hurt just to be sure.
-		 */
-		if (!EC_POINT_is_on_curve(session->group, session->pwe, NULL)) {
-			DEBUG("EAP-pwd: point is not on curve");
-			continue;
-		}
+		* check whether x^3 + a*x + b is a quadratic residue
+		*
+		* save the first quadratic residue we find in the loop but do
+		* it in constant time.
+		*/
+		do_equation(session->group, y_sqrd, x_candidate, session->bnctx);
+		qr_or_qnr = is_quadratic_residue(y_sqrd, session->prime, qr, qnr, session->bnctx);
 
-		if (BN_cmp(cofactor, BN_value_one())) {
-			/* make sure the point is not in a small sub-group */
-			if (!EC_POINT_mul(session->group, session->pwe, NULL, session->pwe,
-				cofactor, NULL)) {
-				DEBUG("EAP-pwd: cannot multiply generator by order");
-				continue;
-			}
+		/*
+		* if the candidate >= prime then we want to skip it
+		*/
+		qr_or_qnr = const_time_select(skip, 0, qr_or_qnr);
 
-			if (EC_POINT_is_at_infinity(session->group, session->pwe)) {
-				DEBUG("EAP-pwd: point is at infinity");
-				continue;
-			}
-		}
-		/* if we got here then we have a new generator. */
-		break;
+		/*
+		* if we haven't found PWE yet (found = 0) then mask will be true,
+		* if we have found PWE then mask will be false
+		*/
+		mask = const_time_select(found, 0, -1);
+
+		/*
+		* save will be 1 if we want to save this value-- i.e. we haven't
+		* found PWE yet and this is a quadratic residue-- and 0 otherwise
+		*/
+		save = const_time_select(mask, qr_or_qnr, 0);
+
+		/*
+		* mask will be true (-1) if we want to save this and false (0)
+		* otherwise
+		*/
+		mask = const_time_eq(save, 1);
+
+		const_time_select_bin(mask, prfbuf, xbuf, primebytelen, xbuf);
+		save_is_odd = const_time_select(mask, is_odd, save_is_odd);
+		found = const_time_select(mask, -1, found);
+	}
+
+	/*
+	* now we can savely construct PWE
+	*/
+	BN_bin2bn(xbuf, primebytelen, x_candidate);
+	do_equation(session->group, y_sqrd, x_candidate, session->bnctx);
+	if ( !BN_add(exp, session->prime, BN_value_one()) ||
+		 !BN_rshift(exp, exp, 2) ||
+		 !BN_mod_exp_mont_consttime(y1, y_sqrd, exp, session->prime, session->bnctx, NULL) ||
+		 !BN_sub(y2, session->prime, y1) ||
+		 !BN_bn2bin(y1, y1buf) ||
+		 !BN_bn2bin(y2, y2buf)) {
+		DEBUG("unable to compute y");
+		goto fail;
+	}
+	mask = const_time_eq(save_is_odd, BN_is_odd(y1));
+	const_time_select_bin(mask, y1buf, y2buf, primebytelen, ybuf);
+	if (BN_bin2bn(ybuf, primebytelen, y) == NULL ||
+		!EC_POINT_set_affine_coordinates(session->group, session->pwe, x_candidate, y, session->bnctx)) {
+		DEBUG("unable to set point coordinate");
+		goto fail;
 	}
 
 	session->group_num = grp_num;
@@ -278,78 +490,89 @@ int compute_password_element (pwd_session_t *session, uint16_t grp_num,
 	}
 
 	/* cleanliness and order.... */
-	BN_clear_free(cofactor);
 	BN_clear_free(x_candidate);
+	BN_clear_free(y_sqrd);
+	BN_clear_free(qr);
+	BN_clear_free(qnr);
 	BN_clear_free(rnd);
-	talloc_free(prfbuf);
-	HMAC_CTX_free(ctx);
+	BN_clear_free(y1);
+	BN_clear_free(y2);
+	BN_clear_free(y);
+	BN_clear_free(exp);
+
+	if (prfbuf) talloc_free(prfbuf);
+	if (xbuf) talloc_free(xbuf);
+	if (pm1buf) talloc_free(pm1buf);
+	if (y1buf) talloc_free(y1buf);
+	if (y2buf) talloc_free(y2buf);
+	if (ybuf) talloc_free(ybuf);
+
+	EVP_MD_CTX_free(hmac_ctx);
+	EVP_PKEY_free(hmac_pkey);
 
 	return ret;
 }
 
-int compute_scalar_element (pwd_session_t *session, BN_CTX *bnctx) {
+int compute_scalar_element(REQUEST *request, pwd_session_t *session, BN_CTX *bn_ctx)
+{
 	BIGNUM *mask = NULL;
 	int ret = -1;
 
-	if (((session->private_value = BN_new()) == NULL) ||
-	    ((session->my_element = EC_POINT_new(session->group)) == NULL) ||
-	    ((session->my_scalar = BN_new()) == NULL) ||
-	    ((mask = BN_new()) == NULL)) {
-		DEBUG2("server scalar allocation failed");
-		goto fail;
-	}
+	MEM(session->private_value = BN_new());
+	MEM(session->my_element = EC_POINT_new(session->group));
+	MEM(session->my_scalar = BN_new());
+
+	MEM(mask = BN_new());
 
 	if (BN_rand_range(session->private_value, session->order) != 1) {
-		DEBUG2("Unable to get randomness for private_value");
-		goto fail;
+		REDEBUG("Unable to get randomness for private_value");
+		goto error;
 	}
 	if (BN_rand_range(mask, session->order) != 1) {
-		DEBUG2("Unable to get randomness for mask");
-		goto fail;
+		REDEBUG("Unable to get randomness for mask");
+		goto error;
 	}
 	BN_add(session->my_scalar, session->private_value, mask);
-	BN_mod(session->my_scalar, session->my_scalar, session->order, bnctx);
+	BN_mod(session->my_scalar, session->my_scalar, session->order, bn_ctx);
 
-	if (!EC_POINT_mul(session->group, session->my_element, NULL, session->pwe, mask, bnctx)) {
-		DEBUG2("server element allocation failed");
-		goto fail;
+	if (!EC_POINT_mul(session->group, session->my_element, NULL, session->pwe, mask, bn_ctx)) {
+		REDEBUG("Server element allocation failed");
+		goto error;
 	}
 
-	if (!EC_POINT_invert(session->group, session->my_element, bnctx)) {
-		DEBUG2("server element inversion failed");
-		goto fail;
+	if (!EC_POINT_invert(session->group, session->my_element, bn_ctx)) {
+		REDEBUG("Server element inversion failed");
+		goto error;
 	}
 
 	ret = 0;
 
-fail:
+error:
 	BN_clear_free(mask);
 
 	return ret;
 }
 
-int process_peer_commit (pwd_session_t *session, uint8_t *in, size_t in_len, BN_CTX *bnctx)
+int process_peer_commit(REQUEST *request, pwd_session_t *session, uint8_t *in, size_t in_len, BN_CTX *bn_ctx)
 {
-	uint8_t *ptr;
-	size_t data_len;
-	BIGNUM *x = NULL, *y = NULL, *cofactor = NULL;
-	EC_POINT *K = NULL, *point = NULL;
-	int res = 1;
-
-	if (((session->peer_scalar = BN_new()) == NULL) ||
-	    ((session->k = BN_new()) == NULL) ||
-	    ((cofactor = BN_new()) == NULL) ||
-	    ((x = BN_new()) == NULL) ||
-	    ((y = BN_new()) == NULL) ||
-	    ((point = EC_POINT_new(session->group)) == NULL) ||
-	    ((K = EC_POINT_new(session->group)) == NULL) ||
-	    ((session->peer_element = EC_POINT_new(session->group)) == NULL)) {
-		DEBUG2("pwd: failed to allocate room to process peer's commit");
-		goto finish;
-	}
+	uint8_t		*ptr;
+	size_t		data_len;
+	BIGNUM		*x = NULL, *y = NULL, *cofactor = NULL;
+	EC_POINT	*K = NULL, *point = NULL;
+	int		ret = 1;
+
+	MEM(session->peer_scalar = BN_new());
+	MEM(session->k = BN_new());
+	MEM(session->peer_element = EC_POINT_new(session->group));
+	MEM(point = EC_POINT_new(session->group));
+	MEM(K = EC_POINT_new(session->group));
+
+	MEM(cofactor = BN_new());
+	MEM(x = BN_new());
+	MEM(y = BN_new());
 
 	if (!EC_GROUP_get_cofactor(session->group, cofactor, NULL)) {
-		DEBUG2("pwd: unable to get group co-factor");
+		REDEBUG("Unable to get group co-factor");
 		goto finish;
 	}
 
@@ -361,7 +584,7 @@ int process_peer_commit (pwd_session_t *session, uint8_t *in, size_t in_len, BN_
 	 *	Did the peer send enough data?
 	 */
 	if (in_len < (2 * data_len + BN_num_bytes(session->order))) {
-		DEBUG("pwd: Invalid commit packet");
+		REDEBUG("Invalid commit packet");
 		goto finish;
 	}
 
@@ -377,54 +600,54 @@ int process_peer_commit (pwd_session_t *session, uint8_t *in, size_t in_len, BN_
 	if (BN_is_zero(session->peer_scalar) ||
 	    BN_is_one(session->peer_scalar) ||
 	    BN_cmp(session->peer_scalar, session->order) >= 0) {
-		ERROR("Peer's scalar is not within the allowed range");
+		REDEBUG("Peer's scalar is not within the allowed range");
 		goto finish;
 	}
 
-	if (!EC_POINT_set_affine_coordinates_GFp(session->group, session->peer_element, x, y, bnctx)) {
-		DEBUG2("pwd: unable to get coordinates of peer's element");
+	if (!EC_POINT_set_affine_coordinates(session->group, session->peer_element, x, y, bn_ctx)) {
+		REDEBUG("Unable to get coordinates of peer's element");
 		goto finish;
 	}
 
 	/* validate received element */
-	if (!EC_POINT_is_on_curve(session->group, session->peer_element, bnctx) ||
+	if (!EC_POINT_is_on_curve(session->group, session->peer_element, bn_ctx) ||
 	    EC_POINT_is_at_infinity(session->group, session->peer_element)) {
-		ERROR("Peer's element is not a point on the elliptic curve");
+		REDEBUG("Peer's element is not a point on the elliptic curve");
 		goto finish;
 	}
 
 	/* check to ensure peer's element is not in a small sub-group */
 	if (BN_cmp(cofactor, BN_value_one())) {
 		if (!EC_POINT_mul(session->group, point, NULL, session->peer_element, cofactor, NULL)) {
-			DEBUG2("pwd: unable to multiply element by co-factor");
+			REDEBUG("Unable to multiply element by co-factor");
 			goto finish;
 		}
 
 		if (EC_POINT_is_at_infinity(session->group, point)) {
-			DEBUG2("pwd: peer's element is in small sub-group");
+			REDEBUG("Peer's element is in small sub-group");
 			goto finish;
 		}
 	}
 
 	/* detect reflection attacks */
 	if (BN_cmp(session->peer_scalar, session->my_scalar) == 0 ||
-	    EC_POINT_cmp(session->group, session->peer_element, session->my_element, bnctx) == 0) {
-		ERROR("Reflection attack detected");
+	    EC_POINT_cmp(session->group, session->peer_element, session->my_element, bn_ctx) == 0) {
+		REDEBUG("Reflection attack detected");
 		goto finish;
 	}
 
 	/* compute the shared key, k */
-	if ((!EC_POINT_mul(session->group, K, NULL, session->pwe, session->peer_scalar, bnctx)) ||
-	    (!EC_POINT_add(session->group, K, K, session->peer_element, bnctx)) ||
-	    (!EC_POINT_mul(session->group, K, NULL, K, session->private_value, bnctx))) {
-		DEBUG2("pwd: unable to compute shared key, k");
+	if ((!EC_POINT_mul(session->group, K, NULL, session->pwe, session->peer_scalar, bn_ctx)) ||
+	    (!EC_POINT_add(session->group, K, K, session->peer_element, bn_ctx)) ||
+	    (!EC_POINT_mul(session->group, K, NULL, K, session->private_value, bn_ctx))) {
+		REDEBUG("Unable to compute shared key, k");
 		goto finish;
 	}
 
 	/* ensure that the shared key isn't in a small sub-group */
 	if (BN_cmp(cofactor, BN_value_one())) {
 		if (!EC_POINT_mul(session->group, K, NULL, K, cofactor, NULL)) {
-			DEBUG2("pwd: unable to multiply k by co-factor");
+			REDEBUG("Unable to multiply k by co-factor");
 			goto finish;
 		}
 	}
@@ -436,15 +659,15 @@ int process_peer_commit (pwd_session_t *session, uint8_t *in, size_t in_len, BN_
 	 * sure" so let's be safe.
 	 */
 	if (EC_POINT_is_at_infinity(session->group, K)) {
-		DEBUG2("pwd: k is point-at-infinity!");
+		REDEBUG("K is point-at-infinity");
 		goto finish;
 	}
 
-	if (!EC_POINT_get_affine_coordinates_GFp(session->group, K, session->k, NULL, bnctx)) {
-		DEBUG2("pwd: unable to get shared secret from K");
+	if (!EC_POINT_get_affine_coordinates(session->group, K, session->k, NULL, bn_ctx)) {
+		REDEBUG("Unable to get shared secret from K");
 		goto finish;
 	}
-	res = 0;
+	ret = 0;
 
 finish:
 	EC_POINT_clear_free(K);
@@ -453,36 +676,29 @@ finish:
 	BN_clear_free(x);
 	BN_clear_free(y);
 
-	return res;
+	return ret;
 }
 
-int compute_server_confirm (pwd_session_t *session, uint8_t *out, BN_CTX *bnctx)
+int compute_server_confirm(REQUEST *request, pwd_session_t *session, uint8_t *out, BN_CTX *bn_ctx)
 {
-	BIGNUM *x = NULL, *y = NULL;
-	HMAC_CTX *ctx = NULL;
-	uint8_t *cruft = NULL;
-	int offset, req = -1;
-
-	ctx = HMAC_CTX_new();
-	if (ctx == NULL) {
-		DEBUG2("pwd: unable to allocate HMAC context!");
-		goto finish;
-	}
+	BIGNUM		*x = NULL, *y = NULL;
+	HMAC_CTX	*hmac_ctx = NULL;
+	uint8_t		*cruft = NULL;
+	int		offset, req = -1;
 
 	/*
 	 * Each component of the cruft will be at most as big as the prime
 	 */
-	if (((cruft = talloc_zero_array(session, uint8_t, BN_num_bytes(session->prime))) == NULL) ||
-	    ((x = BN_new()) == NULL) || ((y = BN_new()) == NULL)) {
-		DEBUG2("pwd: unable to allocate space to compute confirm!");
-		goto finish;
-	}
+	MEM(cruft = talloc_zero_array(session, uint8_t, BN_num_bytes(session->prime)));
+	MEM(x = BN_new());
+	MEM(y = BN_new());
 
 	/*
 	 * commit is H(k | server_element | server_scalar | peer_element |
 	 *	       peer_scalar | ciphersuite)
 	 */
-	H_Init(ctx);
+	MEM(hmac_ctx = HMAC_CTX_new());
+	HMAC_Init_ex(hmac_ctx, allzero, SHA256_DIGEST_LENGTH, EVP_sha256(), NULL);
 
 	/*
 	 * Zero the memory each time because this is mod prime math and some
@@ -492,24 +708,24 @@ int compute_server_confirm (pwd_session_t *session, uint8_t *out, BN_CTX *bnctx)
 	 */
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(session->k);
 	BN_bn2bin(session->k, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	/*
 	 * next is server element: x, y
 	 */
-	if (!EC_POINT_get_affine_coordinates_GFp(session->group, session->my_element, x, y, bnctx)) {
-		DEBUG2("pwd: unable to get coordinates of server element");
+	if (!EC_POINT_get_affine_coordinates(session->group, session->my_element, x, y, bn_ctx)) {
+		REDEBUG("Unable to get coordinates of server element");
 		goto finish;
 	}
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(x);
 	BN_bn2bin(x, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(y);
 	BN_bn2bin(y, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	/*
 	 * and server scalar
@@ -517,25 +733,25 @@ int compute_server_confirm (pwd_session_t *session, uint8_t *out, BN_CTX *bnctx)
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->order) - BN_num_bytes(session->my_scalar);
 	BN_bn2bin(session->my_scalar, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->order));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->order));
 
 	/*
 	 * next is peer element: x, y
 	 */
-	if (!EC_POINT_get_affine_coordinates_GFp(session->group, session->peer_element, x, y, bnctx)) {
-		DEBUG2("pwd: unable to get coordinates of peer's element");
+	if (!EC_POINT_get_affine_coordinates(session->group, session->peer_element, x, y, bn_ctx)) {
+		REDEBUG("Unable to get coordinates of peer's element");
 		goto finish;
 	}
 
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(x);
 	BN_bn2bin(x, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(y);
 	BN_bn2bin(y, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	/*
 	 * and peer scalar
@@ -543,52 +759,46 @@ int compute_server_confirm (pwd_session_t *session, uint8_t *out, BN_CTX *bnctx)
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->order) - BN_num_bytes(session->peer_scalar);
 	BN_bn2bin(session->peer_scalar, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->order));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->order));
 
 	/*
 	 * finally, ciphersuite
 	 */
-	H_Update(ctx, (uint8_t *)&session->ciphersuite, sizeof(session->ciphersuite));
+	HMAC_Update(hmac_ctx, (uint8_t *)&session->ciphersuite, sizeof(session->ciphersuite));
 
-	H_Final(ctx, out);
+	pwd_hmac_final(hmac_ctx, out);
 
 	req = 0;
+
 finish:
+	HMAC_CTX_free(hmac_ctx);
 	talloc_free(cruft);
 	BN_free(x);
 	BN_free(y);
-	HMAC_CTX_free(ctx);
 
 	return req;
 }
 
-int compute_peer_confirm (pwd_session_t *session, uint8_t *out, BN_CTX *bnctx)
+int compute_peer_confirm(REQUEST *request, pwd_session_t *session, uint8_t *out, BN_CTX *bn_ctx)
 {
-	BIGNUM *x = NULL, *y = NULL;
-	HMAC_CTX *ctx = NULL;
-	uint8_t *cruft = NULL;
-	int offset, req = -1;
-
-	ctx = HMAC_CTX_new();
-	if (ctx == NULL) {
-		DEBUG2("pwd: unable to allocate HMAC context!");
-		goto finish;
-	}
+	BIGNUM		*x = NULL, *y = NULL;
+	HMAC_CTX	*hmac_ctx = NULL;
+	uint8_t		*cruft = NULL;
+	int		offset, req = -1;
 
 	/*
 	 * Each component of the cruft will be at most as big as the prime
 	 */
-	if (((cruft = talloc_zero_array(session, uint8_t, BN_num_bytes(session->prime))) == NULL) ||
-	    ((x = BN_new()) == NULL) || ((y = BN_new()) == NULL)) {
-		DEBUG2("pwd: unable to allocate space to compute confirm!");
-		goto finish;
-	}
+	MEM(cruft = talloc_zero_array(session, uint8_t, BN_num_bytes(session->prime)));
+	MEM(x = BN_new());
+	MEM(y = BN_new());
 
 	/*
 	 * commit is H(k | server_element | server_scalar | peer_element |
 	 *	       peer_scalar | ciphersuite)
 	 */
-	H_Init(ctx);
+	MEM(hmac_ctx = HMAC_CTX_new());
+	HMAC_Init_ex(hmac_ctx, allzero, SHA256_DIGEST_LENGTH, EVP_sha256(), NULL);
 
 	/*
 	 * Zero the memory each time because this is mod prime math and some
@@ -598,25 +808,25 @@ int compute_peer_confirm (pwd_session_t *session, uint8_t *out, BN_CTX *bnctx)
 	 */
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(session->k);
 	BN_bn2bin(session->k, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	/*
 	* then peer element: x, y
 	*/
-	if (!EC_POINT_get_affine_coordinates_GFp(session->group, session->peer_element, x, y, bnctx)) {
-		DEBUG2("pwd: unable to get coordinates of peer's element");
+	if (!EC_POINT_get_affine_coordinates(session->group, session->peer_element, x, y, bn_ctx)) {
+		REDEBUG("Unable to get coordinates of peer's element");
 		goto finish;
 	}
 
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(x);
 	BN_bn2bin(x, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(y);
 	BN_bn2bin(y, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	/*
 	 * and peer scalar
@@ -624,24 +834,24 @@ int compute_peer_confirm (pwd_session_t *session, uint8_t *out, BN_CTX *bnctx)
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->order) - BN_num_bytes(session->peer_scalar);
 	BN_bn2bin(session->peer_scalar, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->order));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->order));
 
 	/*
 	 * then server element: x, y
 	 */
-	if (!EC_POINT_get_affine_coordinates_GFp(session->group, session->my_element, x, y, bnctx)) {
-		DEBUG2("pwd: unable to get coordinates of server element");
+	if (!EC_POINT_get_affine_coordinates(session->group, session->my_element, x, y, bn_ctx)) {
+		REDEBUG("Unable to get coordinates of server element");
 		goto finish;
 	}
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(x);
 	BN_bn2bin(x, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(y);
 	BN_bn2bin(y, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
 	/*
 	 * and server scalar
@@ -649,94 +859,75 @@ int compute_peer_confirm (pwd_session_t *session, uint8_t *out, BN_CTX *bnctx)
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->order) - BN_num_bytes(session->my_scalar);
 	BN_bn2bin(session->my_scalar, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->order));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->order));
 
 	/*
 	 * finally, ciphersuite
 	 */
-	H_Update(ctx, (uint8_t *)&session->ciphersuite, sizeof(session->ciphersuite));
+	HMAC_Update(hmac_ctx, (uint8_t *)&session->ciphersuite, sizeof(session->ciphersuite));
 
-	H_Final(ctx, out);
+	pwd_hmac_final(hmac_ctx, out);
 
 	req = 0;
 finish:
+	HMAC_CTX_free(hmac_ctx);
 	talloc_free(cruft);
 	BN_free(x);
 	BN_free(y);
-	HMAC_CTX_free(ctx);
 
 	return req;
 }
 
-int compute_keys (pwd_session_t *session, uint8_t *peer_confirm, uint8_t *msk, uint8_t *emsk)
+int compute_keys(UNUSED REQUEST *request, pwd_session_t *session, uint8_t *peer_confirm, uint8_t *msk, uint8_t *emsk)
 {
-	HMAC_CTX *ctx = NULL;
-	uint8_t mk[SHA256_DIGEST_LENGTH], *cruft = NULL;
-	uint8_t session_id[SHA256_DIGEST_LENGTH + 1];
-	uint8_t msk_emsk[128];		/* 64 each */
-	int offset, ret = -1;
-
-	ctx = HMAC_CTX_new();
-	if (ctx == NULL) {
-		DEBUG2("pwd: unable to allocate HMAC context!");
-		goto finish;
-	}
+	HMAC_CTX	*hmac_ctx;
+	uint8_t		mk[SHA256_DIGEST_LENGTH], *cruft;
+	uint8_t		session_id[SHA256_DIGEST_LENGTH + 1];
+	uint8_t		msk_emsk[128];		/* 64 each */
+	int	 	offset;
 
-	if ((cruft = talloc_array(session, uint8_t, BN_num_bytes(session->prime))) == NULL) {
-		DEBUG2("pwd: unable to allocate space to compute keys");
-		goto finish;
-	}
+	MEM(cruft = talloc_array(session, uint8_t, BN_num_bytes(session->prime)));
+	MEM(hmac_ctx = HMAC_CTX_new());
 
 	/*
 	 * first compute the session-id = TypeCode | H(ciphersuite | scal_p |
 	 *	scal_s)
 	 */
 	session_id[0] = PW_EAP_PWD;
-	H_Init(ctx);
-	H_Update(ctx, (uint8_t *)&session->ciphersuite, sizeof(session->ciphersuite));
+	HMAC_Init_ex(hmac_ctx, allzero, SHA256_DIGEST_LENGTH, EVP_sha256(), NULL);
+	HMAC_Update(hmac_ctx, (uint8_t *)&session->ciphersuite, sizeof(session->ciphersuite));
 	offset = BN_num_bytes(session->order) - BN_num_bytes(session->peer_scalar);
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	BN_bn2bin(session->peer_scalar, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->order));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->order));
 	offset = BN_num_bytes(session->order) - BN_num_bytes(session->my_scalar);
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	BN_bn2bin(session->my_scalar, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->order));
-	H_Final(ctx, (uint8_t *)&session_id[1]);
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->order));
+	pwd_hmac_final(hmac_ctx, (uint8_t *)&session_id[1]);
 
 	/* then compute MK = H(k | commit-peer | commit-server) */
-	H_Init(ctx);
+	HMAC_Init_ex(hmac_ctx, allzero, SHA256_DIGEST_LENGTH, EVP_sha256(), NULL);
 
 	memset(cruft, 0, BN_num_bytes(session->prime));
 	offset = BN_num_bytes(session->prime) - BN_num_bytes(session->k);
 	BN_bn2bin(session->k, cruft + offset);
-	H_Update(ctx, cruft, BN_num_bytes(session->prime));
+	HMAC_Update(hmac_ctx, cruft, BN_num_bytes(session->prime));
 
-	H_Update(ctx, peer_confirm, SHA256_DIGEST_LENGTH);
+	HMAC_Update(hmac_ctx, peer_confirm, SHA256_DIGEST_LENGTH);
 
-	H_Update(ctx, session->my_confirm, SHA256_DIGEST_LENGTH);
+	HMAC_Update(hmac_ctx, session->my_confirm, SHA256_DIGEST_LENGTH);
 
-	H_Final(ctx, mk);
+	pwd_hmac_final(hmac_ctx, mk);
 
 	/* stretch the mk with the session-id to get MSK | EMSK */
-	if (eap_pwd_kdf(mk, SHA256_DIGEST_LENGTH, (char const *)session_id,
-		        SHA256_DIGEST_LENGTH + 1, msk_emsk,
-			/* it's bits, ((64 + 64) * 8) */
-			1024) != 0) {
-		DEBUG("key derivation function failed");
-		goto finish;
-	}
+	eap_pwd_kdf(mk, SHA256_DIGEST_LENGTH, (char const *)session_id,
+		    SHA256_DIGEST_LENGTH + 1, msk_emsk, 1024);  /* it's bits, ((64 + 64) * 8) */
 
 	memcpy(msk, msk_emsk, 64);
 	memcpy(emsk, msk_emsk + 64, 64);
 
-	ret = 0;
-finish:
+	HMAC_CTX_free(hmac_ctx);
 	talloc_free(cruft);
-	HMAC_CTX_free(ctx);
-	return ret;
+	return 0;
 }
-
-
-
-
diff --git a/src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.h b/src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.h
index ca12778f61..a40a346069 100644
--- a/src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.h
+++ b/src/modules/rlm_eap/types/rlm_eap_pwd/eap_pwd.h
@@ -102,18 +102,22 @@ typedef struct _pwd_session_t {
     EC_POINT *my_element;
     EC_POINT *peer_element;
     uint8_t my_confirm[SHA256_DIGEST_LENGTH];
+    uint8_t prep;
+    uint8_t salt_present;
+    uint8_t salt_len;
+    uint8_t salt[255];
 } pwd_session_t;
 
-int compute_password_element(pwd_session_t *sess, uint16_t grp_num,
+int compute_password_element(REQUEST *request, pwd_session_t *sess, uint16_t grp_num,
 			     char const *password, int password_len,
 			     char const *id_server, int id_server_len,
 			     char const *id_peer, int id_peer_len,
 			     uint32_t *token);
-int compute_scalar_element(pwd_session_t *sess, BN_CTX *bnctx);
-int process_peer_commit (pwd_session_t *sess, uint8_t *in, size_t in_len, BN_CTX *bnctx);
-int compute_server_confirm(pwd_session_t *sess, uint8_t *out, BN_CTX *bnctx);
-int compute_peer_confirm(pwd_session_t *sess, uint8_t *out, BN_CTX *bnctx);
-int compute_keys(pwd_session_t *sess, uint8_t *peer_confirm,
+int compute_scalar_element(REQUEST *request, pwd_session_t *sess, BN_CTX *bnctx);
+int process_peer_commit(REQUEST *request, pwd_session_t *sess, uint8_t *in, size_t in_len, BN_CTX *bnctx);
+int compute_server_confirm(REQUEST *request, pwd_session_t *sess, uint8_t *out, BN_CTX *bnctx);
+int compute_peer_confirm(REQUEST *request, pwd_session_t *sess, uint8_t *out, BN_CTX *bnctx);
+int compute_keys(REQUEST *request, pwd_session_t *sess, uint8_t *peer_confirm,
 		 uint8_t *msk, uint8_t *emsk);
 #ifdef PRINTBUF
 void print_buf(char *str, uint8_t *buf, int len);
diff --git a/src/modules/rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.c b/src/modules/rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.c
index 18ab97f148..4992a2aeef 100644
--- a/src/modules/rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.c
+++ b/src/modules/rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.c
@@ -41,11 +41,93 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 #define MPPE_KEY_LEN    32
 #define MSK_EMSK_LEN    (2*MPPE_KEY_LEN)
 
+/* EAP-PWD can use different preprocessing (prep) modes to mangle the password
+ * before proving to both parties that they both know the same (mangled) password.
+ *
+ * The server advertises a preprocessing mode to the client. Only "none" is
+ * mandatory to implement.
+ *
+ * What is a good selection on the preprocessing mode?
+ *
+ * a) the server uses a hashed password
+ * b) the client uses a hashed password
+ *
+ * a | b | result
+ * --+---+---------------------------------------
+ * n | n | none
+ * n | y | hint needed (cannot know automatically)
+ * y | n | select by hash given
+ * y | y | only works if both have the same hash; select by hash given
+ *
+ * Which hash functions does the server or client need to implement?
+ *
+ * a | b | server                 | client
+ * --+---+------------------------+----------------------
+ * n | n | none                   | none
+ * n | y | as configured          | none
+ * y | n | none                   | as selected by server
+ * y | y | none                   | none
+ *
+ * RFC 5931 defines 3 and RFC 8146 another 8 hash functions to implement.
+ * Can we avoid implementing them all? Only if they are provided as hash by some
+ * other module, e.g. in SQL or statically in password database.
+ *
+ * Therefore we select the preprocessing mode by the type of password given if
+ * in automatic mode:
+ * a) Cleartext-Password or User-Password: None.
+ *    If the client only supports a hash (e.g. on Windows it might only have an
+ *    NT-Password), do not provide a Cleartext-Password attribute but instead
+ *    preprocess the password externally (e.g. hash the Cleartext-Password
+ *    into an NT-Password and drop the Cleartext-Password).
+ * b) NT-Password: rfc2759 (prep=MS).
+ *    The NT-Password Hash is hashed into a HashNTPasswordHash hash.
+ * c) EAP-Pwd-Password-Hash - provides hash as binary
+ *    EAP-Pwd-Password-Salt - (optional) salt to be transmitted to client
+ *                            (RFC 8146)
+ *    EAP-Pwd-Password-Prep - constant to transmit to client in prep field
+ *
+ * Though, there is one issue left. The method needs to be selected in
+ * EAP-PWD-ID/Request, that is the first message from server and thus before
+ * the client sent its peer-id. This is feasable using the EAP-Identity frame
+ * (outer identity); EAP-PWD does transmit its peer-id in plaintext anyway.
+ * So we need a toggle for this, in case anybody needs rlm_eap_pwd to use
+ * only the peer_id (inner identity). This toogle is an integer to also support
+ * setting currently unknown nor not implemented preprocessing methods.
+ *
+ * The toogle is named "prep", is a module configuration item, and accepts the
+ * following values:
+ *   prep | meaning
+ * -------+--------------------------------------------------------------------
+ * -1     | [automatic] discover using method described above from EAP-Identity
+ *        |             as User-Name before EAP-PWD-Id/Request
+ * 0..255 | [static]    Fixed password preprocessing method. Expects virtual
+ *        |             server to provide matching password given EAP-PWD
+ *        |             peer-id as User-Name. The virtual server is provided
+ *        |             with EAP-Pwd-Password-Prep containing the configured
+ *        |             prep value.
+ * else   | reserved/invalid
+ *
+ * Attributes to provide Password/Password-Hash and possibly salt.
+ *   prep | accepted attributes
+ * -------+--------------------------------------------------------------------
+ * -1     | see above for automatic discovery
+ * 0      | Use Cleartext-Password or give cleartext in EAP-Pwd-Password-Hash
+ * 1      | Use NT-Password, Cleartext-Password, User-Password or
+ *        | give hashed NT-Password hash in EAP-Pwd-Password-Hash
+ * 2..255 | Use EAP-Pwd-Password-Hash and possibly EAP-Pwd-Pasword-Salt.
+ *
+ * To be able to pass EAP-Pwd-Password-Hash and EAP-Pwd-Password-Salt als hex
+ * string, they are decoded as hex if module config option unhex=1 (default).
+ * Set it to zero if you provide binary input.
+ */
+
 static CONF_PARSER pwd_module_config[] = {
 	{ "group", FR_CONF_OFFSET(PW_TYPE_INTEGER, eap_pwd_t, group), "19" },
 	{ "fragment_size", FR_CONF_OFFSET(PW_TYPE_INTEGER, eap_pwd_t, fragment_size), "1020" },
 	{ "server_id", FR_CONF_OFFSET(PW_TYPE_STRING, eap_pwd_t, server_id), NULL },
 	{ "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, eap_pwd_t, virtual_server), NULL },
+	{ "prep", FR_CONF_OFFSET(PW_TYPE_SIGNED, eap_pwd_t, prep), "0" },
+	{ "unhex", FR_CONF_OFFSET(PW_TYPE_SIGNED, eap_pwd_t, unhex), "1" },
 	CONF_PARSER_TERMINATOR
 };
 
@@ -65,6 +147,11 @@ static int mod_instantiate (CONF_SECTION *cs, void **instance)
 		return -1;
 	}
 
+	if (inst->prep < -1 || inst->prep > 255) {
+		cf_log_err_cs(cs, "Invalid value for password preparation method: %d", inst->prep);
+		return -1;
+	}
+
 	return 0;
 }
 
@@ -153,18 +240,282 @@ static int send_pwd_request (pwd_session_t *session, EAP_DS *eap_ds)
 	return 1;
 }
 
+static void normify(REQUEST *request, VALUE_PAIR *vp)
+{
+	size_t decoded;
+	size_t expected_len;
+	uint8_t *buffer;
+
+	rad_assert((vp->da->type == PW_TYPE_OCTETS) || (vp->da->type == PW_TYPE_STRING));
+
+	if (vp->vp_length % 2 != 0 || vp->vp_length == 0) return;
+
+	expected_len = vp->vp_length / 2;
+	buffer = talloc_zero_array(request, uint8_t, expected_len);
+	rad_assert(buffer);
+
+	decoded = fr_hex2bin(buffer, expected_len, vp->vp_strvalue, vp->vp_length);
+	if (decoded == expected_len) {
+		RDEBUG2("Normalizing %s from hex encoding, %zu bytes -> %zu bytes",
+			vp->da->name, vp->vp_length, decoded);
+		fr_pair_value_memcpy(vp, buffer, decoded);
+	} else {
+		RDEBUG2("Normalizing %s from hex encoding, %zu bytes -> %zu bytes failed, got %zu bytes",
+			vp->da->name, vp->vp_length, expected_len, decoded);
+	}
+
+	talloc_free(buffer);
+}
+
+static int fetch_and_process_password(pwd_session_t *session, REQUEST *request, eap_pwd_t *inst) {
+	REQUEST *fake;
+	VALUE_PAIR *vp, *pw;
+	const char *pwbuf;
+	int pw_len;
+	uint8_t nthash[MD4_DIGEST_LENGTH];
+	uint8_t nthashash[MD4_DIGEST_LENGTH];
+	int ret = -1;
+	eap_type_t old_eap_type = 0;
+
+	if ((fake = request_alloc_fake(request)) == NULL) {
+		RDEBUG("pwd unable to create fake request!");
+		return ret;
+	}
+	fake->username = fr_pair_afrom_num(fake->packet, PW_USER_NAME, 0);
+	if (!fake->username) {
+		RDEBUG("Failed creating pair for peer id");
+		goto out;
+	}
+	fr_pair_value_bstrncpy(fake->username, session->peer_id, session->peer_id_len);
+	fr_pair_add(&fake->packet->vps, fake->username);
+
+	if (inst->prep >= 0) {
+		vp = fr_pair_afrom_num(fake->packet, PW_EAP_PWD_PASSWORD_PREP, 0);
+		rad_assert(vp != NULL);
+		vp->vp_byte = inst->prep;
+		fr_pair_add(&fake->packet->vps, vp);
+	}
+
+	if ((vp = fr_pair_find_by_num(request->config, PW_VIRTUAL_SERVER, 0, TAG_ANY)) != NULL) {
+		fake->server = vp->vp_strvalue;
+	} else if (inst->virtual_server) {
+		fake->server = inst->virtual_server;
+	} /* else fake->server == request->server */
+
+	if ((vp = fr_pair_find_by_num(request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY)) != NULL) {
+		/* EAP-Type = NAK here if inst->prep == -1.
+		 * But this does not help the virtual server to differentiate
+		 * based on which EAP method was selected, that is to properly
+		 * prepare session-state: for PWD.
+		 * So fake EAP-Type = PWD here for the time of the inner request.
+		 */
+		old_eap_type = vp->vp_integer;
+		vp->vp_integer = PW_EAP_PWD;
+	}
+	RDEBUG("Sending tunneled request");
+	rdebug_pair_list(L_DBG_LVL_1, request, fake->packet->vps, NULL);
+
+	if (fake->server) {
+		RDEBUG("server %s {", fake->server);
+	} else {
+		RDEBUG("server {");
+	}
+
+	/*
+	 *	Call authorization recursively, which will
+	 *	get the password.
+	 */
+	RINDENT();
+	process_authorize(0, fake);
+	REXDENT();
+
+	/*
+	 *	Note that we don't do *anything* with the reply
+	 *	attributes.
+	 */
+	if (fake->server) {
+		RDEBUG("} # server %s", fake->server);
+	} else {
+		RDEBUG("}");
+	}
+
+	RDEBUG("Got tunneled reply code %d", fake->reply->code);
+	rdebug_pair_list(L_DBG_LVL_1, request, fake->reply->vps, NULL);
+
+	if (old_eap_type && (vp = fr_pair_find_by_num(request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY)) != NULL) {
+		vp->vp_integer = old_eap_type;
+	}
+
+	pw = fr_pair_find_by_num(fake->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY);
+	if (!pw) {
+		pw = fr_pair_find_by_num(fake->config, PW_USER_PASSWORD, 0, TAG_ANY);
+	}
+
+	if (pw && (inst->prep < 0 || inst->prep == EAP_PWD_PREP_NONE)) {
+		VERIFY_VP(pw);
+		session->prep = EAP_PWD_PREP_NONE;
+
+		RDEBUG("Use Cleartext-Password or User-Password for %s to do pwd authentication",
+			session->peer_id);
+
+		pwbuf = pw->vp_strvalue;
+		pw_len = pw->vp_length;
+
+		goto success;
+	}
+
+	pw = fr_pair_find_by_num(fake->config, PW_NT_PASSWORD, 0, TAG_ANY);
+
+	if (pw && (inst->prep < 0 || inst->prep == EAP_PWD_PREP_MS)) {
+		VERIFY_VP(pw);
+		session->prep = EAP_PWD_PREP_MS;
+
+		RDEBUG("Use NT-Password for %s to do pwd authentication",
+			session->peer_id);
+
+		if (pw->vp_length != MD4_DIGEST_LENGTH) {
+			RDEBUG("NT-Password invalid length");
+			goto out;
+		}
+
+		fr_md4_calc(nthashash, pw->vp_octets, pw->vp_length);
+		pwbuf = (const char*) nthashash;
+		pw_len = MD4_DIGEST_LENGTH;
+
+		goto success;
+	}
+
+	pw = fr_pair_find_by_num(fake->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY);
+	if (!pw) {
+		pw = fr_pair_find_by_num(fake->config, PW_USER_PASSWORD, 0, TAG_ANY);
+	}
+
+	if (pw && inst->prep == EAP_PWD_PREP_MS) {
+		VERIFY_VP(pw);
+		session->prep = EAP_PWD_PREP_NONE;
+
+		RDEBUG("Use Cleartext-Password or User-Password as NT-Password for %s to do pwd authentication",
+			session->peer_id);
+
+		// compute NT-Hash from Cleartext-Password
+		ssize_t len;
+		uint8_t ucs2_password[512];
+		len = fr_utf8_to_ucs2(ucs2_password, sizeof(ucs2_password), pw->vp_strvalue, pw->vp_length);
+		if (len < 0) {
+			ERROR("rlm_eap_pwd: Error converting password to UCS2");
+			goto out;
+		}
+		fr_md4_calc(nthash, ucs2_password, len);
+
+		fr_md4_calc(nthashash, nthash, MD4_DIGEST_LENGTH);
+		pwbuf = (const char*) nthashash;
+		pw_len = MD4_DIGEST_LENGTH;
+
+		goto success;
+	}
+
+	vp = fr_pair_find_by_num(fake->config, PW_EAP_PWD_PASSWORD_PREP, 0, TAG_ANY);
+	if (vp) {
+		VERIFY_VP(vp);
+	}
+	if (vp && inst->prep < 0) {
+		RDEBUG("Use EAP-Pwd-Password-Prep %u for %s to do pwd authentication",
+			vp->vp_byte, session->peer_id);
+		session->prep = vp->vp_byte;
+	} else if (vp && inst->prep != vp->vp_byte) {
+		RDEBUG2("Mismatch of configured password preparation method and provided EAP-Pwd-Password-Prep attribute type for %s",
+			session->peer_id);
+		goto out;
+	} else if (inst->prep < 0) {
+		RDEBUG2("Missing EAP-Pwd-Password-Prep for %s",
+			session->peer_id);
+		goto out;
+	}
+
+	pw = fr_pair_find_by_num(fake->config, PW_EAP_PWD_PASSWORD_SALT, 0, TAG_ANY);
+	if (pw) {
+		VERIFY_VP(pw);
+
+		RDEBUG("Use EAP-Pwd-Password-Salt for %s to do pwd authentication",
+			session->peer_id);
+
+		if (inst->unhex) normify(request, pw);
+
+		if (pw->vp_length > 255) {
+			/* salt len is 1 byte */
+			RDEBUG("EAP-Pwd-Password-Salt too long (more than 255 octets)");
+			goto out;
+		}
+		rad_assert(pw->vp_length <= sizeof(session->salt));
+
+		session->salt_present = 1;
+		session->salt_len = pw->vp_length;
+		memcpy(session->salt, pw->vp_octets, pw->vp_length);
+	}
+
+	pw = fr_pair_find_by_num(fake->config, PW_EAP_PWD_PASSWORD_HASH, 0, TAG_ANY);
+	if (pw) {
+		VERIFY_VP(pw);
+
+		RDEBUG("Use EAP-Pwd-Password-Hash for %s to do pwd authentication",
+			session->peer_id);
+
+		if (inst->unhex) normify(request, pw);
+
+		pwbuf = (const char*) pw->vp_octets;
+		pw_len = pw->vp_length;
+
+		goto success;
+	}
+
+	RDEBUG2("Mismatch of password preparation method and provided password attribute type for %s",
+		session->peer_id);
+	goto out;
+
+success:
+	if (RDEBUG_ENABLED4) {
+		char outbuf[1024];
+		char *p = outbuf;
+		for (int i = 0; i < pw_len && p < outbuf + sizeof(outbuf) - 3; i++) {
+			p += sprintf(p, "%02hhX", pwbuf[i]);
+		}
+		RDEBUG4("hex pw data: %s (%d)", outbuf, pw_len);
+	}
+
+	if (compute_password_element(request, session, session->group_num,
+				     pwbuf, pw_len,
+				     inst->server_id, strlen(inst->server_id),
+				     session->peer_id, strlen(session->peer_id),
+				     &session->token)) {
+		RDEBUG("failed to obtain password element");
+		goto out;
+	}
+
+	ret = 0;
+out:
+	talloc_free(fake);
+	return ret;
+}
+
 static int mod_session_init (void *instance, eap_handler_t *handler)
 {
 	pwd_session_t *session;
 	eap_pwd_t *inst = (eap_pwd_t *)instance;
 	VALUE_PAIR *vp;
 	pwd_id_packet_t *packet;
+	REQUEST *request;
 
 	if (!inst || !handler) {
 		ERROR("rlm_eap_pwd: Initiate, NULL data provided");
 		return 0;
 	}
 
+	request = handler->request;
+	if (!request) {
+		ERROR("rlm_eap_pwd: NULL request provided");
+		return 0;
+	}
+
 	/*
 	* make sure the server's been configured properly
 	*/
@@ -232,6 +583,30 @@ static int mod_session_init (void *instance, eap_handler_t *handler)
 	session->out_pos = 0;
 	handler->opaque = session;
 
+	session->token = fr_rand();
+	if (inst->prep < 0) {
+		RDEBUG2("using outer identity %s to configure EAP-PWD", handler->identity);
+		session->peer_id_len = strlen(handler->identity);
+		if (session->peer_id_len >= sizeof(session->peer_id)) {
+			RDEBUG("identity is malformed");
+			return 0;
+		}
+		memcpy(session->peer_id, handler->identity, session->peer_id_len);
+		session->peer_id[session->peer_id_len] = '\0';
+
+		/*
+		 * make fake request to get the password for the usable ID
+		 * in order to identity prep
+		 */
+		if (fetch_and_process_password(session, handler->request, inst) < 0) {
+			RDEBUG("failed to find password for %s to do pwd authentication (init)",
+				session->peer_id);
+			return 0;
+		}
+	} else {
+		session->prep = inst->prep;
+	}
+
 	/*
 	 * construct an EAP-pwd-ID/Request
 	 */
@@ -244,9 +619,8 @@ static int mod_session_init (void *instance, eap_handler_t *handler)
 	packet->group_num = htons(session->group_num);
 	packet->random_function = EAP_PWD_DEF_RAND_FUN;
 	packet->prf = EAP_PWD_DEF_PRF;
-	session->token = fr_rand();
 	memcpy(packet->token, (char *)&session->token, 4);
-	packet->prep = EAP_PWD_PREP_NONE;
+	packet->prep = session->prep;
 	memcpy(packet->identity, inst->server_id, session->out_len - sizeof(pwd_id_packet_t) );
 
 	handler->stage = PROCESS;
@@ -259,16 +633,16 @@ static int mod_process(void *arg, eap_handler_t *handler)
 	pwd_session_t *session;
 	pwd_hdr *hdr;
 	pwd_id_packet_t *packet;
+	REQUEST *request;
 	eap_packet_t *response;
-	REQUEST *request, *fake;
-	VALUE_PAIR *pw, *vp;
 	EAP_DS *eap_ds;
-	size_t in_len;
+	size_t in_len, peer_id_len;
 	int ret = 0;
 	eap_pwd_t *inst = (eap_pwd_t *)arg;
 	uint16_t offset;
 	uint8_t exch, *in, *ptr, msk[MSK_EMSK_LEN], emsk[MSK_EMSK_LEN];
 	uint8_t peer_confirm[SHA256_DIGEST_LENGTH];
+	char *peer_id;
 
 	if (((eap_ds = handler->eap_ds) == NULL) || !inst) return 0;
 
@@ -389,7 +763,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 
 		if ((packet->prf != EAP_PWD_DEF_PRF) ||
 		    (packet->random_function != EAP_PWD_DEF_RAND_FUN) ||
-		    (packet->prep != EAP_PWD_PREP_NONE) ||
+		    (packet->prep != session->prep) ||
 		    (CRYPTO_memcmp(packet->token, &session->token, 4)) ||
 		    (packet->group_num != ntohs(session->group_num))) {
 			RDEBUG2("pwd id response is invalid");
@@ -405,89 +779,46 @@ static int mod_process(void *arg, eap_handler_t *handler)
 		ptr += sizeof(uint8_t);
 		*ptr = EAP_PWD_DEF_PRF;
 
-		session->peer_id_len = in_len - sizeof(pwd_id_packet_t);
-		if (session->peer_id_len >= sizeof(session->peer_id)) {
+		peer_id_len = in_len - sizeof(pwd_id_packet_t);
+		if (peer_id_len >= sizeof(session->peer_id)) {
 			RDEBUG2("pwd id response is malformed");
 			return 0;
 		}
+		peer_id = packet->identity;
 
-		memcpy(session->peer_id, packet->identity, session->peer_id_len);
-		session->peer_id[session->peer_id_len] = '\0';
-
-		/*
-		 * make fake request to get the password for the usable ID
-		 */
-		if ((fake = request_alloc_fake(handler->request)) == NULL) {
-			RDEBUG("pwd unable to create fake request!");
-			return 0;
-		}
-		fake->username = fr_pair_afrom_num(fake->packet, PW_USER_NAME, 0);
-		if (!fake->username) {
-			RDEBUG("Failed creating pair for peer id");
-			talloc_free(fake);
-			return 0;
-		}
-		fr_pair_value_bstrncpy(fake->username, session->peer_id, session->peer_id_len);
-		fr_pair_add(&fake->packet->vps, fake->username);
-
-		if ((vp = fr_pair_find_by_num(request->config, PW_VIRTUAL_SERVER, 0, TAG_ANY)) != NULL) {
-			fake->server = vp->vp_strvalue;
-		} else if (inst->virtual_server) {
-			fake->server = inst->virtual_server;
-		} /* else fake->server == request->server */
-
-		RDEBUG("Sending tunneled request");
-		rdebug_pair_list(L_DBG_LVL_1, request, fake->packet->vps, NULL);
-
-		if (fake->server) {
-			RDEBUG("server %s {", fake->server);
-		} else {
-			RDEBUG("server {");
-		}
+		if (inst->prep >= 0) {
+			/*
+			 * make fake request to get the password for the usable ID
+			 */
 
-		/*
-		 *	Call authorization recursively, which will
-		 *	get the password.
-		 */
-		RINDENT();
-		process_authorize(0, fake);
-		REXDENT();
+			session->peer_id_len = peer_id_len;
+			memcpy(session->peer_id, peer_id, peer_id_len);
+			session->peer_id[peer_id_len] = '\0';
 
-		/*
-		 *	Note that we don't do *anything* with the reply
-		 *	attributes.
-		 */
-		if (fake->server) {
-			RDEBUG("} # server %s", fake->server);
+			if (fetch_and_process_password(session, request, inst) < 0) {
+				RDEBUG2("failed to find password for %s to do pwd authentication",
+				session->peer_id);
+				return 0;
+			}
 		} else {
-			RDEBUG("}");
+			/* verify inner identity == outer identity */
+			if (session->peer_id_len != peer_id_len ||
+			    memcmp(session->peer_id, peer_id, peer_id_len) != 0) {
+				char buf[sizeof(session->peer_id)];
+				memcpy(buf, peer_id, peer_id_len);
+				buf[peer_id_len] = '\0';
+
+				RDEBUG2("inner identity(peer_id) %s does not match outer identity %s",
+				buf, session->peer_id);
+				return 0;
+			}
+			RDEBUG2("inner identity matched for %s", session->peer_id);
 		}
 
-		RDEBUG("Got tunneled reply code %d", fake->reply->code);
-		rdebug_pair_list(L_DBG_LVL_1, request, fake->reply->vps, NULL);
-
-		if ((pw = fr_pair_find_by_num(fake->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY)) == NULL) {
-			DEBUG2("failed to find password for %s to do pwd authentication",
-			session->peer_id);
-			talloc_free(fake);
-			return 0;
-		}
-
-		if (compute_password_element(session, session->group_num,
-			     		     pw->data.strvalue, strlen(pw->data.strvalue),
-					     inst->server_id, strlen(inst->server_id),
-					     session->peer_id, strlen(session->peer_id),
-					     &session->token)) {
-			DEBUG2("failed to obtain password element");
-			talloc_free(fake);
-			return 0;
-		}
-		TALLOC_FREE(fake);
-
 		/*
 		 * compute our scalar and element
 		 */
-		if (compute_scalar_element(session, session->bnctx)) {
+		if (compute_scalar_element(request, session, session->bnctx)) {
 			DEBUG2("failed to compute server's scalar and element");
 			return 0;
 		}
@@ -498,7 +829,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 		/*
 		 * element is a point, get both coordinates: x and y
 		 */
-		if (!EC_POINT_get_affine_coordinates_GFp(session->group, session->my_element, x, y,
+		if (!EC_POINT_get_affine_coordinates(session->group, session->my_element, x, y,
 							 session->bnctx)) {
 			DEBUG2("server point assignment failed");
 			BN_clear_free(x);
@@ -510,12 +841,23 @@ static int mod_process(void *arg, eap_handler_t *handler)
 		 * construct request
 		 */
 		session->out_len = BN_num_bytes(session->order) + (2 * BN_num_bytes(session->prime));
+		if (session->salt_present)
+			session->out_len += 1 + session->salt_len;
+
 		if ((session->out = talloc_array(session, uint8_t, session->out_len)) == NULL) {
 			return 0;
 		}
 		memset(session->out, 0, session->out_len);
 
 		ptr = session->out;
+		if (session->salt_present) {
+			*ptr = session->salt_len;
+			ptr++;
+
+			memcpy(ptr, session->salt, session->salt_len);
+			ptr += session->salt_len;
+		}
+
 		offset = BN_num_bytes(session->prime) - BN_num_bytes(x);
 		BN_bn2bin(x, ptr + offset);
 		BN_clear_free(x);
@@ -534,7 +876,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 	}
 		break;
 
-		case PWD_STATE_COMMIT:
+	case PWD_STATE_COMMIT:
 		if (EAP_PWD_GET_EXCHANGE(hdr) != EAP_PWD_EXCH_COMMIT) {
 			RDEBUG2("pwd exchange is incorrect: not commit!");
 			return 0;
@@ -543,7 +885,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 		/*
 		 * process the peer's commit and generate the shared key, k
 		 */
-		if (process_peer_commit(session, in, in_len, session->bnctx)) {
+		if (process_peer_commit(request, session, in, in_len, session->bnctx)) {
 			RDEBUG2("failed to process peer's commit");
 			return 0;
 		}
@@ -551,7 +893,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 		/*
 		 * compute our confirm blob
 		 */
-		if (compute_server_confirm(session, session->my_confirm, session->bnctx)) {
+		if (compute_server_confirm(request, session, session->my_confirm, session->bnctx)) {
 			ERROR("rlm_eap_pwd: failed to compute confirm!");
 			return 0;
 		}
@@ -582,7 +924,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 			RDEBUG2("pwd exchange is incorrect: not commit!");
 			return 0;
 		}
-		if (compute_peer_confirm(session, peer_confirm, session->bnctx)) {
+		if (compute_peer_confirm(request, session, peer_confirm, session->bnctx)) {
 			RDEBUG2("pwd exchange cannot compute peer's confirm");
 			return 0;
 		}
@@ -590,7 +932,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 			RDEBUG2("pwd exchange fails: peer confirm is incorrect!");
 			return 0;
 		}
-		if (compute_keys(session, peer_confirm, msk, emsk)) {
+		if (compute_keys(request, session, peer_confirm, msk, emsk)) {
 			RDEBUG2("pwd exchange cannot generate (E)MSK!");
 			return 0;
 		}
diff --git a/src/modules/rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.h b/src/modules/rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.h
index 2264566bb6..966646c360 100644
--- a/src/modules/rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.h
+++ b/src/modules/rlm_eap/types/rlm_eap_pwd/rlm_eap_pwd.h
@@ -44,6 +44,8 @@ typedef struct _eap_pwd_t {
     uint32_t	fragment_size;
     char const	*server_id;
     char const	*virtual_server;
+    int32_t	prep;
+    int32_t	unhex;
 } eap_pwd_t;
 
 #endif  /* _RLM_EAP_PWD_H */
diff --git a/src/modules/rlm_eap/types/rlm_eap_tls/rlm_eap_tls.c b/src/modules/rlm_eap/types/rlm_eap_tls/rlm_eap_tls.c
index 4d41cd42e6..d327c575fc 100644
--- a/src/modules/rlm_eap/types/rlm_eap_tls/rlm_eap_tls.c
+++ b/src/modules/rlm_eap/types/rlm_eap_tls/rlm_eap_tls.c
@@ -43,6 +43,7 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 static CONF_PARSER module_config[] = {
 	{ "tls", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_eap_tls_t, tls_conf_name), NULL },
 	{ "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_eap_tls_t, virtual_server), NULL },
+	{ "configurable_client_cert", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_eap_tls_t, configurable_client_cert), NULL },
 	CONF_PARSER_TERMINATOR
 };
 
@@ -71,6 +72,19 @@ static int mod_instantiate(CONF_SECTION *cs, void **instance)
 		return -1;
 	}
 
+#ifdef TLS1_3_VERSION
+	if ((inst->tls_conf->max_version == TLS1_3_VERSION) ||
+	    (inst->tls_conf->min_version == TLS1_3_VERSION)) {
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+		WARN("!! Most supplicants do not support EAP-TLS with TLS 1.3");
+		WARN("!! Please set tls_max_version = \"1.2\"");
+		WARN("!! FreeRADIUS only supports TLS 1.3 for special builds of wpa_supplicant and Windows");
+		WARN("!! This limitation is likely to change in late 2021.");
+		WARN("!! If you are using this version of FreeRADIUS after 2021, you will probably need to upgrade");
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+	}
+#endif
+
 	return 0;
 }
 
@@ -84,25 +98,38 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 	tls_session_t	*ssn;
 	rlm_eap_tls_t	*inst;
 	REQUEST		*request = handler->request;
+	bool		require_client_cert = true;
 
 	inst = type_arg;
 
 	handler->tls = true;
 
 	/*
-	 *	EAP-TLS always requires a client certificate.
+	 *	Respect EAP-TLS-Require-Client-Cert, but only if
+	 *	enabled in the module configuration.
+	 *
+	 *	We can't change behavior of existing systems, so this
+	 *	change has to be enabled via a new configuration
+	 *	option.
 	 */
-	ssn = eaptls_session(handler, inst->tls_conf, true);
+	if (inst->configurable_client_cert) {
+		VALUE_PAIR *vp;
+
+		vp = fr_pair_find_by_num(handler->request->config, PW_EAP_TLS_REQUIRE_CLIENT_CERT, 0, TAG_ANY);
+		if (vp && !vp->vp_integer) require_client_cert = false;
+	}
+
+	/*
+	 *	EAP-TLS always requires a client certificate, and
+	 *	allows for TLS 1.3 if permitted.
+	 */
+	ssn = eaptls_session(handler, inst->tls_conf, require_client_cert, true);
 	if (!ssn) {
 		return 0;
 	}
 
 	handler->opaque = ((void *)ssn);
-
-	/*
-	 *	Set up type-specific information.
-	 */
-	ssn->prf_label = "client EAP encryption";
+	ssn->quick_session_tickets = true; /* send as soon as we've seen the client cert */
 
 	/*
 	 *	TLS session initialization is over.  Now handle TLS
@@ -112,7 +139,7 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 	if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) {
 		REDEBUG("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	} else {
-		RDEBUG2("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		RDEBUG3("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	}
 	if (status == 0) return 0;
 
@@ -141,7 +168,7 @@ static int CC_HINT(nonnull) mod_process(void *type_arg, eap_handler_t *handler)
 	if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) {
 		REDEBUG("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	} else {
-		RDEBUG2("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		RDEBUG3("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	}
 
 
@@ -195,6 +222,13 @@ static int CC_HINT(nonnull) mod_process(void *type_arg, eap_handler_t *handler)
 			/* success */
 		}
 
+		/*
+		 *	Set the label to a fixed string.  For TLS 1.3,
+		 *	the label is the same for all TLS-based EAP
+		 *	methods.
+		 */
+		tls_session->label = "client EAP encryption";
+
 		/*
 		 *	Success: Automatically return MPPE keys.
 		 */
diff --git a/src/modules/rlm_eap/types/rlm_eap_tls/rlm_eap_tls.h b/src/modules/rlm_eap/types/rlm_eap_tls/rlm_eap_tls.h
index cebcb92f57..550cbbdce3 100644
--- a/src/modules/rlm_eap/types/rlm_eap_tls/rlm_eap_tls.h
+++ b/src/modules/rlm_eap/types/rlm_eap_tls/rlm_eap_tls.h
@@ -42,6 +42,11 @@ typedef struct rlm_eap_tls_t {
 	 *	Virtual server for checking certificates
 	 */
 	char const *virtual_server;
+
+	/*
+	 * 	Configurable EAP-TLS-Require-Client-Cert
+	 */
+	bool configurable_client_cert;
 } rlm_eap_tls_t;
 
 #endif /* _RLM_EAP_TLS_H */
diff --git a/src/modules/rlm_eap/types/rlm_eap_ttls/rlm_eap_ttls.c b/src/modules/rlm_eap/types/rlm_eap_ttls/rlm_eap_ttls.c
index a3c575bceb..4e53c92244 100644
--- a/src/modules/rlm_eap/types/rlm_eap_ttls/rlm_eap_ttls.c
+++ b/src/modules/rlm_eap/types/rlm_eap_ttls/rlm_eap_ttls.c
@@ -181,7 +181,10 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 		client_cert = inst->req_client_cert;
 	}
 
-	ssn = eaptls_session(handler, inst->tls_conf, client_cert);
+	/*
+	 *	Allow TLS 1.3, it works.
+	 */
+	ssn = eaptls_session(handler, inst->tls_conf, client_cert, true);
 	if (!ssn) {
 		return 0;
 	}
@@ -189,9 +192,13 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 	handler->opaque = ((void *)ssn);
 
 	/*
-	 *	Set up type-specific information.
+	 *	Set the label to a fixed string.  For TLS 1.3, the
+	 *	label is the same for all TLS-based EAP methods.  If
+	 *	the client is using TLS 1.3, then eaptls_success()
+	 *	will over-ride this label with the correct label for
+	 *	TLS 1.3.
 	 */
-	ssn->prf_label = "ttls keying material";
+	ssn->label = "ttls keying material";
 
 	/*
 	 *	TLS session initialization is over.  Now handle TLS
@@ -201,7 +208,7 @@ static int mod_session_init(void *type_arg, eap_handler_t *handler)
 	if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) {
 		REDEBUG("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	} else {
-		RDEBUG2("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		RDEBUG3("[eaptls start] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	}
 	if (status == 0) return 0;
 
@@ -238,7 +245,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 	if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) {
 		REDEBUG("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	} else {
-		RDEBUG2("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
+		RDEBUG3("[eaptls process] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>"));
 	}
 
 	/*
@@ -342,8 +349,7 @@ static int mod_process(void *arg, eap_handler_t *handler)
 		 *	Success: Automatically return MPPE keys.
 		 */
 	case PW_CODE_ACCESS_ACCEPT:
-		ret = eaptls_success(handler, 0);
-		goto done;
+		goto do_keys;
 
 		/*
 		 *	No response packet, MUST be proxying it.
diff --git a/src/modules/rlm_eap/types/rlm_eap_ttls/ttls.c b/src/modules/rlm_eap/types/rlm_eap_ttls/ttls.c
index 627d722ed7..cbe423951a 100644
--- a/src/modules/rlm_eap/types/rlm_eap_ttls/ttls.c
+++ b/src/modules/rlm_eap/types/rlm_eap_ttls/ttls.c
@@ -145,9 +145,10 @@ static VALUE_PAIR *diameter2vp(REQUEST *request, REQUEST *fake, SSL *ssl,
 	size_t		size;
 	size_t		data_left = data_len;
 	VALUE_PAIR	*first = NULL;
-	VALUE_PAIR	*vp;
+	VALUE_PAIR	*vp = NULL;
 	RADIUS_PACKET	*packet = fake->packet; /* FIXME: api issues */
 	vp_cursor_t	out;
+	DICT_ATTR const *da;
 
 	fr_cursor_init(&out, &first);
 
@@ -258,13 +259,13 @@ static VALUE_PAIR *diameter2vp(REQUEST *request, REQUEST *fake, SSL *ssl,
 			if (decoded < 0) {
 				REDEBUG2("diameter2vp failed decoding attr: %s",
 					fr_strerror());
-				goto do_octets;
+				goto raw;
 			}
 
 			if ((size_t) decoded != size + 2) {
 				REDEBUG2("diameter2vp failed to entirely decode VSA");
 				fr_pair_list_free(&vp);
-				goto do_octets;
+				goto raw;
 			}
 
 			fr_cursor_merge(&out, vp);
@@ -275,8 +276,10 @@ static VALUE_PAIR *diameter2vp(REQUEST *request, REQUEST *fake, SSL *ssl,
 		/*
 		 *	Create it.  If this fails, it's because we're OOM.
 		 */
-	do_octets:
-		vp = fr_pair_afrom_num(packet, attr, vendor);
+		da = dict_attrbyvalue(attr, vendor);
+		if (!da) goto raw;
+
+		vp = fr_pair_afrom_da(packet, da);
 		if (!vp) {
 			RDEBUG2("Failure in creating VP");
 			fr_pair_list_free(&first);
@@ -293,8 +296,6 @@ static VALUE_PAIR *diameter2vp(REQUEST *request, REQUEST *fake, SSL *ssl,
 		case PW_TYPE_INTEGER:
 		case PW_TYPE_DATE:
 			if (size != vp->vp_length) {
-				DICT_ATTR const *da;
-
 				/*
 				 *	Bad format.  Create a "raw"
 				 *	attribute.
@@ -405,7 +406,7 @@ static VALUE_PAIR *diameter2vp(REQUEST *request, REQUEST *fake, SSL *ssl,
 		 */
 		if (((vp->da->vendor == 0) && (vp->da->attr == PW_CHAP_CHALLENGE)) ||
 		    ((vp->da->vendor == VENDORPEC_MICROSOFT) && (vp->da->attr == PW_MSCHAP_CHALLENGE))) {
-			uint8_t	challenge[16];
+			uint8_t	challenge[17];
 
 			if ((vp->vp_length < 8) ||
 			    (vp->vp_length > 16)) {
@@ -415,8 +416,11 @@ static VALUE_PAIR *diameter2vp(REQUEST *request, REQUEST *fake, SSL *ssl,
 				return NULL;
 			}
 
-			eapttls_gen_challenge(ssl, challenge,
-					      sizeof(challenge));
+			/*
+			 *	TLSv1.3 exports a different key depending on the length
+			 *	requested so ask for *exactly* what the spec requires
+			 */
+			eapttls_gen_challenge(ssl, challenge, vp->vp_length + 1);
 
 			if (memcmp(challenge, vp->vp_octets,
 				   vp->vp_length) != 0) {
@@ -644,6 +648,7 @@ static rlm_rcode_t CC_HINT(nonnull) process_reply(eap_handler_t *handler, tls_se
 	 */
 	switch (reply->code) {
 	case PW_CODE_ACCESS_ACCEPT:
+		tls_session->authentication_success = true;
 		RDEBUG("Got tunneled Access-Accept");
 
 		rcode = RLM_MODULE_OK;
diff --git a/src/modules/rlm_exec/rlm_exec.c b/src/modules/rlm_exec/rlm_exec.c
index e7dbfa685e..f7e23628e6 100644
--- a/src/modules/rlm_exec/rlm_exec.c
+++ b/src/modules/rlm_exec/rlm_exec.c
@@ -353,7 +353,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_exec_dispatch(void *instance, REQUEST *r
 	 *	If we're not waiting, then there are no output pairs.
 	 */
 	if (inst->output) {
-		fr_pair_list_move(ctx, output_pairs, &answer);
+		fr_pair_list_move(ctx, output_pairs, &answer, T_OP_ADD);
 	}
 	fr_pair_list_free(&answer);
 
@@ -399,7 +399,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 	/*
 	 *	Always add the value-pairs to the reply.
 	 */
-	fr_pair_list_move(request->reply, &request->reply->vps, &tmp);
+	fr_pair_list_move(request->reply, &request->reply->vps, &tmp, T_OP_ADD);
 	fr_pair_list_free(&tmp);
 
 	finish:
diff --git a/src/modules/rlm_expr/rlm_expr.c b/src/modules/rlm_expr/rlm_expr.c
index c449d7776b..f835800376 100644
--- a/src/modules/rlm_expr/rlm_expr.c
+++ b/src/modules/rlm_expr/rlm_expr.c
@@ -970,6 +970,49 @@ static ssize_t md5_xlat(UNUSED void *instance, REQUEST *request,
 	return strlen(out);
 }
 
+/** Calculate the MD4 hash of a string or attribute.
+ *
+ * Example: "%{md4:foo}" == "0ac6700c491d70fb8650940b1ca1e4b2"
+ */
+static ssize_t md4_xlat(UNUSED void *instance, REQUEST *request,
+			char const *fmt, char *out, size_t outlen)
+{
+	uint8_t digest[16];
+	ssize_t i, len, inlen;
+	uint8_t const *p;
+	FR_MD4_CTX ctx;
+
+	/*
+	 *	We need room for at least one octet of output.
+	 */
+	if (outlen < 3) {
+		*out = '\0';
+		return 0;
+	}
+
+	inlen = xlat_fmt_to_ref(&p, request, fmt);
+	if (inlen < 0) {
+		return -1;
+	}
+
+	fr_md4_init(&ctx);
+	fr_md4_update(&ctx, p, inlen);
+	fr_md4_final(digest, &ctx);
+
+	/*
+	 *	Each digest octet takes two hex digits, plus one for
+	 *	the terminating NUL.
+	 */
+	len = (outlen / 2) - 1;
+	if (len > 16) len = 16;
+
+	for (i = 0; i < len; i++) {
+		snprintf(out + i * 2, 3, "%02x", digest[i]);
+	}
+
+	return strlen(out);
+}
+
 /** Calculate the SHA1 hash of a string or attribute.
  *
  * Example: "%{sha1:foo}" == "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
@@ -1570,6 +1613,76 @@ static ssize_t next_time_xlat(UNUSED void *instance, REQUEST *request,
 	return snprintf(out, outlen, "%" PRIu64, (uint64_t)(mktime(local) - now));
 }
 
+/** Calculate number of seconds until the previous n hour(s), day(s), week(s), year(s).
+ *
+ * For example, if it were 16:18 %{lasttime:1h} would expand to -2520.
+ */
+static ssize_t last_time_xlat(UNUSED void *instance, REQUEST *request,
+			      char const *fmt, char *out, size_t outlen)
+{
+	long		num;
+
+	char const 	*p;
+	char 		*q;
+	time_t		now;
+	struct tm	*local, local_buff;
+
+	now = time(NULL);
+	local = localtime_r(&now, &local_buff);
+
+	p = fmt;
+
+	num = strtoul(p, &q, 10);
+	if (!q || *q == '\0') {
+		REDEBUG("nexttime: <int> must be followed by period specifier (h|d|w|m|y)");
+		return -1;
+	}
+
+	if (p == q) {
+		num = 1;
+	} else {
+		p += q - p;
+	}
+
+	local->tm_sec = 0;
+	local->tm_min = 0;
+
+	switch (*p) {
+	case 'h':
+		local->tm_hour -= num;
+		break;
+
+	case 'd':
+		local->tm_hour = 0;
+		local->tm_mday -= num;
+		break;
+
+	case 'w':
+		local->tm_hour = 0;
+		local->tm_mday -= (7 - local->tm_wday) + (7 * (num-1));
+		break;
+
+	case 'm':
+		local->tm_hour = 0;
+		local->tm_mday = 1;
+		local->tm_mon -= num;
+		break;
+
+	case 'y':
+		local->tm_hour = 0;
+		local->tm_mday = 1;
+		local->tm_mon = 0;
+		local->tm_year -= num;
+		break;
+
+	default:
+		REDEBUG("lasttime: Invalid period specifier '%c', must be h|d|w|m|y", *p);
+		return -1;
+	}
+
+	return snprintf(out, outlen, "%" PRIu64, (uint64_t)(now - mktime(local)));
+}
+
 
 /*
  *	Parse the 3 arguments to lpad / rpad.
@@ -1761,6 +1874,7 @@ static int mod_bootstrap(CONF_SECTION *conf, void *instance)
 	xlat_register("unescape", unescape_xlat, NULL, inst);
 	xlat_register("tolower", tolower_xlat, NULL, inst);
 	xlat_register("toupper", toupper_xlat, NULL, inst);
+	xlat_register("md4", md4_xlat, NULL, inst);
 	xlat_register("md5", md5_xlat, NULL, inst);
 	xlat_register("sha1", sha1_xlat, NULL, inst);
 #ifdef HAVE_OPENSSL_EVP_H
@@ -1778,6 +1892,7 @@ static int mod_bootstrap(CONF_SECTION *conf, void *instance)
 	xlat_register("explode", explode_xlat, NULL, inst);
 
 	xlat_register("nexttime", next_time_xlat, NULL, inst);
+	xlat_register("lasttime", last_time_xlat, NULL, inst);
 	xlat_register("lpad", lpad_xlat, NULL, inst);
 	xlat_register("rpad", rpad_xlat, NULL, inst);
 
diff --git a/src/modules/rlm_files/rlm_files.c b/src/modules/rlm_files/rlm_files.c
index c825a9230b..9e77cd7ff1 100644
--- a/src/modules/rlm_files/rlm_files.c
+++ b/src/modules/rlm_files/rlm_files.c
@@ -422,7 +422,7 @@ static rlm_rcode_t file_common(rlm_files_t *inst, REQUEST *request, char const *
 			/* ctx may be reply or proxy */
 			reply_tmp = fr_pair_list_copy(reply_packet, pl->reply);
 			radius_pairmove(request, &reply_packet->vps, reply_tmp, true);
-			fr_pair_list_move(request, &request->config, &check_tmp);
+			fr_pair_list_move(request, &request->config, &check_tmp, T_OP_ADD);
 			fr_pair_list_free(&check_tmp);
 
 			/*
diff --git a/src/modules/rlm_ldap/ldap.h b/src/modules/rlm_ldap/ldap.h
index 7dc874d35e..29e7cfd757 100644
--- a/src/modules/rlm_ldap/ldap.h
+++ b/src/modules/rlm_ldap/ldap.h
@@ -20,6 +20,7 @@
  *	always need to support that.
  */
 #define LDAP_DEPRECATED 1
+USES_APPLE_DEPRECATED_API	/* Apple wants us to use OpenDirectory Framework, we don't want that */
 #include <lber.h>
 #include <ldap.h>
 #include "config.h"
@@ -265,6 +266,9 @@ typedef struct ldap_instance {
 
 	int		tls_require_cert;		//!< OpenLDAP constant representing the require cert string.
 
+	char const	*tls_min_version_str;		//!< Minimum TLS version
+	int		tls_min_version;
+
 	/*
 	 *	Options
 	 */
diff --git a/src/modules/rlm_mschap/rlm_mschap.c b/src/modules/rlm_mschap/rlm_mschap.c
index 83c84a63e2..25bcd9c44e 100644
--- a/src/modules/rlm_mschap/rlm_mschap.c
+++ b/src/modules/rlm_mschap/rlm_mschap.c
@@ -322,6 +322,56 @@ static ssize_t mschap_xlat(void *instance, REQUEST *request,
 		data = response->vp_octets + 2;
 		data_len = 24;
 
+	/*
+	 *	Pull the domain name out of the User-Name, if it exists.
+	 *
+	 *	This is the full domain name, not just the name after host/
+	 */
+	} else if (strncasecmp(fmt, "Domain-Name", 11) == 0) {
+		char *p;
+
+		user_name = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+		if (!user_name) {
+			REDEBUG("No User-Name was found in the request");
+			return -1;
+		}
+
+		/*
+		 *	First check to see if this is a host/ style User-Name
+		 *	(a la Kerberos host principal)
+		 */
+		if (strncmp(user_name->vp_strvalue, "host/", 5) == 0) {
+			/*
+			 *	If we're getting a User-Name formatted in this way,
+			 *	it's likely due to PEAP.  The Windows Domain will be
+			 *	the first domain component following the hostname,
+			 *	or the machine name itself if only a hostname is supplied
+			 */
+			p = strchr(user_name->vp_strvalue, '.');
+			if (!p) {
+				RDEBUG2("setting NT-Domain to same as machine name");
+				strlcpy(out, user_name->vp_strvalue + 5, outlen);
+			} else {
+				p++;	/* skip the period */
+				strlcpy(out, p, outlen);
+			}
+		} else {
+			p = strchr(user_name->vp_strvalue, '\\');
+			if (!p) {
+				REDEBUG("No NT-Domain was found in the User-Name");
+				return -1;
+			}
+
+			/*
+			 *	Hack.  This is simpler than the alternatives.
+			 */
+			*p = '\0';
+			strlcpy(out, user_name->vp_strvalue, outlen);
+			*p = '\\';
+		}
+
+		return strlen(out);
+
 	/*
 	 *	Pull the NT-Domain out of the User-Name, if it exists.
 	 */
@@ -616,7 +666,7 @@ static int mod_instantiate(CONF_SECTION *conf, void *instance)
 			return -1;
 		}
 #else
-		cf_log_err_cs(conf, "'winbind' auth not enabled at compiled time");
+		cf_log_err_cs(conf, "'winbind' is not enabled in this build.");
 		return -1;
 #endif
 	}
@@ -942,7 +992,6 @@ ntlm_auth_err:
 		ssize_t result_len;
 		char result[253];
 		uint8_t nt_pass_decrypted[516], old_nt_hash_expected[NT_DIGEST_LENGTH];
-		RC4_KEY key;
 
 		if (!nt_password) {
 			RDEBUG("Local MS-CHAPv2 password change requires NT-Password attribute");
@@ -951,11 +1000,47 @@ ntlm_auth_err:
 			RDEBUG("Doing MS-CHAPv2 password change locally");
 		}
 
-		/*
-		 *  Decrypt the blob
-		 */
-		RC4_set_key(&key, nt_password->vp_length, nt_password->vp_octets);
-		RC4(&key, 516, new_nt_password, nt_pass_decrypted);
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+		{
+			EVP_CIPHER_CTX *ctx;
+			int ntlen = sizeof(nt_pass_decrypted);
+
+			ctx = EVP_CIPHER_CTX_new();
+			if (!ctx) {
+				REDEBUG("Failed getting RC4 from OpenSSL");
+			error:
+				if (ctx) EVP_CIPHER_CTX_free(ctx);
+				return -1;
+			}
+
+			if (!EVP_CIPHER_CTX_set_key_length(ctx, nt_password->vp_length)) {
+				REDEBUG("Failed setting key length");
+				goto error;
+			}
+
+			if (!EVP_EncryptInit_ex(ctx, EVP_rc4(), NULL, nt_password->vp_octets, NULL)) {
+				REDEBUG("Failed setting key value");
+				goto error;;
+			}
+
+			if (!EVP_EncryptUpdate(ctx, nt_pass_decrypted, &ntlen, new_nt_password, ntlen)) {
+				REDEBUG("Failed getting output");
+				goto error;
+			}
+
+			EVP_CIPHER_CTX_free(ctx);
+		}
+#else
+		{
+			RC4_KEY key;
+
+			/*
+			 *  Decrypt the blob
+			 */
+			RC4_set_key(&key, nt_password->vp_length, nt_password->vp_octets);
+			RC4(&key, 516, new_nt_password, nt_pass_decrypted);
+		}
+#endif
 
 		/*
 		 *  pwblock is
diff --git a/src/modules/rlm_otp/otp_mppe.c b/src/modules/rlm_otp/otp_mppe.c
index 399689abf3..932e44abe9 100644
--- a/src/modules/rlm_otp/otp_mppe.c
+++ b/src/modules/rlm_otp/otp_mppe.c
@@ -39,10 +39,16 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 
 #include <string.h>
 
+#if defined(OPENSSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER < 0x30000000L)
+#define UNUSED3
+#else
+#define UNUSED3 UNUSED
+#endif
+
 /*
  * Add MPPE attributes to a request, if required.
  */
-void otp_mppe(REQUEST *request, otp_pwe_t pwe, rlm_otp_t const *opt, char const *passcode)
+void otp_mppe(REQUEST *request, otp_pwe_t pwe, UNUSED3 rlm_otp_t const *opt, UNUSED3 char const *passcode)
 {
 	VALUE_PAIR *cvp, *rvp;
 
@@ -58,6 +64,7 @@ void otp_mppe(REQUEST *request, otp_pwe_t pwe, rlm_otp_t const *opt, char const
 	case PWE_CHAP:
 		return;
 
+#if defined(OPENSSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER < 0x30000000L)
 	case PWE_MSCHAP:
 		/* First, set some related attributes. */
 		pair_make_reply("MS-MPPE-Encryption-Policy", otp_mppe_policy[opt->mschap_mppe_policy], T_OP_EQ);
@@ -368,7 +375,12 @@ void otp_mppe(REQUEST *request, otp_pwe_t pwe, rlm_otp_t const *opt, char const
 
 		break; /* PWE_MSCHAP2 */
 	} /* PWE_MSCHAP2 */
-
+#else
+	case PWE_MSCHAP:
+	case PWE_MSCHAP2:
+		REDEBUG("MS-CHAP is unsupported for OpenSSL 3.");
+		break;
+#endif
 	} /* switch (pwe) */
 
 	return;
diff --git a/src/modules/rlm_otp/otp_pwe.c b/src/modules/rlm_otp/otp_pwe.c
index 56a4dbc71b..99a6d07769 100644
--- a/src/modules/rlm_otp/otp_pwe.c
+++ b/src/modules/rlm_otp/otp_pwe.c
@@ -28,19 +28,11 @@
 RCSID("$Id: 56a4dbc71b0117cb5eb788367e1fad7be9c8419a $")
 
 /* avoid inclusion of these FR headers which conflict w/ OpenSSL */
-#define _FR_MD4_H
-#define _FR_SHA1_H
 #include <freeradius-devel/radiusd.h>
 #include <freeradius-devel/rad_assert.h>
 
 #include "extern.h"
 
-USES_APPLE_DEPRECATED_API
-#include <openssl/des.h>
-#include <openssl/md4.h>
-#include <openssl/md5.h>
-#include <openssl/sha.h>
-
 #include <string.h>
 
 /* Attribute IDs for supported password encodings. */
diff --git a/src/modules/rlm_otp/otp_radstate.c b/src/modules/rlm_otp/otp_radstate.c
index 66fd8b4987..6e53430c2f 100644
--- a/src/modules/rlm_otp/otp_radstate.c
+++ b/src/modules/rlm_otp/otp_radstate.c
@@ -22,17 +22,13 @@
 RCSID("$Id: 66fd8b4987c0353e5679da23efc31595aa7017db $")
 USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 
-/* avoid inclusion of these FR headers which conflict w/ OpenSSL */
-#define _FR_MD4_H
-#define _FR_SHA1_H
-
 #include "extern.h"
 
 #include <string.h>
 
-#include <openssl/des.h> /* des_cblock */
 #include <openssl/md5.h>
 #include <openssl/hmac.h>
+#include <freeradius-devel/openssl3.h>
 
 /*
  * Generate the State attribute, suitable for passing to fr_pair_make().
@@ -113,6 +109,7 @@ size_t otp_gen_state(char state[OTP_MAX_RADSTATE_LEN],
 	HMAC_CTX *hmac_ctx;
 	uint8_t hmac[MD5_DIGEST_LENGTH];
 	char *p;
+	unsigned int len = sizeof(hmac);
 
 	/*
 	 *	Generate the hmac.  We already have a dependency on openssl for
@@ -125,7 +122,7 @@ size_t otp_gen_state(char state[OTP_MAX_RADSTATE_LEN],
 	HMAC_Update(hmac_ctx, (uint8_t const *) challenge, clen);
 	HMAC_Update(hmac_ctx, (uint8_t *) &flags, 4);
 	HMAC_Update(hmac_ctx, (uint8_t *) &when, 4);
-	HMAC_Final(hmac_ctx, hmac, NULL);
+	HMAC_Final(hmac_ctx, hmac, &len);
 	HMAC_CTX_free(hmac_ctx);
 
 	/*
diff --git a/src/modules/rlm_rest/rest.c b/src/modules/rlm_rest/rest.c
index 8b85df0662..fcb3fd11fc 100644
--- a/src/modules/rlm_rest/rest.c
+++ b/src/modules/rlm_rest/rest.c
@@ -115,6 +115,15 @@ do {\
 	}\
 } while (0)
 
+/*
+ * that macro is originally declared in include/curl/curlver.h
+ * We have to use this as curl uses lots of enums
+ */
+#ifndef CURL_AT_LEAST_VERSION
+#  define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z))
+#  define CURL_AT_LEAST_VERSION(x, y, z) (LIBCURL_VERSION_NUM >= CURL_VERSION_BITS(x, y, z))
+#endif
+
 const unsigned long http_curl_auth[HTTP_AUTH_NUM_ENTRIES] = {
 	0,			// HTTP_AUTH_UNKNOWN
 	0,			// HTTP_AUTH_NONE
@@ -221,6 +230,40 @@ const FR_NAME_NUMBER http_content_type_table[] = {
 	{  NULL , -1 }
 };
 
+/** Conversion table for "HTTP" protocol version to use.
+ *
+ * Used by rlm_rest_t for specify the http client version.
+ *
+ * Values we expect to use in curl_easy_setopt()
+ *
+ * @see fr_str2int
+ * @see fr_int2str
+ */
+const FR_NAME_NUMBER http_negotiation_table[] = {
+
+	{ "1.0", 	CURL_HTTP_VERSION_1_0 },		//!< Enforce HTTP 1.0 requests.
+	{ "1.1",	CURL_HTTP_VERSION_1_1 },		//!< Enforce HTTP 1.1 requests.
+/*
+ *	These are all enum values
+ */
+#if CURL_AT_LEAST_VERSION(7,49,0)
+	{ "2.0", 	CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE },	//!< Enforce HTTP 2.0 requests.
+#endif
+#if CURL_AT_LEAST_VERSION(7,33,0)
+	{ "2.0+auto",	CURL_HTTP_VERSION_2_0 },		//!< Attempt HTTP 2 requests. libcurl will fall back
+								///< to HTTP 1.1 if HTTP 2 can't be negotiated with the
+								///< server. (Added in 7.33.0)
+#endif
+#if CURL_AT_LEAST_VERSION(7,47,0)
+	{ "2.0+tls",	CURL_HTTP_VERSION_2TLS },		//!< Attempt HTTP 2 over TLS (HTTPS) only.
+								///< libcurl will fall back to HTTP 1.1 if HTTP 2
+								///< can't be negotiated with the HTTPS server.
+								///< For clear text HTTP servers, libcurl will use 1.1.
+#endif
+	{ "default", 	CURL_HTTP_VERSION_NONE }		//!< We don't care about what version the library uses.
+								///< libcurl will use whatever it thinks fit.
+};
+
 /*
  *	Encoder specific structures.
  *	@todo split encoders/decoders into submodules.
@@ -603,19 +646,30 @@ static size_t rest_encode_post(void *out, size_t size, size_t nmemb, void *userd
 		}
 
 		/*
-		 *  there are more attributes, insert a separator
+		 *  there are no more attributes, stop
 		 */
-		if (fr_cursor_next(&ctx->cursor)) {
-			if (freespace < 1) goto no_space;
-			*p++ = '&';
-			freespace--;
+		if (!fr_cursor_next_peek(&ctx->cursor)) {
+			ctx->state = READ_STATE_END;
+			break;
 		}
 
+		if (freespace < 1) goto no_space;
+		*p++ = '&';
+		freespace--;
+		/*
+		 *	Only advance once we have a separator
+		 *	really we should have an additional
+		 *	state for encoding the separator,
+		 *	but, we don't, and v3.0.x is stable
+		 *	so let's do the easiest fix with the
+		 *	lowest risk.
+		 */
+		fr_cursor_next(&ctx->cursor);
+
 		/*
 		 *  We wrote one full attribute value pair, record progress.
 		 */
 		encoded = p;
-
 		ctx->state = READ_STATE_ATTR_BEGIN;
 	}
 
@@ -747,7 +801,13 @@ static size_t rest_encode_json(void *out, size_t size, size_t nmemb, void *userd
 
 			type = fr_int2str(dict_attr_types, vp->da->type, "<INVALID>");
 
-			len = snprintf(p, freespace + 1, "\"%s\":{\"type\":\"%s\",\"value\":[", vp->da->name, type);
+			if (ctx->section->attr_num) {
+				len = snprintf(p, freespace + 1, "\"%s\":{\"attr_num\":%d,\"type\":\"%s\",\"value\":[",
+						vp->da->name, vp->da->attr, type);
+			} else {
+				len = snprintf(p, freespace + 1, "\"%s\":{\"type\":\"%s\",\"value\":[", vp->da->name, type);
+			}
+
 			if (len >= freespace) goto no_space;
 			p += len;
 			freespace -= len;
@@ -783,7 +843,7 @@ static size_t rest_encode_json(void *out, size_t size, size_t nmemb, void *userd
 				 *  write that out.
 				 */
 				attr_space = fr_cursor_next_peek(&ctx->cursor) ? freespace - 1 : freespace;
-				len = vp_prints_value_json(p, attr_space + 1, vp);
+				len = vp_prints_value_json(p, attr_space + 1, vp, ctx->section->raw_value);
 				if (is_truncated(len, attr_space + 1)) goto no_space;
 
 				/*
@@ -1575,8 +1635,9 @@ static size_t rest_response_header(void *in, size_t size, size_t nmemb, void *us
 		 *  HTTP/<version> <reason_code>[ <reason_phrase>]\r\n
 		 *
 		 *  "HTTP/1.1 " (8) + "100 " (4) + "\r\n" (2) = 14
+		 *  "HTTP/2 " (8) + "100 " (4) + "\r\n" (2) = 12
 		 */
-		if (s < 14) {
+		if (s < 12) {
 			REDEBUG("Malformed HTTP header: Status line too short");
 			goto malformed;
 		}
@@ -1614,8 +1675,10 @@ static size_t rest_response_header(void *in, size_t size, size_t nmemb, void *us
 		p++;
 		s--;
 
-		/*  Char after reason code must be a space, or \r */
-		if (!((p[3] == ' ') || (p[3] == '\r'))) goto malformed;
+		/*
+		 *  "xxx( |\r)" status code and terminator.
+		 */
+		if (!isdigit(p[0]) || !isdigit(p[1]) || !isdigit(p[2]) || !((p[3] == ' ') || (p[3] == '\r'))) goto malformed;
 
 		ctx->code = atoi(p);
 
@@ -1996,11 +2059,24 @@ int rest_request_config(rlm_rest_t *instance, rlm_rest_section_t *section,
 	SET_OPTION(CURLOPT_NOSIGNAL, 1);
 	SET_OPTION(CURLOPT_USERAGENT, "FreeRADIUS " RADIUSD_VERSION_STRING);
 
+	/*
+	 *	As described in https://curl.se/libcurl/c/CURLOPT_HTTP_VERSION.html,
+	 *	The libcurl decides which http version should be
+	 *  used by default accoring by library version.
+	 */
+	if (instance->http_negotiation != CURL_HTTP_VERSION_NONE) {
+		RDEBUG3("Set HTTP negotiation for %s", instance->http_negotiation_str);
+		SET_OPTION(CURLOPT_HTTP_VERSION, instance->http_negotiation);
+	}
+
 	content_type = fr_int2str(http_content_type_table, type, section->body_str);
 	snprintf(buffer, sizeof(buffer), "Content-Type: %s", content_type);
 	ctx->headers = curl_slist_append(ctx->headers, buffer);
 	if (!ctx->headers) goto error_header;
 
+	// Pass configuration to the request
+	ctx->request.section = section;
+
 	SET_OPTION(CURLOPT_CONNECTTIMEOUT_MS, instance->connect_timeout);
 	SET_OPTION(CURLOPT_TIMEOUT_MS, section->timeout);
 
@@ -2322,6 +2398,7 @@ int rest_request_perform(UNUSED rlm_rest_t *instance, UNUSED rlm_rest_section_t
 	rlm_rest_handle_t	*randle = handle;
 	CURL			*candle = randle->handle;
 	CURLcode		ret;
+	VALUE_PAIR 		*vp;
 
 	ret = curl_easy_perform(candle);
 	if (ret != CURLE_OK) {
@@ -2330,6 +2407,14 @@ int rest_request_perform(UNUSED rlm_rest_t *instance, UNUSED rlm_rest_section_t
 		return -1;
 	}
 
+	/*
+	 *  Save the HTTP return status code.
+	 */
+	vp = pair_make_reply("REST-HTTP-Status-Code", NULL, T_OP_SET);
+	vp->vp_integer = rest_get_handle_code(handle);
+
+	RDEBUG2("Adding reply:REST-HTTP-Status-Code = \"%d\"", vp->vp_integer);
+
 	return 0;
 }
 
diff --git a/src/modules/rlm_rest/rest.h b/src/modules/rlm_rest/rest.h
index 9e278d16c4..4c41cf46c2 100644
--- a/src/modules/rlm_rest/rest.h
+++ b/src/modules/rlm_rest/rest.h
@@ -31,6 +31,10 @@ RCSIDH(other_h, "$Id$")
 #define CURL_NO_OLDIES 1
 #include <curl/curl.h>
 
+#ifdef HAVE_WDOCUMENTATION
+DIAG_OFF(documentation)
+#endif
+
 #ifdef HAVE_JSON
 #  if defined(HAVE_JSONMC_JSON_H)
 #    include <json-c/json.h>
@@ -39,6 +43,10 @@ RCSIDH(other_h, "$Id$")
 #  endif
 #endif
 
+#ifdef HAVE_WDOCUMENTATION
+DIAG_ON(documentation)
+#endif
+
 #define REST_URI_MAX_LEN		2048
 #define REST_BODY_MAX_LEN		8192
 #define REST_BODY_INIT			1024
@@ -102,6 +110,8 @@ extern const FR_NAME_NUMBER http_body_type_table[];
 
 extern const FR_NAME_NUMBER http_content_type_table[];
 
+extern const FR_NAME_NUMBER http_negotiation_table[];
+
 /*
  *	Structure for section configuration
  */
@@ -115,6 +125,9 @@ typedef struct rlm_rest_section_t {
 	char const		*body_str;	//!< The string version of the encoding/content type.
 	http_body_type_t	body;		//!< What encoding type should be used.
 
+	bool			attr_num;	//!< If true, the the attribute number is supplied for each attribute.
+	bool			raw_value;	//!< If true, enumerated attributes are provided as a numeric value
+
 	char const		*force_to_str;	//!< Force decoding with this decoder.
 	http_body_type_t	force_to;	//!< Override the Content-Type header in the response
 						//!< to force decoding as a particular type.
@@ -154,6 +167,9 @@ typedef struct rlm_rest_t {
 	struct timeval		connect_timeout_tv;	//!< Connection timeout timeval.
 	long			connect_timeout;	//!< Connection timeout ms.
 
+	char const		*http_negotiation_str;	//!< The string version of the http_negotiation
+	long			http_negotiation;	//!< The HTTP protocol version to use
+
 	fr_connection_pool_t	*pool;		//!< Pointer to the connection pool.
 
 	rlm_rest_section_t	authorize;	//!< Configuration specific to authorisation.
@@ -205,6 +221,8 @@ typedef struct rlm_rest_request_t {
 
 	size_t			chunk;		//!< Chunk size
 
+	rlm_rest_section_t *section;	//!< Configuration data
+
 	void			*encoder;	//!< Encoder specific data.
 } rlm_rest_request_t;
 
diff --git a/src/modules/rlm_rest/rlm_rest.c b/src/modules/rlm_rest/rlm_rest.c
index 6fa1345614..1337ae5425 100644
--- a/src/modules/rlm_rest/rlm_rest.c
+++ b/src/modules/rlm_rest/rlm_rest.c
@@ -60,6 +60,8 @@ static const CONF_PARSER section_config[] = {
 	{ "uri", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_rest_section_t, uri), ""   },
 	{ "method", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, method_str), "GET" },
 	{ "body", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, body_str), "none" },
+	{ "attr_num", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_rest_section_t, attr_num), "no" },
+	{ "raw_value", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_rest_section_t, raw_value), "no" },
 	{ "data", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_rest_section_t, data), NULL },
 	{ "force_to", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, force_to_str), NULL },
 
@@ -81,6 +83,8 @@ static const CONF_PARSER section_config[] = {
 static const CONF_PARSER module_config[] = {
 	{ "connect_uri", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_t, connect_uri), NULL },
 	{ "connect_timeout", FR_CONF_OFFSET(PW_TYPE_TIMEVAL, rlm_rest_t, connect_timeout_tv), "4.0" },
+	{ "http_negotiation", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_t, http_negotiation_str), "default" },
+
 	CONF_PARSER_TERMINATOR
 };
 
@@ -223,6 +227,8 @@ static ssize_t rest_xlat(void *instance, REQUEST *request,
 		.name = "xlat",
 		.method = HTTP_METHOD_GET,
 		.body = HTTP_BODY_NONE,
+		.attr_num = false,
+		.raw_value = false,
 		.body_str = "application/x-www-form-urlencoded",
 		.require_auth = false,
 		.force_to = HTTP_BODY_PLAIN
@@ -926,6 +932,12 @@ static int mod_instantiate(CONF_SECTION *conf, void *instance)
 		return -1;
 	}
 
+	inst->http_negotiation = fr_str2int(http_negotiation_table, inst->http_negotiation_str, -1);
+	if (inst->http_negotiation == -1) {
+		cf_log_err_cs(conf, "Unsupported HTTP version \"%s\".", inst->http_negotiation_str);
+		return -1;
+	}
+
 	/*
 	 *	Initialise REST libraries.
 	 */
diff --git a/src/modules/rlm_wimax/milenage.c b/src/modules/rlm_wimax/milenage.c
new file mode 100644
index 0000000000..e14086e78c
--- /dev/null
+++ b/src/modules/rlm_wimax/milenage.c
@@ -0,0 +1,642 @@
+/**
+ * @file src/modules/rlm_wimax/milenage.c
+ * @brief 3GPP AKA - Milenage algorithm (3GPP TS 35.205, .206, .207, .208)
+ *
+ * This file implements an example authentication algorithm defined for 3GPP
+ * AKA. This can be used to implement a simple HLR/AuC into hlr_auc_gw to allow
+ * EAP-AKA to be tested properly with real USIM cards.
+ *
+ * This implementations assumes that the r1..r5 and c1..c5 constants defined in
+ * TS 35.206 are used, i.e., r1=64, r2=0, r3=32, r4=64, r5=96, c1=00..00,
+ * c2=00..01, c3=00..02, c4=00..04, c5=00..08. The block cipher is assumed to
+ * be AES (Rijndael).
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ *
+ * @copyright 2017 The FreeRADIUS server project
+ * @copyright 2006-2007 (j@w1.fi)
+ */
+#include <stddef.h>
+#include <string.h>
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <openssl/evp.h>
+#include "milenage.h"
+
+#define MILENAGE_MAC_A_SIZE	8
+#define MILENAGE_MAC_S_SIZE	8
+
+static inline int aes_128_encrypt_block(EVP_CIPHER_CTX *evp_ctx,
+					uint8_t const key[16], uint8_t const in[16], uint8_t out[16])
+{
+	size_t len;
+
+	if (unlikely(EVP_EncryptInit_ex(evp_ctx, EVP_aes_128_ecb(), NULL, key, NULL) != 1)) {
+		fr_strerror_printf("Failed initialising AES-128-ECB context");
+		return -1;
+	}
+
+	/*
+	 *	By default OpenSSL will try and pad out a 16 byte
+	 *	plaintext to 32 bytes so that it's detectable that
+	 *	there was padding.
+	 *
+	 *	In this case we know the length of the plaintext
+	 *	we're trying to recover, so we explicitly tell
+	 *	OpenSSL not to pad here, and not to expected padding
+	 *	when decrypting.
+	 */
+	EVP_CIPHER_CTX_set_padding(evp_ctx, 0);
+	if (unlikely(EVP_EncryptUpdate(evp_ctx, out, (int *)&len, in, 16) != 1) ||
+	    unlikely(EVP_EncryptFinal_ex(evp_ctx, out + len, (int *)&len) != 1)) {
+		fr_strerror_printf("Failed encrypting data");
+		return -1;
+	}
+
+	return 0;
+}
+
+/** milenage_f1 - Milenage f1 and f1* algorithms
+ *
+ * @param[in] opc	128-bit value derived from OP and K.
+ * @param[in] k		128-bit subscriber key.
+ * @param[in] rand	128-bit random challenge.
+ * @param[in] sqn	48-bit sequence number.
+ * @param[in] amf	16-bit authentication management field.
+ * @param[out] mac_a	Buffer for MAC-A = 64-bit network authentication code, or NULL
+ * @param[out] mac_s	Buffer for MAC-S = 64-bit resync authentication code, or NULL
+ * @return
+ *	- 0 on success.
+ *	- -1 on failure.
+ */
+static int milenage_f1(uint8_t mac_a[MILENAGE_MAC_A_SIZE],
+		       uint8_t mac_s[MILENAGE_MAC_S_SIZE],
+		       uint8_t const opc[MILENAGE_OPC_SIZE],
+		       uint8_t const k[MILENAGE_KI_SIZE],
+		       uint8_t const rand[MILENAGE_RAND_SIZE],
+		       uint8_t const sqn[MILENAGE_SQN_SIZE],
+		       uint8_t const amf[MILENAGE_AMF_SIZE])
+{
+	uint8_t		tmp1[16], tmp2[16], tmp3[16];
+	int		i;
+	EVP_CIPHER_CTX	*evp_ctx;
+
+	/* tmp1 = TEMP = E_K(RAND XOR OP_C) */
+	for (i = 0; i < 16; i++) tmp1[i] = rand[i] ^ opc[i];
+
+	evp_ctx = EVP_CIPHER_CTX_new();
+	if (!evp_ctx) {
+	  //tls_strerror_printf("Failed allocating EVP context");
+		return -1;
+	}
+
+ 	if (aes_128_encrypt_block(evp_ctx, k, tmp1, tmp1) < 0) {
+ 	error:
+		EVP_CIPHER_CTX_free(evp_ctx);
+		return -1;
+ 	}
+
+	/* tmp2 = IN1 = SQN || AMF || SQN || AMF */
+	memcpy(tmp2, sqn, 6);
+	memcpy(tmp2 + 6, amf, 2);
+	memcpy(tmp2 + 8, tmp2, 8);
+
+	/* OUT1 = E_K(TEMP XOR rot(IN1 XOR OP_C, r1) XOR c1) XOR OP_C */
+
+	/*
+	 *  rotate (tmp2 XOR OP_C) by r1 (= 0x40 = 8 bytes)
+	 */
+	for (i = 0; i < 16; i++) tmp3[(i + 8) % 16] = tmp2[i] ^ opc[i];
+
+	/*
+	 *  XOR with TEMP = E_K(RAND XOR OP_C)
+	 */
+	for (i = 0; i < 16; i++) tmp3[i] ^= tmp1[i];
+	/* XOR with c1 (= ..00, i.e., NOP) */
+
+	/*
+	 *	f1 || f1* = E_K(tmp3) XOR OP_c
+	 */
+ 	if (aes_128_encrypt_block(evp_ctx, k, tmp3, tmp1) < 0) goto error; /* Reuses existing key */
+
+	for (i = 0; i < 16; i++) tmp1[i] ^= opc[i];
+
+	if (mac_a) memcpy(mac_a, tmp1, 8);	/* f1 */
+	if (mac_s) memcpy(mac_s, tmp1 + 8, 8);	/* f1* */
+
+	EVP_CIPHER_CTX_free(evp_ctx);
+
+	return 0;
+}
+
+/** milenage_f2345 - Milenage f2, f3, f4, f5, f5* algorithms
+ *
+ * @param[out] res		Buffer for RES = 64-bit signed response (f2), or NULL
+ * @param[out] ck		Buffer for CK = 128-bit confidentiality key (f3), or NULL
+ * @param[out] ik		Buffer for IK = 128-bit integrity key (f4), or NULL
+ * @param[out] ak		Buffer for AK = 48-bit anonymity key (f5), or NULL
+ * @param[out] ak_resync	Buffer for AK = 48-bit anonymity key (f5*), or NULL
+ * @param[in] opc		128-bit value derived from OP and K.
+ * @param[in] k			128-bit subscriber key
+ * @param[in] rand		128-bit random challenge
+ * @return
+ *	- 0 on success.
+ *	- -1 on failure.
+ */
+static int milenage_f2345(uint8_t res[MILENAGE_RES_SIZE],
+			  uint8_t ik[MILENAGE_IK_SIZE],
+			  uint8_t ck[MILENAGE_CK_SIZE],
+			  uint8_t ak[MILENAGE_AK_SIZE],
+			  uint8_t ak_resync[MILENAGE_AK_SIZE],
+			  uint8_t const opc[MILENAGE_OPC_SIZE],
+			  uint8_t const k[MILENAGE_KI_SIZE],
+			  uint8_t const rand[MILENAGE_RAND_SIZE])
+{
+	uint8_t			tmp1[16], tmp2[16], tmp3[16];
+	int			i;
+	EVP_CIPHER_CTX		*evp_ctx;
+
+	/* tmp2 = TEMP = E_K(RAND XOR OP_C) */
+	for (i = 0; i < 16; i++) tmp1[i] = rand[i] ^ opc[i];
+
+	evp_ctx = EVP_CIPHER_CTX_new();
+	if (!evp_ctx) {
+		fr_strerror_printf("Failed allocating EVP context");
+		return -1;
+	}
+
+	if (aes_128_encrypt_block(evp_ctx, k, tmp1, tmp2) < 0) {
+	error:
+		EVP_CIPHER_CTX_free(evp_ctx);
+		return -1;
+	}
+
+	/* OUT2 = E_K(rot(TEMP XOR OP_C, r2) XOR c2) XOR OP_C */
+	/* OUT3 = E_K(rot(TEMP XOR OP_C, r3) XOR c3) XOR OP_C */
+	/* OUT4 = E_K(rot(TEMP XOR OP_C, r4) XOR c4) XOR OP_C */
+	/* OUT5 = E_K(rot(TEMP XOR OP_C, r5) XOR c5) XOR OP_C */
+
+	/* f2 and f5 */
+	/* rotate by r2 (= 0, i.e., NOP) */
+	for (i = 0; i < 16; i++) tmp1[i] = tmp2[i] ^ opc[i];
+	tmp1[15] ^= 1; /* XOR c2 (= ..01) */
+	/* f5 || f2 = E_K(tmp1) XOR OP_c */
+
+	if (aes_128_encrypt_block(evp_ctx, k, tmp1, tmp3) < 0) goto error;
+
+	for (i = 0; i < 16; i++) tmp3[i] ^= opc[i];
+	if (res) memcpy(res, tmp3 + 8, 8); /* f2 */
+	if (ak) memcpy(ak, tmp3, 6); /* f5 */
+
+	/* f3 */
+	if (ck) {
+		/* rotate by r3 = 0x20 = 4 bytes */
+		for (i = 0; i < 16; i++) tmp1[(i + 12) % 16] = tmp2[i] ^ opc[i];
+		tmp1[15] ^= 2; /* XOR c3 (= ..02) */
+
+		if (aes_128_encrypt_block(evp_ctx, k, tmp1, ck) < 0) goto error;
+
+		for (i = 0; i < 16; i++) ck[i] ^= opc[i];
+	}
+
+	/* f4 */
+	if (ik) {
+		/* rotate by r4 = 0x40 = 8 bytes */
+		for (i = 0; i < 16; i++) tmp1[(i + 8) % 16] = tmp2[i] ^ opc[i];
+		tmp1[15] ^= 4; /* XOR c4 (= ..04) */
+
+		if (aes_128_encrypt_block(evp_ctx, k, tmp1, ik) < 0) goto error;
+
+		for (i = 0; i < 16; i++) ik[i] ^= opc[i];
+	}
+
+	/* f5* */
+	if (ak_resync) {
+		/* rotate by r5 = 0x60 = 12 bytes */
+		for (i = 0; i < 16; i++) tmp1[(i + 4) % 16] = tmp2[i] ^ opc[i];
+		tmp1[15] ^= 8; /* XOR c5 (= ..08) */
+
+		if (aes_128_encrypt_block(evp_ctx, k, tmp1, tmp1) < 0) goto error;
+
+		for (i = 0; i < 6; i++) ak_resync[i] = tmp1[i] ^ opc[i];
+	}
+	EVP_CIPHER_CTX_free(evp_ctx);
+
+	return 0;
+}
+
+/** Derive OPc from OP and Ki
+ *
+ * @param[out] opc	The derived Operator Code used as an input to other Milenage
+ *			functions.
+ * @param[in] op	Operator Code.
+ * @param[in] ki	Subscriber key.
+ * @return
+ *	- 0 on success.
+ *	- -1 on failure.
+ */
+int milenage_opc_generate(uint8_t opc[MILENAGE_OPC_SIZE],
+			  uint8_t const op[MILENAGE_OP_SIZE],
+			  uint8_t const ki[MILENAGE_KI_SIZE])
+{
+	int		ret;
+	uint8_t		tmp[MILENAGE_OPC_SIZE];
+	EVP_CIPHER_CTX	*evp_ctx;
+	size_t		i;
+
+	evp_ctx = EVP_CIPHER_CTX_new();
+	if (!evp_ctx) {
+		fr_strerror_printf("Failed allocating EVP context");
+		return -1;
+	}
+ 	ret = aes_128_encrypt_block(evp_ctx, ki, op, tmp);
+ 	EVP_CIPHER_CTX_free(evp_ctx);
+	if (ret < 0) return ret;
+
+ 	for (i = 0; i < sizeof(tmp); i++) opc[i] = op[i] ^ tmp[i];
+
+ 	return 0;
+}
+
+/** Generate AKA AUTN, IK, CK, RES
+ *
+ * @param[out] autn	Buffer for AUTN = 128-bit authentication token.
+ * @param[out] ik	Buffer for IK = 128-bit integrity key (f4), or NULL.
+ * @param[out] ck	Buffer for CK = 128-bit confidentiality key (f3), or NULL.
+ * @param[out] ak	Buffer for AK = 48-bit anonymity key (f5), or NULL
+ * @param[out] res	Buffer for RES = 64-bit signed response (f2), or NULL.
+ * @param[in] opc	128-bit operator variant algorithm configuration field (encr.).
+ * @param[in] amf	16-bit authentication management field.
+ * @param[in] ki	128-bit subscriber key.
+ * @param[in] sqn	48-bit sequence number (host byte order).
+ * @param[in] rand	128-bit random challenge.
+ * @return
+ *	- 0 on success.
+ *	- -1 on failure.
+ */
+int milenage_umts_generate(uint8_t autn[MILENAGE_AUTN_SIZE],
+			   uint8_t ik[MILENAGE_IK_SIZE],
+			   uint8_t ck[MILENAGE_CK_SIZE],
+			   uint8_t ak[MILENAGE_AK_SIZE],
+			   uint8_t res[MILENAGE_RES_SIZE],
+			   uint8_t const opc[MILENAGE_OPC_SIZE],
+			   uint8_t const amf[MILENAGE_AMF_SIZE],
+			   uint8_t const ki[MILENAGE_KI_SIZE],
+			   uint64_t sqn,
+			   uint8_t const rand[MILENAGE_RAND_SIZE])
+{
+	uint8_t		mac_a[8], ak_buff[MILENAGE_AK_SIZE];
+	uint8_t		sqn_buff[MILENAGE_SQN_SIZE];
+	uint8_t		*p = autn;
+	size_t		i;
+
+	if ((milenage_f1(mac_a, NULL, opc, ki, rand,
+			 uint48_to_buff(sqn_buff, sqn), amf) < 0) ||
+	    (milenage_f2345(res, ik, ck, ak_buff, NULL, opc, ki, rand) < 0)) return -1;
+
+	/*
+	 *	AUTN = (SQN ^ AK) || AMF || MAC_A
+	 */
+	for (i = 0; i < sizeof(sqn_buff); i++) *p++ = sqn_buff[i] ^ ak_buff[i];
+	memcpy(p, amf, MILENAGE_AMF_SIZE);
+	p += MILENAGE_AMF_SIZE;
+	memcpy(p, mac_a, sizeof(mac_a));
+
+	/*
+	 *	Output the anonymity key if required
+	 */
+	if (ak) memcpy(ak, ak_buff, sizeof(ak_buff));
+
+	return 0;
+}
+
+/** Milenage AUTS validation
+ *
+ * @param[out] sqn	SQN = 48-bit sequence number (host byte order).
+ * @param[in] opc	128-bit operator variant algorithm configuration field (encr.).
+ * @param[in] ki	128-bit subscriber key.
+ * @param[in] rand	128-bit random challenge.
+ * @param[in] auts	112-bit authentication token from client.
+ * @return
+ *	- 0 on success with sqn filled.
+ *	- -1 on failure.
+ */
+int milenage_auts(uint64_t *sqn,
+		  uint8_t const opc[MILENAGE_OPC_SIZE],
+		  uint8_t const ki[MILENAGE_KI_SIZE],
+		  uint8_t const rand[MILENAGE_RAND_SIZE],
+		  uint8_t const auts[MILENAGE_AUTS_SIZE])
+{
+	uint8_t		amf[MILENAGE_AMF_SIZE] = { 0x00, 0x00 }; /* TS 33.102 v7.0.0, 6.3.3 */
+	uint8_t		ak[MILENAGE_AK_SIZE], mac_s[MILENAGE_MAC_S_SIZE];
+	uint8_t		sqn_buff[MILENAGE_SQN_SIZE];
+	size_t		i;
+
+	if (milenage_f2345(NULL, NULL, NULL, NULL, ak, opc, ki, rand)) return -1;
+	for (i = 0; i < sizeof(sqn_buff); i++) sqn_buff[i] = auts[i] ^ ak[i];
+
+	if (milenage_f1(NULL, mac_s, opc, ki, rand, sqn_buff, amf) || CRYPTO_memcmp(mac_s, auts + 6, 8) != 0) return -1;
+
+	*sqn = uint48_from_buff(sqn_buff);
+
+	return 0;
+}
+
+/** Generate GSM-Milenage (3GPP TS 55.205) authentication triplet from a quintuplet
+ *
+ * @param[out] sres	Buffer for SRES = 32-bit SRES.
+ * @param[out] kc	64-bit Kc.
+ * @param[in] ik	128-bit integrity.
+ * @param[in] ck	Confidentiality key.
+ * @param[in] res	64-bit signed response.
+ */
+void milenage_gsm_from_umts(uint8_t sres[MILENAGE_SRES_SIZE],
+			    uint8_t kc[MILENAGE_KC_SIZE],
+			    uint8_t const ik[MILENAGE_IK_SIZE],
+			    uint8_t const ck[MILENAGE_CK_SIZE],
+			    uint8_t const res[MILENAGE_RES_SIZE])
+{
+	int i;
+
+	for (i = 0; i < 8; i++) kc[i] = ck[i] ^ ck[i + 8] ^ ik[i] ^ ik[i + 8];
+
+#ifdef GSM_MILENAGE_ALT_SRES
+	memcpy(sres, res, 4);
+#else	/* GSM_MILENAGE_ALT_SRES */
+	for (i = 0; i < 4; i++) sres[i] = res[i] ^ res[i + 4];
+#endif	/* GSM_MILENAGE_ALT_SRES */
+}
+
+/** Generate GSM-Milenage (3GPP TS 55.205) authentication triplet
+ *
+ * @param[out] sres	Buffer for SRES = 32-bit SRES.
+ * @param[out] kc	64-bit Kc.
+ * @param[in] opc	128-bit operator variant algorithm configuration field (encr.).
+ * @param[in] ki	128-bit subscriber key.
+ * @param[in] rand	128-bit random challenge.
+ * @return
+ *	- 0 on success.
+ *	- -1 on failure.
+ */
+int milenage_gsm_generate(uint8_t sres[MILENAGE_SRES_SIZE],
+			  uint8_t kc[MILENAGE_KC_SIZE],
+			  uint8_t const opc[MILENAGE_OPC_SIZE],
+			  uint8_t const ki[MILENAGE_KI_SIZE],
+			  uint8_t const rand[MILENAGE_RAND_SIZE])
+{
+	uint8_t		res[MILENAGE_RES_SIZE], ck[MILENAGE_CK_SIZE], ik[MILENAGE_IK_SIZE];
+
+	if (milenage_f2345(res, ik, ck, NULL, NULL, opc, ki, rand)) return -1;
+
+	milenage_gsm_from_umts(sres, kc, ik, ck, res);
+
+	return 0;
+}
+
+/** Milenage check
+ *
+ * @param[out] ik	Buffer for IK = 128-bit integrity key (f4), or NULL.
+ * @param[out] ck	Buffer for CK = 128-bit confidentiality key (f3), or NULL.
+ * @param[out] res	Buffer for RES = 64-bit signed response (f2), or NULL.
+ * @param[in] auts	112-bit buffer for AUTS.
+ * @param[in] opc	128-bit operator variant algorithm configuration field (encr.).
+ * @param[in] ki	128-bit subscriber key.
+ * @param[in] sqn	48-bit sequence number.
+ * @param[in] rand	128-bit random challenge.
+ * @param[in] autn	128-bit authentication token.
+ * @return
+ *	- 0 on success.
+ *	- -1 on failure.
+ *	- -2 on synchronization failure
+ */
+int milenage_check(uint8_t ik[MILENAGE_IK_SIZE],
+		   uint8_t ck[MILENAGE_CK_SIZE],
+		   uint8_t res[MILENAGE_RES_SIZE],
+		   uint8_t auts[MILENAGE_AUTS_SIZE],
+		   uint8_t const opc[MILENAGE_OPC_SIZE],
+		   uint8_t const ki[MILENAGE_KI_SIZE],
+		   uint64_t sqn,
+		   uint8_t const rand[MILENAGE_RAND_SIZE],
+		   uint8_t const autn[MILENAGE_AUTN_SIZE])
+{
+
+	uint8_t mac_a[MILENAGE_MAC_A_SIZE], ak[MILENAGE_AK_SIZE], rx_sqn[MILENAGE_SQN_SIZE];
+	uint8_t sqn_buff[MILENAGE_SQN_SIZE];
+	const uint8_t *amf;
+	size_t i;
+
+	uint48_to_buff(sqn_buff, sqn);
+
+	//FR_PROTO_HEX_DUMP(autn, MILENAGE_AUTN_SIZE, "AUTN");
+	//FR_PROTO_HEX_DUMP(rand, MILENAGE_RAND_SIZE, "RAND");
+
+	if (milenage_f2345(res, ck, ik, ak, NULL, opc, ki, rand)) return -1;
+
+	//FR_PROTO_HEX_DUMP(res, MILENAGE_RES_SIZE, "RES");
+	//FR_PROTO_HEX_DUMP(ck, MILENAGE_CK_SIZE, "CK");
+	//FR_PROTO_HEX_DUMP(ik, MILENAGE_IK_SIZE, "IK");
+	//FR_PROTO_HEX_DUMP(ak, MILENAGE_AK_SIZE, "AK");
+
+	/* AUTN = (SQN ^ AK) || AMF || MAC */
+	for (i = 0; i < 6; i++) rx_sqn[i] = autn[i] ^ ak[i];
+	//FR_PROTO_HEX_DUMP(rx_sqn, MILENAGE_SQN_SIZE, "SQN");
+
+	if (CRYPTO_memcmp(rx_sqn, sqn_buff, sizeof(rx_sqn)) <= 0) {
+		uint8_t auts_amf[MILENAGE_AMF_SIZE] = { 0x00, 0x00 }; /* TS 33.102 v7.0.0, 6.3.3 */
+
+		if (milenage_f2345(NULL, NULL, NULL, NULL, ak, opc, ki, rand)) return -1;
+
+		//FR_PROTO_HEX_DUMP(ak, sizeof(ak), "AK*");
+		for (i = 0; i < 6; i++) auts[i] = sqn_buff[i] ^ ak[i];
+
+		if (milenage_f1(NULL, auts + 6, opc, ki, rand, sqn_buff, auts_amf) < 0) return -1;
+		//FR_PROTO_HEX_DUMP(auts, 14, "AUTS");
+		return -2;
+	}
+
+	amf = autn + 6;
+	//FR_PROTO_HEX_DUMP(amf, MILENAGE_AMF_SIZE, "AMF");
+	if (milenage_f1(mac_a, NULL, opc, ki, rand, rx_sqn, amf) < 0) return -1;
+
+	//FR_PROTO_HEX_DUMP(mac_a, MILENAGE_MAC_A_SIZE, "MAC_A");
+
+	if (CRYPTO_memcmp(mac_a, autn + 8, 8) != 0) {
+	  //FR_PROTO_HEX_DUMP(autn + 8, 8, "Received MAC_A");
+		fr_strerror_printf("MAC mismatch");
+		return -1;
+	}
+
+	return 0;
+}
+
+#ifdef TESTING_MILENAGE
+/*
+ *  cc milenage.c -g3 -Wall -DHAVE_DLFCN_H -DTESTING_MILENAGE -DWITH_TLS -I../../../../ -I../../../ -I ../base/ -I /usr/local/opt/openssl/include/ -include ../include/build.h -L /usr/local/opt/openssl/lib/ -l ssl -l crypto -l talloc -L ../../../../../build/lib/local/.libs/ -lfreeradius-server -lfreeradius-tls -lfreeradius-util -o test_milenage && ./test_milenage
+ */
+#include <freeradius-devel/util/acutest.h>
+
+void test_set_1(void)
+{
+	/*
+	 *	Inputs
+	 */
+	uint8_t ki[]		= { 0x46, 0x5b, 0x5c, 0xe8, 0xb1, 0x99, 0xb4, 0x9f,
+				    0xaa, 0x5f, 0x0a, 0x2e, 0xe2, 0x38, 0xa6, 0xbc };
+	uint8_t rand[]		= { 0x23, 0x55, 0x3c, 0xbe, 0x96, 0x37, 0xa8, 0x9d,
+				    0x21, 0x8a, 0xe6, 0x4d, 0xae, 0x47, 0xbf, 0x35  };
+	uint8_t sqn[]		= { 0xff, 0x9b, 0xb4, 0xd0, 0xb6, 0x07 };
+	uint8_t amf[]		= { 0xb9, 0xb9 };
+	uint8_t op[]		= { 0xcd, 0xc2, 0x02, 0xd5, 0x12, 0x3e, 0x20, 0xf6,
+				    0x2b, 0x6d, 0x67, 0x6a, 0xc7, 0x2c, 0xb3, 0x18 };
+	uint8_t opc[]		= { 0xcd, 0x63, 0xcb, 0x71, 0x95, 0x4a, 0x9f, 0x4e,
+				    0x48, 0xa5, 0x99, 0x4e, 0x37, 0xa0, 0x2b, 0xaf };
+
+	/*
+	 *	Outputs
+	 */
+	uint8_t opc_out[MILENAGE_OPC_SIZE];
+	uint8_t	mac_a_out[MILENAGE_MAC_A_SIZE];
+	uint8_t	mac_s_out[MILENAGE_MAC_S_SIZE];
+	uint8_t res_out[MILENAGE_RES_SIZE];
+	uint8_t ck_out[MILENAGE_CK_SIZE];
+	uint8_t ik_out[MILENAGE_IK_SIZE];
+	uint8_t ak_out[MILENAGE_AK_SIZE];
+	uint8_t ak_resync_out[MILENAGE_AK_SIZE];
+
+	/* function 1 */
+	uint8_t mac_a[]		= { 0x4a, 0x9f, 0xfa, 0xc3, 0x54, 0xdf, 0xaf, 0xb3 };
+	/* function 1* */
+	uint8_t mac_s[]		= { 0x01, 0xcf, 0xaf, 0x9e, 0xc4, 0xe8, 0x71, 0xe9 };
+	/* function 2 */
+	uint8_t res[]		= { 0xa5, 0x42, 0x11, 0xd5, 0xe3, 0xba, 0x50, 0xbf };
+	/* function 3 */
+	uint8_t ck[]		= { 0xb4, 0x0b, 0xa9, 0xa3, 0xc5, 0x8b, 0x2a, 0x05,
+				    0xbb, 0xf0, 0xd9, 0x87, 0xb2, 0x1b, 0xf8, 0xcb };
+	/* function 4 */
+	uint8_t ik[]		= { 0xf7, 0x69, 0xbc, 0xd7, 0x51, 0x04, 0x46, 0x04,
+			    	    0x12, 0x76, 0x72, 0x71, 0x1c, 0x6d, 0x34, 0x41 };
+	/* function 5 */
+	uint8_t ak[]		= { 0xaa, 0x68, 0x9c, 0x64, 0x83, 0x70 };
+	/* function 5* */
+	uint8_t ak_resync[]	= { 0x45, 0x1e, 0x8b, 0xec, 0xa4, 0x3b };
+
+	int ret = 0;
+
+/*
+	fr_debug_lvl = 4;
+*/
+	ret = milenage_opc_generate(opc_out, op, ki);
+	TEST_CHECK(ret == 0);
+
+	//FR_PROTO_HEX_DUMP(opc_out, sizeof(opc_out), "opc");
+
+	TEST_CHECK(memcmp(opc_out, opc, sizeof(opc_out)) == 0);
+
+	if ((milenage_f1(mac_a_out, mac_s_out, opc, ki, rand, sqn, amf) < 0) ||
+	    (milenage_f2345(res_out, ik_out, ck_out, ak_out, ak_resync_out, opc, ki, rand) < 0)) ret = -1;
+
+	//FR_PROTO_HEX_DUMP(mac_a, sizeof(mac_a_out), "mac_a");
+	//FR_PROTO_HEX_DUMP(mac_s, sizeof(mac_s_out), "mac_s");
+	//FR_PROTO_HEX_DUMP(ik_out, sizeof(ik_out), "ik");
+	//FR_PROTO_HEX_DUMP(ck_out, sizeof(ck_out), "ck");
+	//FR_PROTO_HEX_DUMP(res_out, sizeof(res_out), "res");
+	//FR_PROTO_HEX_DUMP(ak_out, sizeof(ak_out), "ak");
+	//FR_PROTO_HEX_DUMP(ak_resync_out, sizeof(ak_resync_out), "ak_resync");
+
+	TEST_CHECK(ret == 0);
+	TEST_CHECK(memcmp(mac_a_out, mac_a, sizeof(mac_a_out)) == 0);
+	TEST_CHECK(memcmp(mac_s_out, mac_s, sizeof(mac_s_out)) == 0);
+	TEST_CHECK(memcmp(res_out, res, sizeof(res_out)) == 0);
+	TEST_CHECK(memcmp(ck_out, ck, sizeof(ck_out)) == 0);
+	TEST_CHECK(memcmp(ik_out, ik, sizeof(ik_out)) == 0);
+	TEST_CHECK(memcmp(ak_out, ak, sizeof(ak_out)) == 0);
+	TEST_CHECK(memcmp(ak_resync, ak_resync, sizeof(ak_resync_out)) == 0);
+}
+
+void test_set_19(void)
+{
+	/*
+	 *	Inputs
+	 */
+	uint8_t ki[]		= { 0x51, 0x22, 0x25, 0x02, 0x14, 0xc3, 0x3e, 0x72,
+				    0x3a, 0x5d, 0xd5, 0x23, 0xfc, 0x14, 0x5f, 0xc0 };
+	uint8_t rand[]		= { 0x81, 0xe9, 0x2b, 0x6c, 0x0e, 0xe0, 0xe1, 0x2e,
+				    0xbc, 0xeb, 0xa8, 0xd9, 0x2a, 0x99, 0xdf, 0xa5 };
+	uint8_t sqn[]		= { 0x16, 0xf3, 0xb3, 0xf7, 0x0f, 0xc2 };
+	uint8_t amf[]		= { 0xc3, 0xab };
+	uint8_t op[]		= { 0xc9, 0xe8, 0x76, 0x32, 0x86, 0xb5, 0xb9, 0xff,
+				    0xbd, 0xf5, 0x6e, 0x12, 0x97, 0xd0, 0x88, 0x7b };
+	uint8_t opc[]		= { 0x98, 0x1d, 0x46, 0x4c, 0x7c, 0x52, 0xeb, 0x6e,
+				    0x50, 0x36, 0x23, 0x49, 0x84, 0xad, 0x0b, 0xcf };
+
+	/*
+	 *	Outputs
+	 */
+	uint8_t opc_out[MILENAGE_OPC_SIZE];
+	uint8_t	mac_a_out[MILENAGE_MAC_A_SIZE];
+	uint8_t	mac_s_out[MILENAGE_MAC_S_SIZE];
+	uint8_t res_out[MILENAGE_RES_SIZE];
+	uint8_t ck_out[MILENAGE_CK_SIZE];
+	uint8_t ik_out[MILENAGE_IK_SIZE];
+	uint8_t ak_out[MILENAGE_AK_SIZE];
+	uint8_t ak_resync_out[MILENAGE_AK_SIZE];
+
+	/* function 1 */
+	uint8_t mac_a[]		= { 0x2a, 0x5c, 0x23, 0xd1, 0x5e, 0xe3, 0x51, 0xd5 };
+	/* function 1* */
+	uint8_t mac_s[]		= { 0x62, 0xda, 0xe3, 0x85, 0x3f, 0x3a, 0xf9, 0xd2 };
+	/* function 2 */
+	uint8_t res[]		= { 0x28, 0xd7, 0xb0, 0xf2, 0xa2, 0xec, 0x3d, 0xe5 };
+	/* function 3 */
+	uint8_t ck[]		= { 0x53, 0x49, 0xfb, 0xe0, 0x98, 0x64, 0x9f, 0x94,
+				    0x8f, 0x5d, 0x2e, 0x97, 0x3a, 0x81, 0xc0, 0x0f };
+	/* function 4 */
+	uint8_t ik[]		= { 0x97, 0x44, 0x87, 0x1a, 0xd3, 0x2b, 0xf9, 0xbb,
+				    0xd1, 0xdd, 0x5c, 0xe5, 0x4e, 0x3e, 0x2e, 0x5a };
+	/* function 5 */
+	uint8_t ak[]		= { 0xad, 0xa1, 0x5a, 0xeb, 0x7b, 0xb8 };
+	/* function 5* */
+	uint8_t ak_resync[]	= { 0xd4, 0x61, 0xbc, 0x15, 0x47, 0x5d };
+
+	int ret = 0;
+
+/*
+	fr_debug_lvl = 4;
+*/
+
+	ret = milenage_opc_generate(opc_out, op, ki);
+	TEST_CHECK(ret == 0);
+
+	//FR_PROTO_HEX_DUMP(opc_out, sizeof(opc_out), "opc");
+
+	TEST_CHECK(memcmp(opc_out, opc, sizeof(opc_out)) == 0);
+
+	if ((milenage_f1(mac_a_out, mac_s_out, opc, ki, rand, sqn, amf) < 0) ||
+	    (milenage_f2345(res_out, ik_out, ck_out, ak_out, ak_resync_out, opc, ki, rand) < 0)) ret = -1;
+
+	//FR_PROTO_HEX_DUMP(mac_a, sizeof(mac_a_out), "mac_a");
+	//FR_PROTO_HEX_DUMP(mac_s, sizeof(mac_s_out), "mac_s");
+	//FR_PROTO_HEX_DUMP(ik_out, sizeof(ik_out), "ik");
+	//FR_PROTO_HEX_DUMP(ck_out, sizeof(ck_out), "ck");
+	//FR_PROTO_HEX_DUMP(res_out, sizeof(res_out), "res");
+	//FR_PROTO_HEX_DUMP(ak_out, sizeof(ak_out), "ak");
+	//FR_PROTO_HEX_DUMP(ak_resync_out, sizeof(ak_resync_out), "ak_resync");
+
+	TEST_CHECK(ret == 0);
+	TEST_CHECK(memcmp(mac_a_out, mac_a, sizeof(mac_a_out)) == 0);
+	TEST_CHECK(memcmp(mac_s_out, mac_s, sizeof(mac_s_out)) == 0);
+	TEST_CHECK(memcmp(res_out, res, sizeof(res_out)) == 0);
+	TEST_CHECK(memcmp(ck_out, ck, sizeof(ck_out)) == 0);
+	TEST_CHECK(memcmp(ik_out, ik, sizeof(ik_out)) == 0);
+	TEST_CHECK(memcmp(ak_out, ak, sizeof(ak_out)) == 0);
+	TEST_CHECK(memcmp(ak_resync, ak_resync, sizeof(ak_resync_out)) == 0);
+}
+
+TEST_LIST = {
+	{ "test_set_1",		test_set_1 },
+	{ "test_set_19",	test_set_19 },
+	{ NULL }
+};
+#endif
diff --git a/src/modules/rlm_wimax/milenage.h b/src/modules/rlm_wimax/milenage.h
new file mode 100644
index 0000000000..758383aba2
--- /dev/null
+++ b/src/modules/rlm_wimax/milenage.h
@@ -0,0 +1,128 @@
+#pragma once
+/**
+ * @file src/modules/rlm_wimax/milenage.h
+ * @brief 3GPP AKA - Milenage algorithm (3GPP TS 35.205, .206, .207, .208)
+ *
+ * This file implements an example authentication algorithm defined for 3GPP
+ * AKA. This can be used to implement a simple HLR/AuC into hlr_auc_gw to allow
+ * EAP-AKA to be tested properly with real USIM cards.
+ *
+ * This implementations assumes that the r1..r5 and c1..c5 constants defined in
+ * TS 35.206 are used, i.e., r1=64, r2=0, r3=32, r4=64, r5=96, c1=00..00,
+ * c2=00..01, c3=00..02, c4=00..04, c5=00..08. The block cipher is assumed to
+ * be AES (Rijndael).
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ *
+ * @copyright 2017 The FreeRADIUS server project
+ * @copyright 2006-2007 (j@w1.fi)
+ */
+#include <stddef.h>
+
+/*
+ *	Inputs
+ */
+#define MILENAGE_KI_SIZE	16		//!< Subscriber key.
+#define MILENAGE_OP_SIZE	16		//!< Operator code (unique to the operator)
+#define MILENAGE_OPC_SIZE	16		//!< Derived operator code (unique to the operator and subscriber).
+#define MILENAGE_AMF_SIZE	2		//!< Authentication management field.
+#define MILENAGE_SQN_SIZE	6		//!< Sequence number.
+#define MILENAGE_RAND_SIZE	16		//!< Random challenge.
+
+/*
+ *	UMTS Outputs
+ */
+#define MILENAGE_AK_SIZE	6		//!< Anonymisation key.
+#define MILENAGE_AUTN_SIZE	16		//!< Network authentication key.
+#define MILENAGE_IK_SIZE	16		//!< Integrity key.
+#define	MILENAGE_CK_SIZE	16		//!< Ciphering key.
+#define MILENAGE_RES_SIZE	8
+#define MILENAGE_AUTS_SIZE	14
+
+/*
+ *	GSM (COMP128-4) outputs
+ */
+#define MILENAGE_SRES_SIZE	4
+#define MILENAGE_KC_SIZE	8
+
+/** Copy a 48bit value from a 64bit integer into a uint8_t buff in big endian byte order
+ *
+ * There may be fast ways of doing this, but this is the *correct*
+ * way, and does not make assumptions about how integers are laid
+ * out in memory.
+ *
+ * @param[out] out      6 byte butter to store value.
+ * @param[in] i         integer value.
+ * @return pointer to out.
+ */
+static inline uint8_t *uint48_to_buff(uint8_t out[6], uint64_t i)
+{
+	out[0] = (i & 0xff0000000000) >> 40;
+	out[1] = (i & 0x00ff00000000) >> 32;
+	out[2] = (i & 0x0000ff000000) >> 24;
+	out[3] = (i & 0x000000ff0000) >> 16;
+	out[4] = (i & 0x00000000ff00) >> 8;
+	out[5] = (i & 0x0000000000ff);
+
+	return out;
+}
+
+/** Convert a 48bit big endian value into a unsigned 64bit integer
+ *
+ */
+static inline uint64_t uint48_from_buff(uint8_t const in[6])
+{
+	uint64_t i = 0;
+
+	i |= ((uint64_t)in[0]) << 40;
+	i |= ((uint64_t)in[1]) << 32;
+	i |= ((uint32_t)in[2]) << 24;
+	i |= ((uint32_t)in[3]) << 16;
+	i |= ((uint16_t)in[4]) << 8;
+	i |= in[5];
+
+	return i;
+}
+
+int	milenage_opc_generate(uint8_t opc[MILENAGE_OPC_SIZE],
+			      uint8_t const op[MILENAGE_OP_SIZE],
+			      uint8_t const ki[MILENAGE_KI_SIZE]);
+
+int	milenage_umts_generate(uint8_t autn[MILENAGE_AUTN_SIZE],
+			       uint8_t ik[MILENAGE_IK_SIZE],
+			       uint8_t ck[MILENAGE_CK_SIZE],
+			       uint8_t ak[MILENAGE_AK_SIZE],
+			       uint8_t res[MILENAGE_RES_SIZE],
+			       uint8_t const opc[MILENAGE_OPC_SIZE],
+			       uint8_t const amf[MILENAGE_AMF_SIZE],
+			       uint8_t const ki[MILENAGE_KI_SIZE],
+			       uint64_t sqn,
+			       uint8_t const rand[MILENAGE_RAND_SIZE]);
+
+int	milenage_auts(uint64_t *sqn,
+		      uint8_t const opc[MILENAGE_OPC_SIZE],
+		      uint8_t const ki[MILENAGE_KI_SIZE],
+		      uint8_t const rand[MILENAGE_RAND_SIZE],
+		      uint8_t const auts[MILENAGE_AUTS_SIZE]);
+
+void	milenage_gsm_from_umts(uint8_t sres[MILENAGE_SRES_SIZE],
+			       uint8_t kc[MILENAGE_KC_SIZE],
+			       uint8_t const ik[MILENAGE_IK_SIZE],
+			       uint8_t const ck[MILENAGE_CK_SIZE],
+			       uint8_t const res[MILENAGE_RES_SIZE]);
+
+int	milenage_gsm_generate(uint8_t sres[MILENAGE_SRES_SIZE], uint8_t kc[MILENAGE_KC_SIZE],
+			      uint8_t const opc[MILENAGE_OPC_SIZE],
+			      uint8_t const ki[MILENAGE_KI_SIZE],
+			      uint8_t const rand[MILENAGE_RAND_SIZE]);
+
+int	milenage_check(uint8_t ik[MILENAGE_IK_SIZE],
+		       uint8_t ck[MILENAGE_CK_SIZE],
+		       uint8_t res[MILENAGE_RES_SIZE],
+		       uint8_t auts[MILENAGE_AUTS_SIZE],
+		       uint8_t const opc[MILENAGE_OPC_SIZE],
+		       uint8_t const ki[MILENAGE_KI_SIZE],
+		       uint64_t sqn,
+		       uint8_t const rand[MILENAGE_RAND_SIZE],
+		       uint8_t const autn[MILENAGE_AUTN_SIZE]);
diff --git a/src/modules/rlm_wimax/rlm_wimax.c b/src/modules/rlm_wimax/rlm_wimax.c
index f0fb394fcd..d2125eb3a5 100644
--- a/src/modules/rlm_wimax/rlm_wimax.c
+++ b/src/modules/rlm_wimax/rlm_wimax.c
@@ -26,16 +26,43 @@ USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
 
 #include <freeradius-devel/radiusd.h>
 #include <freeradius-devel/modules.h>
+#include "milenage.h"
 
 #ifdef HAVE_OPENSSL_HMAC_H
 #include <openssl/hmac.h>
 #endif
 
+#include <freeradius-devel/openssl3.h>
+
+#define WIMAX_EPSAKA_RAND_SIZE	16
+#define WIMAX_EPSAKA_KI_SIZE	16
+#define WIMAX_EPSAKA_OPC_SIZE	16
+#define WIMAX_EPSAKA_AMF_SIZE	2
+#define WIMAX_EPSAKA_SQN_SIZE	6
+#define WIMAX_EPSAKA_MAC_A_SIZE	8
+#define WIMAX_EPSAKA_MAC_S_SIZE	8
+#define WIMAX_EPSAKA_XRES_SIZE	8
+#define WIMAX_EPSAKA_CK_SIZE	16
+#define WIMAX_EPSAKA_IK_SIZE	16
+#define WIMAX_EPSAKA_AK_SIZE	6
+#define WIMAX_EPSAKA_AK_RESYNC_SIZE	6
+#define WIMAX_EPSAKA_KK_SIZE	32
+#define WIMAX_EPSAKA_KS_SIZE	14
+#define WIMAX_EPSAKA_PLMN_SIZE	3
+#define WIMAX_EPSAKA_KASME_SIZE	32
+#define WIMAX_EPSAKA_AUTN_SIZE	16
+#define WIMAX_EPSAKA_AUTS_SIZE  14
+
 /*
  *	FIXME: Fix the build system to create definitions from names.
  */
 typedef struct rlm_wimax_t {
 	bool	delete_mppe_keys;
+
+	DICT_ATTR const	*resync_info;
+	DICT_ATTR const	*xres;
+	DICT_ATTR const	*autn;
+	DICT_ATTR const	*kasme;
 } rlm_wimax_t;
 
 /*
@@ -52,15 +79,37 @@ static const CONF_PARSER module_config[] = {
 	CONF_PARSER_TERMINATOR
 };
 
+/*
+ *	Print hex values in a readable format for debugging
+ *	Example:
+ *	FOO: 00 11 AA 22 00 FF
+ */
+static void rdebug_hex(REQUEST *request, char const *prefix, uint8_t const *data, int len)
+{
+	int i;
+	char buffer[256];	/* 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 * 3, sizeof(buffer) - i * 3, "%02x ", data[i]);
+	}
+
+	RDEBUG("%s %s", prefix, buffer);
+}
+#define RDEBUG_HEX if (rad_debug_lvl) rdebug_hex
+
 /*
  *	Find the named user in this modules database.  Create the set
  *	of attribute-value pairs to check and reply with for this user
  *	from the database. The authentication code only needs to check
  *	the password, the rest is done here.
  */
-static rlm_rcode_t CC_HINT(nonnull) mod_authorize(UNUSED void *instance, REQUEST *request)
+static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request)
 {
 	VALUE_PAIR *vp;
+	rlm_wimax_t *inst = instance;
 
 	/*
 	 *	Fix Calling-Station-Id.  Damn you, WiMAX!
@@ -93,10 +142,124 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authorize(UNUSED void *instance, REQUEST
 		return RLM_MODULE_OK;
 	}
 
+	/*
+	 *	Check for attr WiMAX-Re-synchronization-Info
+	 *	which contains the concatenation of RAND and AUTS
+	 *
+	 *	If it is present then we proceed to verify the SIM and
+	 *	extract the new value of SQN
+	 */
+	VALUE_PAIR *resync_info, *ki, *opc, *sqn, *rand;
+	int m_ret;
+
+	/* Look for the Re-synchronization-Info attribute in the request */
+	resync_info = fr_pair_find_by_da(request->packet->vps, inst->resync_info, TAG_ANY);
+	if (resync_info && (resync_info->vp_length < (WIMAX_EPSAKA_RAND_SIZE + WIMAX_EPSAKA_AUTS_SIZE))) {
+		RWDEBUG("Found request:WiMAX-Re-synchronization-Info with incorrect length: Ignoring it");
+		resync_info = NULL;
+	}
+
+	/*
+	 *	These are the private keys which should be added to the control
+	 *	list after looking them up in a database by IMSI
+	 *
+	 *	We grab them from the control list here
+	 */
+	ki = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_KI, 0, TAG_ANY);
+	if (ki && (ki->vp_length < MILENAGE_CK_SIZE)) {
+		RWDEBUG("Found config:WiMAX-SIM-Ki with incorrect length: Ignoring it");
+		ki = NULL;
+	}
+
+	opc = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_OPC, 0, TAG_ANY);
+	if (opc && (opc->vp_length < MILENAGE_IK_SIZE)) {
+		RWDEBUG("Found config:WiMAX-SIM-OPC with incorrect length: Ignoring it");
+		opc = NULL;
+	}
+
+	/* If we have resync info (RAND and AUTS), Ki and OPc then we can proceed */
+	if (resync_info && ki && opc) {
+		uint64_t sqn_bin;
+		uint8_t rand_bin[WIMAX_EPSAKA_RAND_SIZE];
+		uint8_t auts_bin[WIMAX_EPSAKA_AUTS_SIZE];
+
+		RDEBUG("Found WiMAX-Re-synchronization-Info. Proceeding with SQN resync");
+
+		/* Split Re-synchronization-Info into seperate RAND and AUTS */
+
+		memcpy(rand_bin, &resync_info->vp_octets[0], WIMAX_EPSAKA_RAND_SIZE);
+		memcpy(auts_bin, &resync_info->vp_octets[WIMAX_EPSAKA_RAND_SIZE], WIMAX_EPSAKA_AUTS_SIZE);
+
+		RDEBUG_HEX(request, "RAND  ", rand_bin, WIMAX_EPSAKA_RAND_SIZE);
+		RDEBUG_HEX(request, "AUTS  ", auts_bin, WIMAX_EPSAKA_AUTS_SIZE);
+
+		/*
+		 *	This procedure uses the secret keys Ki and OPc to authenticate
+		 *	the SIM and extract the SQN
+		 */
+		m_ret = milenage_auts(&sqn_bin, opc->vp_octets, ki->vp_octets, rand_bin, auts_bin);
+		
+		/*
+		 *	If the SIM verification fails then we can't go any further as
+		 *	we don't have the keys. And that probably means something bad
+		 *	is happening so we bail out now
+		 */
+		if (m_ret < 0) {
+			RDEBUG("SIM verification failed");
+		  	return RLM_MODULE_REJECT;
+		}
+
+		/*
+		 *	If we got this far it means have got a new SQN and RAND
+		 *	so we store them in:
+		 *	control:WiMAX-SIM-SQN
+		 *	control:WiMAX-SIM-RAND
+		 *
+		 *	From there they can be grabbed by unlang and used later
+		 */
+
+		/* SQN is six bytes so we extract what we need from the 64 bit variable */
+		uint8_t sqn_bin_arr[WIMAX_EPSAKA_SQN_SIZE] = {
+			(sqn_bin & 0x0000FF0000000000ull) >> 40,
+			(sqn_bin & 0x000000FF00000000ull) >> 32,
+			(sqn_bin & 0x00000000FF000000ull) >> 24,
+			(sqn_bin & 0x0000000000FF0000ull) >> 16,
+			(sqn_bin & 0x000000000000FF00ull) >>  8,
+			(sqn_bin & 0x00000000000000FFull) >>  0
+		};
+
+		/* Add SQN to control:WiMAX-SIM-SQN */
+		sqn = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_SQN, 0, TAG_ANY);
+		if (sqn && (sqn->vp_length < WIMAX_EPSAKA_SQN_SIZE)) {
+			RWDEBUG("Found config:WiMAX-SIM-SQN with incorrect length: Ignoring it");
+			sqn = NULL;
+		}
+
+		if (!sqn) {
+			MEM(sqn = pair_make_config("WiMAX-SIM-SQN", NULL, T_OP_SET));
+			fr_pair_value_memcpy(sqn, sqn_bin_arr, WIMAX_EPSAKA_SQN_SIZE);
+		}
+		RDEBUG_HEX(request, "SQN   ", sqn->vp_octets, WIMAX_EPSAKA_SQN_SIZE);
+
+		/* Add RAND to control:WiMAX-SIM-RAND */
+		rand = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_RAND, 0, TAG_ANY);
+		if (rand && (rand->vp_length < WIMAX_EPSAKA_RAND_SIZE)) {
+			RWDEBUG("Found config:WiMAX-SIM-RAND with incorrect length: Ignoring it");
+			rand = NULL;
+		}
+
+		if (!rand) {
+			MEM(rand = pair_make_config("WiMAX-SIM-RAND", NULL, T_OP_SET));
+			fr_pair_value_memcpy(rand, rand_bin, WIMAX_EPSAKA_RAND_SIZE);
+		}
+		RDEBUG_HEX(request, "RAND  ", rand->vp_octets, WIMAX_EPSAKA_RAND_SIZE);
+
+		return RLM_MODULE_UPDATED;
+	}
+
 	return RLM_MODULE_NOOP;
 }
 
-
 /*
  *	Massage the request before recording it or proxying it
  */
@@ -105,21 +268,14 @@ static rlm_rcode_t CC_HINT(nonnull) mod_preacct(void *instance, REQUEST *request
 	return mod_authorize(instance, request);
 }
 
-/*
- *	Write accounting information to this modules database.
- */
-static rlm_rcode_t CC_HINT(nonnull) mod_accounting(UNUSED void *instance, UNUSED REQUEST *request)
-{
-	return RLM_MODULE_OK;
-}
 
 /*
- *	Generate the keys after the user has been authenticated.
+ *	This function generates the keys for old style WiMAX (v1 to v2.0)
  */
-static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *request)
+static int mip_keys_generate(void *instance, REQUEST *request, VALUE_PAIR *msk, VALUE_PAIR *emsk)
 {
 	rlm_wimax_t *inst = instance;
-	VALUE_PAIR *msk, *emsk, *vp;
+	VALUE_PAIR *vp;
 	VALUE_PAIR *mn_nai, *ip, *fa_rk;
 	HMAC_CTX *hmac;
 	unsigned int rk1_len, rk2_len, rk_len;
@@ -128,13 +284,6 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 	uint8_t mip_rk_1[EVP_MAX_MD_SIZE], mip_rk_2[EVP_MAX_MD_SIZE];
 	uint8_t mip_rk[2 * EVP_MAX_MD_SIZE];
 
-	msk = fr_pair_find_by_num(request->reply->vps, PW_EAP_MSK, 0, TAG_ANY);
-	emsk = fr_pair_find_by_num(request->reply->vps, PW_EAP_EMSK, 0, TAG_ANY);
-	if (!msk || !emsk) {
-		RDEBUG("No EAP-MSK or EAP-EMSK.  Cannot create WiMAX keys");
-		return RLM_MODULE_NOOP;
-	}
-
 	/*
 	 *	If we delete the MS-MPPE-*-Key attributes, then add in
 	 *	the WiMAX-MSK so that the client has a key available.
@@ -143,10 +292,8 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 		fr_pair_delete_by_num(&request->reply->vps, 16, VENDORPEC_MICROSOFT, TAG_ANY);
 		fr_pair_delete_by_num(&request->reply->vps, 17, VENDORPEC_MICROSOFT, TAG_ANY);
 
-		vp = pair_make_reply("WiMAX-MSK", NULL, T_OP_EQ);
-		if (vp) {
-			fr_pair_value_memcpy(vp, msk->vp_octets, msk->vp_length);
-		}
+		MEM(vp = pair_make_reply("WiMAX-MSK", NULL, T_OP_EQ));
+		fr_pair_value_memcpy(vp, msk->vp_octets, msk->vp_length);
 	}
 
 	/*
@@ -162,6 +309,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 	 */
 	hmac = HMAC_CTX_new();
 	HMAC_Init_ex(hmac, emsk->vp_octets, emsk->vp_length, EVP_sha256(), NULL);
+	rk1_len = SHA256_DIGEST_LENGTH;
 
 	HMAC_Update(hmac, &usage_data[0], sizeof(usage_data));
 	HMAC_Final(hmac, &mip_rk_1[0], &rk1_len);
@@ -173,6 +321,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 
 	HMAC_Update(hmac, (uint8_t const *) &mip_rk_1, rk1_len);
 	HMAC_Update(hmac, &usage_data[0], sizeof(usage_data));
+	rk2_len = SHA256_DIGEST_LENGTH;
 	HMAC_Final(hmac, &mip_rk_2[0], &rk2_len);
 
 	memcpy(mip_rk, mip_rk_1, rk1_len);
@@ -185,6 +334,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 	HMAC_Init_ex(hmac, mip_rk, rk_len, EVP_sha256(), NULL);
 
 	HMAC_Update(hmac, (uint8_t const *) "SPI CMIP PMIP", 12);
+	rk1_len = SHA256_DIGEST_LENGTH;
 	HMAC_Final(hmac, &mip_rk_1[0], &rk1_len);
 
 	/*
@@ -195,16 +345,8 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 		   (mip_rk_1[2] << 8) | mip_rk_1[3]);
 	if (mip_spi < 256) mip_spi += 256;
 
-	if (rad_debug_lvl) {
-		int len = rk_len;
-		char buffer[512];
-
-		if (len > 128) len = 128; /* buffer size */
-
-		fr_bin2hex(buffer, mip_rk, len);
-		RDEBUG("MIP-RK = 0x%s", buffer);
-		RDEBUG("MIP-SPI = %08x", ntohl(mip_spi));
-	}
+	RDEBUG_HEX(request, "MIP-RK ", mip_rk, rk_len);
+	RDEBUG("MIP-SPI = %08x", ntohl(mip_spi));
 
 	/*
 	 *	FIXME: Perform SPI collision prevention
@@ -250,6 +392,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 		HMAC_Update(hmac, (uint8_t const *) "PMIP4 MN HA", 11);
 		HMAC_Update(hmac, (uint8_t const *) &ip->vp_ipaddr, 4);
 		HMAC_Update(hmac, (uint8_t const *) &mn_nai->vp_strvalue, mn_nai->vp_length);
+		rk1_len = SHA1_DIGEST_LENGTH;
 		HMAC_Final(hmac, &mip_rk_1[0], &rk1_len);
 
 		/*
@@ -300,6 +443,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 		HMAC_Update(hmac, (uint8_t const *) "CMIP4 MN HA", 11);
 		HMAC_Update(hmac, (uint8_t const *) &ip->vp_ipaddr, 4);
 		HMAC_Update(hmac, (uint8_t const *) &mn_nai->vp_strvalue, mn_nai->vp_length);
+		rk1_len = SHA1_DIGEST_LENGTH;
 		HMAC_Final(hmac, &mip_rk_1[0], &rk1_len);
 
 		/*
@@ -350,6 +494,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 		HMAC_Update(hmac, (uint8_t const *) "CMIP6 MN HA", 11);
 		HMAC_Update(hmac, (uint8_t const *) &ip->vp_ipv6addr, 16);
 		HMAC_Update(hmac, (uint8_t const *) &mn_nai->vp_strvalue, mn_nai->vp_length);
+		rk1_len = SHA1_DIGEST_LENGTH;
 		HMAC_Final(hmac, &mip_rk_1[0], &rk1_len);
 
 		/*
@@ -396,6 +541,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 
 		HMAC_Update(hmac, (uint8_t const *) "FA-RK", 5);
 
+		rk1_len = SHA1_DIGEST_LENGTH;
 		HMAC_Final(hmac, &mip_rk_1[0], &rk1_len);
 
 		fr_pair_value_memcpy(fa_rk, &mip_rk_1[0], rk1_len);
@@ -455,6 +601,221 @@ static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *reque
 	return RLM_MODULE_UPDATED;
 }
 
+/*
+ *	Generate the EPS-AKA authentication vector
+ *
+ *	These are the keys needed for new style WiMAX (LTE / 3gpp authentication),
+ 	for WiMAX v2.1
+ */
+static rlm_rcode_t aka_keys_generate(REQUEST *request, rlm_wimax_t const *inst, VALUE_PAIR *ki, VALUE_PAIR *opc,
+				     VALUE_PAIR *amf, VALUE_PAIR *sqn, VALUE_PAIR *plmn)
+{  
+	size_t i;
+	VALUE_PAIR *rand_previous, *rand, *xres, *autn, *kasme;
+
+	/*
+	 *	For most authentication requests we need to generate a fresh RAND
+	 *
+	 *	The exception is after SQN re-syncronisation - in this case we
+	 *	get RAND in the request, and this module if called in authorize should
+	 *	have put it in control:WiMAX-SIM-RAND so we can grab it from there)
+	 */
+	rand_previous = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_RAND, 0, TAG_ANY);
+	if (rand_previous && (rand_previous->vp_length < WIMAX_EPSAKA_RAND_SIZE)) {
+		RWDEBUG("Found config:WiMAX-SIM-Rand with incorrect size.  Ignoring it.");
+		rand_previous = NULL;
+	}
+
+	MEM(rand = pair_make_reply("WiMAX-E-UTRAN-Vector-RAND", NULL, T_OP_SET));
+	if (!rand_previous) {
+		uint32_t lvalue;
+		uint8_t buffer[WIMAX_EPSAKA_RAND_SIZE];
+
+		for (i = 0; i < (WIMAX_EPSAKA_RAND_SIZE / 4); i++) {
+			lvalue = fr_rand();
+			memcpy(buffer + i * 4, &lvalue, sizeof(lvalue));
+		}
+
+		fr_pair_value_memcpy(rand, buffer, WIMAX_EPSAKA_RAND_SIZE);
+
+	} else {
+		fr_pair_value_memcpy(rand, rand_previous->vp_octets, WIMAX_EPSAKA_RAND_SIZE);
+	}
+
+   	/*
+	 *	Feed AMF, Ki, SQN and RAND into the Milenage algorithm (f1, f2, f3, f4, f5)
+	 *	which returns AUTN, AK, CK, IK, XRES.
+	 */
+	uint8_t xres_bin[WIMAX_EPSAKA_XRES_SIZE];
+	uint8_t ck_bin[WIMAX_EPSAKA_CK_SIZE];
+	uint8_t ik_bin[WIMAX_EPSAKA_IK_SIZE];
+	uint8_t ak_bin[WIMAX_EPSAKA_AK_SIZE];
+	uint8_t autn_bin[WIMAX_EPSAKA_AUTN_SIZE];
+
+	/* But first convert uint8 SQN to uint64 */
+	uint64_t sqn_bin = 0x0000000000000000;
+	for (i = 0; i < sqn->vp_length; ++i) sqn_bin = (sqn_bin << 8) | sqn->vp_octets[i];
+
+	if (!opc || (opc->vp_length < MILENAGE_OPC_SIZE)) {
+		RWDEBUG("Found config:WiMAX-SIM-OPC with incorrect size.  Ignoring it");
+		return RLM_MODULE_NOOP;
+	}
+	if (!amf || (amf->vp_length < MILENAGE_AMF_SIZE)) {
+		RWDEBUG("Found config:WiMAX-SIM-AMF with incorrect size.  Ignoring it");
+		return RLM_MODULE_NOOP;
+	}
+	if (!ki || (ki->vp_length < MILENAGE_KI_SIZE)) {
+		RWDEBUG("Found config:WiMAX-SIM-KI with incorrect size.  Ignoring it");
+		return RLM_MODULE_NOOP;
+	}
+
+	/* Call milenage */
+	milenage_umts_generate(autn_bin, ik_bin, ck_bin, ak_bin, xres_bin, opc->vp_octets,
+			       amf->vp_octets, ki->vp_octets, sqn_bin, rand->vp_octets);
+
+	/*
+	 *	Now we genertate KASME
+	 *
+	 *	Officially described in 33401-g30.doc section A.2
+	 *	But an easier to read explanation can be found at:
+	 *	https://medium.com/uw-ictd/lte-authentication-2d0810a061ec
+	 *
+	*/
+
+	/* k = CK || IK */
+	uint8_t kk_bin[WIMAX_EPSAKA_KK_SIZE];
+	memcpy(kk_bin, ck_bin, sizeof(ck_bin));
+	memcpy(kk_bin + sizeof(ck_bin), ik_bin, sizeof(ik_bin));
+
+	/* Initialize a 14 byte buffer s */
+	uint8_t ks_bin[WIMAX_EPSAKA_KS_SIZE];
+
+	/* Assign the first byte of s as 0x10 */
+	ks_bin[0] = 0x10;
+      
+	/* Copy the 3 bytes of PLMN into s */
+	memcpy(ks_bin + 1, plmn->vp_octets, 3);
+
+	/* Assign 5th and 6th byte as 0x00 and 0x03 */
+	ks_bin[4] = 0x00;
+	ks_bin[5] = 0x03;
+      
+	/* Assign the next 6 bytes as SQN XOR AK */
+	for (i = 0; i < 6; i++) {
+		ks_bin[i+6] = sqn->vp_octets[i] ^ ak_bin[i];
+	}
+      
+	/* Assign the last two bytes as 0x00 and 0x06 */
+	ks_bin[12] = 0x00;
+	ks_bin[13] = 0x06;
+
+	/* Perform an HMAC-SHA256 using Key k from step 1 and s as the message. */
+	uint8_t kasme_bin[WIMAX_EPSAKA_KASME_SIZE];
+	HMAC_CTX *hmac;
+	unsigned int kasme_len = sizeof(kasme_bin);
+
+	hmac = HMAC_CTX_new();
+	HMAC_Init_ex(hmac, kk_bin, sizeof(kk_bin), EVP_sha256(), NULL);
+	HMAC_Update(hmac, ks_bin, sizeof(ks_bin));
+	kasme_len = SHA256_DIGEST_LENGTH;
+	HMAC_Final(hmac, &kasme_bin[0], &kasme_len);
+	HMAC_CTX_free(hmac);
+
+	/*
+	 *	Add reply attributes XRES, AUTN and KASME (RAND we added earlier)
+	 *
+	 *	Note that we can't call fr_pair_find_by_num(), as
+	 *	these attributes are buried deep inside of the WiMAX
+	 *	hierarchy.
+	 */
+	xres = fr_pair_find_by_da(request->reply->vps, inst->xres, TAG_ANY);
+	if (!xres) {
+		MEM(xres = pair_make_reply("WiMAX-E-UTRAN-Vector-XRES", NULL, T_OP_SET));
+		fr_pair_value_memcpy(xres, xres_bin, WIMAX_EPSAKA_XRES_SIZE);
+	}
+
+	autn = fr_pair_find_by_da(request->reply->vps, inst->autn, TAG_ANY);
+	if (!autn) {
+		MEM(autn = pair_make_reply("WiMAX-E-UTRAN-Vector-AUTN", NULL, T_OP_SET));
+		fr_pair_value_memcpy(autn, autn_bin, WIMAX_EPSAKA_AUTN_SIZE);
+	}
+
+	kasme = fr_pair_find_by_da(request->reply->vps, inst->kasme, TAG_ANY);
+	if (!kasme) {
+		MEM(kasme = pair_make_reply("WiMAX-E-UTRAN-Vector-KASME", NULL, T_OP_SET));
+		fr_pair_value_memcpy(kasme, kasme_bin, WIMAX_EPSAKA_KASME_SIZE);
+	}
+
+	/* Print keys to log for debugging */
+	if (rad_debug_lvl) {
+		RDEBUG("-------- Milenage in --------");
+		RDEBUG_HEX(request, "OPc   ", opc->vp_octets, opc->vp_length);
+		RDEBUG_HEX(request, "Ki    ", ki->vp_octets, ki->vp_length);
+		RDEBUG_HEX(request, "RAND  ", rand->vp_octets, rand->vp_length);
+		RDEBUG_HEX(request, "SQN   ", sqn->vp_octets, sqn->vp_length);
+		RDEBUG_HEX(request, "AMF   ", amf->vp_octets, amf->vp_length);
+		RDEBUG("-------- Milenage out -------");
+		RDEBUG_HEX(request, "XRES  ", xres->vp_octets, xres->vp_length);
+		RDEBUG_HEX(request, "Ck    ", ck_bin, sizeof(ck_bin));
+		RDEBUG_HEX(request, "Ik    ", ik_bin, sizeof(ik_bin));
+		RDEBUG_HEX(request, "Ak    ", ak_bin, sizeof(ak_bin));
+		RDEBUG_HEX(request, "AUTN  ", autn->vp_octets, autn->vp_length);
+		RDEBUG("-----------------------------");
+		RDEBUG_HEX(request, "Kk    ", kk_bin, sizeof(kk_bin));
+		RDEBUG_HEX(request, "Ks    ", ks_bin, sizeof(ks_bin));
+		RDEBUG_HEX(request, "KASME ", kasme->vp_octets, kasme->vp_length);
+	}
+
+	return RLM_MODULE_UPDATED;
+}
+
+/*
+ *	Generate the keys after the user has been authenticated.
+ */
+static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *request)
+{
+	VALUE_PAIR *msk, *emsk, *ki, *opc, *amf, *sqn, *plmn;
+
+	/*
+	 *	If we have MSK and EMSK then assume we want MIP keys
+	 *	Else if we have the SIM keys then we want the EPS-AKA vector
+	 */
+
+	msk = fr_pair_find_by_num(request->reply->vps, PW_EAP_MSK, 0, TAG_ANY);
+	emsk = fr_pair_find_by_num(request->reply->vps, PW_EAP_EMSK, 0, TAG_ANY);
+
+	if (msk && emsk) {
+		RDEBUG("MSK and EMSK found.  Generating MIP keys");
+		return mip_keys_generate(instance, request, msk, emsk);
+	}
+
+	ki = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_KI, 0, TAG_ANY);
+	opc = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_OPC, 0, TAG_ANY);
+	amf = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_AMF, 0, TAG_ANY);
+	sqn = fr_pair_find_by_num(request->config, PW_WIMAX_SIM_SQN, 0, TAG_ANY);
+	plmn = fr_pair_find_by_num(request->packet->vps, 146, VENDORPEC_WIMAX, TAG_ANY);
+	
+	if (ki && opc && amf && sqn && plmn) {
+		RDEBUG("AKA attributes found.  Generating AKA keys.");
+		return aka_keys_generate(request, instance, ki, opc, amf, sqn, plmn);
+	}
+
+	RDEBUG("Input keys not found.  Cannot create WiMAX keys");
+	return RLM_MODULE_NOOP;
+}
+
+
+static int mod_instantiate(UNUSED CONF_SECTION *conf, void *instance)
+{
+	rlm_wimax_t *inst = instance;
+      
+	inst->resync_info = dict_attrbyname("WiMAX-Re-synchronization-Info");
+	inst->xres = dict_attrbyname("WiMAX-E-UTRAN-Vector-XRES");
+	inst->autn = dict_attrbyname("WiMAX-E-UTRAN-Vector-AUTN");
+	inst->kasme = dict_attrbyname("WiMAX-E-UTRAN-Vector-KASME");
+
+	return 0;
+}
 
 /*
  *	The module name should be the only globally exported symbol.
@@ -472,10 +833,10 @@ module_t rlm_wimax = {
 	.type		= RLM_TYPE_THREAD_SAFE,
 	.inst_size	= sizeof(rlm_wimax_t),
 	.config		= module_config,
+	.instantiate	= mod_instantiate,
 	.methods = {
 		[MOD_AUTHORIZE]		= mod_authorize,
 		[MOD_PREACCT]		= mod_preacct,
-		[MOD_ACCOUNTING]	= mod_accounting,
 		[MOD_POST_AUTH]		= mod_post_auth
 	},
 };
diff --git a/src/tests/keywords/md4 b/src/tests/keywords/md4
new file mode 100644
index 0000000000..7e9b1ffcdf
--- /dev/null
+++ b/src/tests/keywords/md4
@@ -0,0 +1,58 @@
+#
+# PRE: update if
+#
+update reply {
+	Filter-Id := "filter"
+}
+
+update {
+       control:Cleartext-Password := 'hello'
+       request:Tmp-String-0 := "This is a string\n"
+       request:Tmp-Octets-0 := 0x000504030201
+       request:Tmp-String-1 := "what do ya want for nothing?"
+       request:Tmp-String-2 := "Jefe"
+}
+
+#
+#  Put "This is a string" into a file and call "md5sum" on it.
+#  You should get this string.
+#
+if ("%{md4:This is a string\n}" != '1f60d5cd85e17bfbdda7c923822f060c') {
+	update reply {
+		Filter-Id += 'fail 1'
+	}
+}
+
+if ("%{md4:&Tmp-String-0}" != '1f60d5cd85e17bfbdda7c923822f060c') {
+	update reply {
+		Filter-Id += 'fail 2'
+	}
+}
+
+if ("%{md4:&request:Tmp-String-0}" != '1f60d5cd85e17bfbdda7c923822f060c') {
+	update reply {
+		Filter-Id += 'fail 3'
+	}
+}
+
+if ("%{md4:%{request:Tmp-String-0}}" != '1f60d5cd85e17bfbdda7c923822f060c') {
+	update reply {
+		Filter-Id += 'fail 4'
+	}
+}
+
+#
+#  MD4 should also be able to cope with references to octet attributes
+#
+if ("%{md4:&request:Tmp-Octets-0}" != 'ac3ed17b3cf19ec38352ec534a932fc6') {
+	update reply {
+		Filter-Id += 'fail 5'
+	}
+}
+
+if ("%{md4:&Tmp-String-1}" != 'f7b44afb9cfdc877aa99d44654fe808b') {
+	update reply {
+		Filter-Id += 'fail 6'
+	}
+}
+