PAP: adding support for OpenLDAP and 389ds PBKDF2 passwords

Oliver Lorenz dev at lrnz.at
Mon Jun 3 10:11:56 UTC 2024


Hi,

while testing LDAP authentication with FreeIPA, I noticed PAP complained 
about no "known good" password found for the user. After looking into 
it, I saw that the Password.With-Header attribute has an unknown header. 
PAP rewrote it to Cleartext and obviously failed to verify it.

Below is a patch to make authentication work. The additional password 
type was necessary because password_process_header in password.c always 
strips the header it finds, which sucks for OpenLDAP and 389ds passwords 
because they encode the hash algorithm there.

I would really appreciate it if somebody could provide some feedback. 
Since it's such a massive codebase, chances are that I'm doing it (very) 
wrong.

Thanks,
Oliver


---
  .../dictionary.freeradius.internal.password   |  2 +
  src/lib/server/password.c                     | 18 ++++++-
  src/modules/rlm_pap/rlm_pap.c                 | 49 +++++++++++++++++++
  src/tests/modules/pap/pbkdf2_389ds_sha1.attrs | 11 +++++
  .../modules/pap/pbkdf2_389ds_sha1.unlang      | 17 +++++++
  .../modules/pap/pbkdf2_389ds_sha256.attrs     | 11 +++++
  .../modules/pap/pbkdf2_389ds_sha256.unlang    | 17 +++++++
  .../modules/pap/pbkdf2_389ds_sha512.attrs     | 11 +++++
  .../modules/pap/pbkdf2_389ds_sha512.unlang    | 17 +++++++
  9 files changed, 151 insertions(+), 2 deletions(-)
  create mode 100644 src/tests/modules/pap/pbkdf2_389ds_sha1.attrs
  create mode 100644 src/tests/modules/pap/pbkdf2_389ds_sha1.unlang
  create mode 100644 src/tests/modules/pap/pbkdf2_389ds_sha256.attrs
  create mode 100644 src/tests/modules/pap/pbkdf2_389ds_sha256.unlang
  create mode 100644 src/tests/modules/pap/pbkdf2_389ds_sha512.attrs
  create mode 100644 src/tests/modules/pap/pbkdf2_389ds_sha512.unlang

diff --git 
a/share/dictionary/freeradius/dictionary.freeradius.internal.password 
b/share/dictionary/freeradius/dictionary.freeradius.internal.password
index 2d184710ae..4e9ed86539 100644
--- a/share/dictionary/freeradius/dictionary.freeradius.internal.password
+++ b/share/dictionary/freeradius/dictionary.freeradius.internal.password
@@ -64,6 +64,8 @@ ATTRIBUTE	SSHA3-256				28	octets
  ATTRIBUTE	SSHA3-384				29	octets
  ATTRIBUTE	SSHA3-512				30	octets

+ATTRIBUTE	PBKDF2-389DS				31	octets
+
  END-TLV		Password

  #  TOTP passwords and secrets
diff --git a/src/lib/server/password.c b/src/lib/server/password.c
index 669c815efe..fe42a76890 100644
--- a/src/lib/server/password.c
+++ b/src/lib/server/password.c
@@ -73,6 +73,8 @@ typedef struct {
  						///< attribute using the hex/base64 decoders.
  	bool			always_allow;	//!< Always allow processing of this attribute, 
irrespective
  						///< of what the caller says.
+	bool			retain_header;	//!< Do not strip header from password string before
+						///< passing it to the authentication module
  } password_info_t;

  static fr_dict_t const *dict_freeradius = NULL;
@@ -112,6 +114,7 @@ static fr_dict_attr_t const *attr_ssha3_384;
  static fr_dict_attr_t const *attr_ssha3_512;

  static fr_dict_attr_t const *attr_pbkdf2;
+static fr_dict_attr_t const *attr_pbkdf2_389ds;
  static fr_dict_attr_t const *attr_lm;
  static fr_dict_attr_t const *attr_nt;
  static fr_dict_attr_t const *attr_ns_mta_md5;
@@ -160,6 +163,7 @@ fr_dict_attr_autoload_t password_dict_attr[] = {
  	{ .out = &attr_ssha3_512, .name = "Password.SSHA3-512", .type = 
FR_TYPE_OCTETS, .dict = &dict_freeradius },

  	{ .out = &attr_pbkdf2, .name = "Password.PBKDF2", .type = 
FR_TYPE_OCTETS, .dict = &dict_freeradius },
+	{ .out = &attr_pbkdf2_389ds, .name = "Password.PBKDF2-389DS", .type = 
FR_TYPE_OCTETS, .dict = &dict_freeradius },
  	{ .out = &attr_lm, .name = "Password.LM", .type = FR_TYPE_OCTETS, 
.dict = &dict_freeradius },
  	{ .out = &attr_nt, .name = "Password.NT", .type = FR_TYPE_OCTETS, 
.dict = &dict_freeradius },
  	{ .out = &attr_ns_mta_md5, .name = "Password.NS-MTA-MD5", .type = 
FR_TYPE_STRING, .dict = &dict_freeradius },
@@ -205,6 +209,9 @@ static fr_table_num_sorted_t const 
password_header_table[] = {
  	{ L("{ns-mta-md5}"),			FR_NS_MTA_MD5	},
  	{ L("{nt}"),				FR_NT		},
  	{ L("{nthash}"),			FR_NT		},
+	{ L("{pbkdf2-sha1}"),			FR_PBKDF2_389DS	},
+	{ L("{pbkdf2-sha256}"),			FR_PBKDF2_389DS	},
+	{ L("{pbkdf2-sha512}"),			FR_PBKDF2_389DS	},

  #ifdef HAVE_OPENSSL_EVP_H
  	{ L("{sha224}"),			FR_SHA2	},
@@ -285,6 +292,11 @@ static password_info_t password_info[] = {
  						.type = PASSWORD_HASH_VARIABLE,
  						.da = &attr_pbkdf2
  					},
+	[FR_PBKDF2_389DS]		= {
+						.type = PASSWORD_HASH_VARIABLE,
+						.da = &attr_pbkdf2_389ds,
+						.retain_header = true
+					},
  	[FR_SHA1]			= {
  						.type = PASSWORD_HASH,
  						.da = &attr_sha1,
@@ -681,11 +693,13 @@ do_header:
  			goto bad_header;
  		}

-		p = q + 1;
-
  		if (!fr_cond_assert(known_good->da->attr < 
NUM_ELEMENTS(password_info))) return NULL;
  		info = &password_info[attr];

+		if (!info->retain_header) {
+			p = q + 1;
+		}
+
  		MEM(new = fr_pair_afrom_da(ctx, *(info->da)));
  		switch ((*(info->da))->type) {
  		case FR_TYPE_OCTETS:
diff --git a/src/modules/rlm_pap/rlm_pap.c b/src/modules/rlm_pap/rlm_pap.c
index 67a363fb7d..216c68ddf5 100644
--- a/src/modules/rlm_pap/rlm_pap.c
+++ b/src/modules/rlm_pap/rlm_pap.c
@@ -682,6 +682,54 @@ finish:
  	RETURN_MODULE_RCODE(rcode);
  }

+static inline unlang_action_t CC_HINT(nonnull) 
pap_auth_pbkdf2_389ds(rlm_rcode_t *p_result,
+								     UNUSED rlm_pap_t const *inst,
+								     request_t *request,
+								     fr_pair_t const *known_good,
+								     fr_value_box_t const *password)
+{
+	uint8_t const *p = known_good->vp_octets, *q, *end = p + 
known_good->vp_length;
+
+	/*
+	 *	OpenLDAP and 389ds carry algorithm information in
+	 *	the userPassword header, so make sure it's there.
+	 *
+	 *	Message format:
+	 *	  OpenLDAP: {PBKDF2-<digest>}<rounds>$<alt_b64_salt>$<alt_b64_hash>
+	 *	     389ds: {PBKDF2-<digest>}<rounds>$<b64_salt>$<b64_hash>
+	 *
+	 *	The format is almost identical to Python's passlib.
+	 *	If we advance the pointer a bit and use '}' as scheme separator,
+	 *	pap_auth_pbkdf2_parse will do the rest for us
+	 */
+
+	if (end - p < 2) {
+		REDEBUG("Password.PBKDF2-389DS too short");
+		RETURN_MODULE_INVALID;
+	}
+
+	if (*p != '{') {
+		REDEBUG("Password.PBKDF2-389DS has invalid header");
+		RETURN_MODULE_INVALID;
+	}
+
+	q = memchr(p, '}', end - p);
+	if (!q || q - p < 2) {
+		REDEBUG("Password.PBKDF2-389DS has invalid header");
+		RETURN_MODULE_INVALID;
+	}
+
+	if ((size_t)(q - p) > sizeof("{PBKDF2-") && strncasecmp((char const *) 
p, "{PBKDF2-", 8) == 0) {
+		p += sizeof("{PBKDF2-") - 1;
+		return pap_auth_pbkdf2_parse(p_result, request, p, end - p,
+					     pbkdf2_passlib_names, pbkdf2_passlib_names_len,
+					     '}', '$', '$', false, password);
+	}
+
+	REDEBUG("Can't determine format of Password.PBKDF2-389DS");
+	RETURN_MODULE_INVALID;
+}
+
  static inline unlang_action_t CC_HINT(nonnull) 
pap_auth_pbkdf2(rlm_rcode_t *p_result,
  							       UNUSED rlm_pap_t const *inst,
  							       request_t *request,
@@ -870,6 +918,7 @@ static const pap_auth_func_t auth_func_table[] = {

  #ifdef HAVE_OPENSSL_EVP_H
  	[FR_PBKDF2]	= pap_auth_pbkdf2,
+	[FR_PBKDF2_389DS] = pap_auth_pbkdf2_389ds,
  	[FR_SHA2]	= pap_auth_dummy,
  	[FR_SHA2_224]	= pap_auth_sha2_224,
  	[FR_SHA2_256]	= pap_auth_sha2_256,
diff --git a/src/tests/modules/pap/pbkdf2_389ds_sha1.attrs 
b/src/tests/modules/pap/pbkdf2_389ds_sha1.attrs
new file mode 100644
index 0000000000..74119f6a78
--- /dev/null
+++ b/src/tests/modules/pap/pbkdf2_389ds_sha1.attrs
@@ -0,0 +1,11 @@
+#
+#  Input packet
+#
+Packet-Type = Access-Request
+User-Name = 'pbkdf2_389ds_sha1'
+User-Password = 'password'
+
+#
+#  Expected answer
+#
+Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkdf2_389ds_sha1.unlang 
b/src/tests/modules/pap/pbkdf2_389ds_sha1.unlang
new file mode 100644
index 0000000000..22fd74c7bc
--- /dev/null
+++ b/src/tests/modules/pap/pbkdf2_389ds_sha1.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+	test_pass
+	return
+}
+
+if (&User-Name == 'pbkdf2_389ds_sha1') {
+	&control.Password.With-Header := 
'{PBKDF2-SHA1}10000$kX7ZFznyliHoBZ+7ZIIAI1yH+V9yZg6j$35n7xsdaw763OdDgKGSMsJUWz8s='
+
+	pap.authorize
+	pap.authenticate
+
+	if (ok) {
+		test_pass
+	} else {
+		test_fail
+	}
+}
diff --git a/src/tests/modules/pap/pbkdf2_389ds_sha256.attrs 
b/src/tests/modules/pap/pbkdf2_389ds_sha256.attrs
new file mode 100644
index 0000000000..f1a566a94c
--- /dev/null
+++ b/src/tests/modules/pap/pbkdf2_389ds_sha256.attrs
@@ -0,0 +1,11 @@
+#
+#  Input packet
+#
+Packet-Type = Access-Request
+User-Name = 'pbkdf2_389ds_sha256'
+User-Password = 'password'
+
+#
+#  Expected answer
+#
+Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkdf2_389ds_sha256.unlang 
b/src/tests/modules/pap/pbkdf2_389ds_sha256.unlang
new file mode 100644
index 0000000000..b9cf455ad9
--- /dev/null
+++ b/src/tests/modules/pap/pbkdf2_389ds_sha256.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+	test_pass
+	return
+}
+
+if (&User-Name == 'pbkdf2_389ds_sha256') {
+	&control.Password.With-Header := 
'{PBKDF2-SHA256}10000$kX7ZFznyliHoBZ+7ZIIAI1yH+V9yZg6j$kH2xGFdC+rS5+tjPnTXj/pGK0eFXTez2QqF2QZhunBM='
+
+	pap.authorize
+	pap.authenticate
+
+	if (ok) {
+		test_pass
+	} else {
+		test_fail
+	}
+}
diff --git a/src/tests/modules/pap/pbkdf2_389ds_sha512.attrs 
b/src/tests/modules/pap/pbkdf2_389ds_sha512.attrs
new file mode 100644
index 0000000000..15cbd64dc3
--- /dev/null
+++ b/src/tests/modules/pap/pbkdf2_389ds_sha512.attrs
@@ -0,0 +1,11 @@
+#
+#  Input packet
+#
+Packet-Type = Access-Request
+User-Name = 'pbkdf2_389ds_sha512'
+User-Password = 'password'
+
+#
+#  Expected answer
+#
+Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkdf2_389ds_sha512.unlang 
b/src/tests/modules/pap/pbkdf2_389ds_sha512.unlang
new file mode 100644
index 0000000000..9444d690ea
--- /dev/null
+++ b/src/tests/modules/pap/pbkdf2_389ds_sha512.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+	test_pass
+	return
+}
+
+if (&User-Name == 'pbkdf2_389ds_sha512') {
+	&control.Password.With-Header := 
'{PBKDF2-SHA512}10000$kX7ZFznyliHoBZ+7ZIIAI1yH+V9yZg6j$kyzz433C/pdvdkzPTjwi8wr6cHNTFD0dCKuUkKVMmSgilGBDQjNxfDQwZzFBiPgChQJTFAm8oThm65xrLGFnAg=='
+
+	pap.authorize
+	pap.authenticate
+
+	if (ok) {
+		test_pass
+	} else {
+		test_fail
+	}
+}
-- 


More information about the Freeradius-Devel mailing list