]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
Move password normalisation into utility functions, and call those from rlm_chap
authorArran Cudbard-Bell <a.cudbardb@freeradius.org>
Mon, 29 Jul 2019 12:38:29 +0000 (21:38 +0900)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Mon, 29 Jul 2019 12:38:29 +0000 (21:38 +0900)
This allows Password-With-Header to be used with rlm_chap, and in future, other modules, without calling rlm_pap first.

src/lib/server/all.mk
src/lib/server/base.h
src/lib/server/password.c [new file with mode: 0644]
src/lib/server/password.h [new file with mode: 0644]
src/modules/rlm_chap/rlm_chap.c
src/modules/rlm_pap/rlm_pap.c

index f6eec2d40e4f317d20cf0189b70ad5755c9fb871..c36067ea07e6557b039bb9e7dbf2deb924479647 100644 (file)
@@ -23,6 +23,7 @@ SOURCES       := \
        module.c \
        paircmp.c \
        pairmove.c \
+       password.c \
        pool.c \
        rcode.c \
        regex.c \
index 297ed3cfb901e30f5ac9294698c3aeddb7fd5e5f..f3c8a20f113596d6deb484ab7b4af5d829c4566c 100644 (file)
@@ -51,6 +51,7 @@ RCSIDH(base_h, "$Id$")
 #include <freeradius-devel/server/pair.h>
 #include <freeradius-devel/server/paircmp.h>
 #include <freeradius-devel/server/pairmove.h>
+#include <freeradius-devel/server/password.h>
 #include <freeradius-devel/server/cond.h>
 #include <freeradius-devel/server/pool.h>
 #include <freeradius-devel/server/protocol.h>
diff --git a/src/lib/server/password.c b/src/lib/server/password.c
new file mode 100644 (file)
index 0000000..11dd1f7
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+ *   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
+ */
+
+/**
+ * @file lib/servers/password.c
+ * @brief Password normalisation functions
+ *
+ * @copyright 2019 The FreeRADIUS server project
+ * @copyright 2019 Arran Cudbard-Bell \<a.cudbardb@freeradius.org\>
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/server/password.h>
+
+#include <freeradius-devel/util/value.h>
+#include <freeradius-devel/util/misc.h>
+#include <freeradius-devel/util/base64.h>
+
+typedef enum {
+       NORMALISED_NOTHING = 0,
+       NORMALISED_B64,
+       NORMALISED_HEX
+} normalise_t;
+
+static FR_NAME_NUMBER const normalise_table[] = {
+       { "nothing",    NORMALISED_NOTHING      },
+       { "base64",     NORMALISED_B64          },
+       { "hex",        NORMALISED_HEX          },
+       {  NULL , -1 }
+};
+
+static ssize_t normify(normalise_t *action, uint8_t *buffer, size_t bufflen,
+                      char const *known_good, size_t len, size_t min_len)
+{
+       if (min_len >= bufflen) return 0; /* paranoia */
+
+       /*
+        *      Hex encoding. Length is even, and it's greater than
+        *      twice the minimum length.
+        */
+       if (!(len & 0x01) && len >= (2 * min_len)) {
+               size_t  decoded;
+
+               decoded = fr_hex2bin(buffer, bufflen, known_good, len);
+               if (decoded == (len >> 1)) {
+                       if (action) *action = NORMALISED_HEX;
+                       return decoded;
+               }
+       }
+
+       /*
+        *      Base 64 encoding.  It's at least 4/3 the original size,
+        *      and we want to avoid division...
+        */
+       if ((len * 3) >= ((min_len * 4))) {
+               ssize_t decoded;
+
+               decoded = fr_base64_decode(buffer, bufflen, known_good, len);
+               if (decoded < 0) return 0;
+               if (decoded >= (ssize_t) min_len) {
+                       if (action) *action = NORMALISED_B64;
+                       return decoded;
+               }
+       }
+
+       /*
+        *      Else unknown encoding, or already binary.  Leave it.
+        */
+       if (action) *action = NORMALISED_NOTHING;
+       return 0;
+}
+
+/** Hex or base64 or bin auto-discovery
+ *
+ * Here we try and autodiscover what encoding was used for the password/hash, and
+ * convert it back to binary or plaintext.
+ *
+ * @note Earlier versions used a 0x prefix as a hard indicator that the string was
+ *       hex encoded, and would fail if the 0x was present but the string didn't
+ *       consist of hexits. The base64 char set is a superset of hex, and it was
+ *       observed in the wild, that occasionally base64 encoded data really could
+ *       start with 0x. That's why min_len (and decodability) are used as the
+ *       only heuristics now.
+ *
+ * @param[in] request          The current request.
+ * @param[in] known_good       password to normify.
+ * @param[in] min_len          we expect the decoded version to be.
+ * @return
+ *     - NULL if known_good was already normalised.
+ *     - A new normalised password pair.
+ */
+VALUE_PAIR *password_normify(TALLOC_CTX *ctx, REQUEST *request, VALUE_PAIR const *known_good, size_t min_len)
+{
+       uint8_t                 buffer[256];
+       ssize_t                 decoded;
+       VALUE_PAIR              *out;
+       normalise_t             normalised;
+
+       if (min_len >= sizeof(buffer)) return NULL; /* paranoia */
+
+       switch (known_good->da->type) {
+       case FR_TYPE_OCTETS:
+               decoded = normify(&normalised, buffer, sizeof(buffer),
+                                 (char const *)known_good->vp_octets, known_good->vp_length, min_len);
+               break;
+
+       case FR_TYPE_STRING:
+               decoded = normify(&normalised, buffer, sizeof(buffer),
+                                 known_good->vp_strvalue, known_good->vp_length, min_len);
+               break;
+
+       default:
+               return NULL;
+       }
+
+       if (normalised != NORMALISED_NOTHING) {
+               RDEBUG2("Normalizing %s %s encoding, %zu bytes -> %zu bytes",
+                       known_good->da->name, fr_int2str(normalise_table, normalised, 0),
+                       known_good->vp_length, decoded);
+               MEM(out = fr_pair_afrom_da(ctx, known_good->da));
+               fr_pair_value_memcpy(out, buffer, decoded, known_good->vp_tainted);
+       }
+
+       /*
+        *      Else unknown encoding, or already binary.  Leave it.
+        */
+       return NULL;
+}
+
+/** Convert a Password-With-Header attribute to the correct type
+ *
+ * Attribute may be base64 encoded, in which case it will be decoded
+ * first, then evaluated.
+ *
+ * @note The buffer for octets types\ attributes is extended by one byte
+ *     and '\0' terminated, to allow it to be used as a char buff.
+ *
+ * @param[in] ctx              to allocate new attributes in.
+ * @param[in] request          Current request.
+ * @param[in] known_good       Password-With-Header attribute to convert.
+ * @param[in] func             to convert header strings to fr_dict_attr_t.
+ * @param[in] def              Default attribute to copy value to if we
+ *                             don't recognise the header.
+ * @return
+ *     - New #VALUE_PAIR on success.
+ *     - NULL on error.
+ */
+VALUE_PAIR *password_normify_with_header(TALLOC_CTX *ctx, REQUEST *request, VALUE_PAIR *known_good,
+                                        password_header_lookup_t func, fr_dict_attr_t const *def)
+{
+       char const              *p, *q, *end;
+
+       uint8_t                 n1[256], n2[256];
+       ssize_t                 decoded;
+
+       char                    header[128];
+       normalise_t             normalised;
+
+       int                     i;
+
+       VALUE_PAIR              *new;
+
+       VP_VERIFY(known_good);
+
+       /*
+        *      Ensure this is only ever called with a
+        *      string type attribute.
+        */
+       rad_assert(known_good->da->type == FR_TYPE_STRING);
+
+       p = known_good->vp_strvalue;
+       end = p + known_good->vp_length;
+
+       /*
+        *      Only allow one additional level of
+        *      normification and header parsing.
+        */
+       for (i = 0; i <= 1; i++) {
+               /*
+                *      Has a header {...} prefix
+                */
+               if ((*p == '{') && (q = memchr(p, '}', end - p))) {
+                       size_t                  hlen;
+                       fr_dict_attr_t const    *da;
+                       ssize_t                 slen;
+
+                       hlen = (q - p) + 1;
+                       if (hlen >= sizeof(header)) {
+                               REDEBUG("Password header too long.  Got %zu bytes must be less than %zu bytes",
+                                       hlen, sizeof(header));
+                               return NULL;
+                       }
+
+                       memcpy(header, p, hlen);
+                       header[hlen] = '\0';
+
+                       slen = func(&da, header);
+                       if (slen <= 0) {
+                               /*
+                                *      header buffer retains { and }
+                                */
+                               if (RDEBUG_ENABLED3) {
+                                       RDEBUG3("Unknown header %s in %pP, re-writing to %s",
+                                               header, known_good, def->name);
+                               } else {
+                                       RDEBUG2("Unknown header %s in %s, re-writing to %s",
+                                               header, known_good->da->name, def->name);
+                               }
+                               goto unknown_header;
+                       }
+
+                       p = q + 1;
+
+                       /*
+                        *      Try and base64 decode, and if we can
+                        *      use the decoded value.
+                        *
+                        *      FIXME: Should pass in min length for
+                        *      password hash da represents.
+                        */
+                       decoded = normify(&normalised, n1, sizeof(n1), p, end - p, 1);
+                       if (decoded > 0) {
+                               RDEBUG2("Normalizing %s %s encoding, %zu bytes -> %zu bytes",
+                                       da->name, fr_int2str(normalise_table, normalised, 0),
+                                       (end - p), decoded);
+                               p = (char const *)n1;
+                               end = p + decoded;
+                       }
+
+                       new = fr_pair_afrom_da(ctx, da);
+                       switch (da->type) {
+                       case FR_TYPE_OCTETS:
+                               fr_pair_value_memcpy(new, (uint8_t const *)p, end - p, true);
+                               break;
+
+                       case FR_TYPE_STRING:
+                               fr_pair_value_bstrncpy(new, (uint8_t const *)p, end - p);
+                               break;
+
+                       default:
+                               if (!fr_cond_assert(0)) return NULL;
+                       }
+                       return new;
+               }
+
+               /*
+                *      Doesn't have a header {...} prefix
+                *
+                *      See if it's base64 or hex, if it is, decode it and check again!
+                */
+               decoded = normify(&normalised, n1, sizeof(n1), p, end - p, 1);
+               if (decoded > 0) {
+                       if ((n1[0] == '{') && (memchr(n1, '}', decoded) != NULL)) {
+                               RDEBUG2("Normalizing %s %s encoding, %zu bytes -> %zu bytes",
+                                       known_good->da->name, fr_int2str(normalise_table, normalised, 0),
+                                       known_good->vp_length, decoded);
+
+                               /*
+                                *      Password-With-Header is a string attribute.
+                                *      Even though we're handling binary data, the header
+                                *      must be \0 terminated.
+                                */
+                               memcpy(n2, n1, decoded);
+                               p = (char const *)n2;
+                               end = p + decoded;
+                               continue;
+                       }
+               }
+
+               break;
+       }
+
+
+       if (RDEBUG_ENABLED3) {
+               RDEBUG3("No {...} in &%pP, re-writing to %s", known_good, def->name);
+       } else {
+               RDEBUG2("No {...} in &%s, re-writing to %s", known_good->da->name, def->name);
+       }
+
+unknown_header:
+       new = fr_pair_afrom_da(request, def);
+       fr_pair_value_bstrncpy(new, p, end - p);
+
+       return new;
+}
diff --git a/src/lib/server/password.h b/src/lib/server/password.h
new file mode 100644 (file)
index 0000000..ea3736f
--- /dev/null
@@ -0,0 +1,41 @@
+#pragma once
+/*
+ *   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
+ */
+
+/**
+ * @file lib/servers/password.h
+ * @brief Password normalisation functions
+ *
+ * @copyright 2019 The FreeRADIUS server project
+ * @copyright 2019 Arran Cudbard-Bell \<a.cudbardb@freeradius.org\>
+ */
+RCSIDH(password_h, "$Id$")
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <freeradius-devel/server/request.h>
+
+typedef ssize_t(*password_header_lookup_t)(fr_dict_attr_t const **out, char const *header);
+
+VALUE_PAIR *password_normify(TALLOC_CTX *ctx, REQUEST *request, VALUE_PAIR const *known_good, size_t min_len);
+
+VALUE_PAIR *password_normify_with_header(TALLOC_CTX *ctx, REQUEST *request, VALUE_PAIR *known_good,
+                                        password_header_lookup_t func, fr_dict_attr_t const *def);
+#ifdef __cplusplus
+}
+#endif
index a410bfb499f226253a81e286c6c682969fd103b2..612f780c4a797e3e153d4262ee85e015fe884f3c 100644 (file)
@@ -25,6 +25,7 @@
 RCSID("$Id$")
 
 #include <freeradius-devel/server/base.h>
+#include <freeradius-devel/server/password.h>
 #include <freeradius-devel/server/module.h>
 
 typedef struct {
@@ -43,7 +44,10 @@ fr_dict_autoload_t rlm_chap_dict[] = {
 };
 
 static fr_dict_attr_t const *attr_auth_type;
+static fr_dict_attr_t const *attr_proxy_to_realm;
+static fr_dict_attr_t const *attr_realm;
 static fr_dict_attr_t const *attr_cleartext_password;
+static fr_dict_attr_t const *attr_password_with_header;
 
 static fr_dict_attr_t const *attr_chap_password;
 static fr_dict_attr_t const *attr_chap_challenge;
@@ -52,7 +56,11 @@ static fr_dict_attr_t const *attr_user_password;
 extern fr_dict_attr_autoload_t rlm_chap_dict_attr[];
 fr_dict_attr_autoload_t rlm_chap_dict_attr[] = {
        { .out = &attr_auth_type, .name = "Auth-Type", .type = FR_TYPE_UINT32, .dict = &dict_freeradius },
+       { .out = &attr_proxy_to_realm, .name = "Proxy-To-Realm", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
+       { .out = &attr_realm, .name = "Realm", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
+
        { .out = &attr_cleartext_password, .name = "Cleartext-Password", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
+       { .out = &attr_password_with_header, .name = "Password-With-Header", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
 
        { .out = &attr_chap_password, .name = "Chap-Password", .type = FR_TYPE_OCTETS, .dict = &dict_radius },
        { .out = &attr_chap_challenge, .name = "Chap-Challenge", .type = FR_TYPE_OCTETS, .dict = &dict_radius },
@@ -60,23 +68,82 @@ fr_dict_attr_autoload_t rlm_chap_dict_attr[] = {
        { NULL }
 };
 
+static const FR_NAME_NUMBER header_names[] = {
+       { "{clear}",            FR_CLEARTEXT_PASSWORD },
+       { "{cleartext}",        FR_CLEARTEXT_PASSWORD },
+       { NULL, 0 }
+};
+
+static ssize_t chap_password_header(fr_dict_attr_t const **out, char const *header)
+{
+       switch (fr_str2int(header_names, header, 0)) {
+       case FR_CLEARTEXT_PASSWORD:
+               *out = attr_cleartext_password;
+               return strlen(header);
+
+       default:
+               *out = NULL;
+               return -1;
+       }
+}
+
 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, UNUSED void *thread, REQUEST *request)
 {
        rlm_chap_t      *inst = instance;
        VALUE_PAIR      *vp;
+       VALUE_PAIR      *known_good;
 
+       /*
+        *      This case means the warnings below won't be printed
+        *      unless there's a CHAP-Password in the request.
+        */
        if (!fr_pair_find_by_da(request->packet->vps, attr_chap_password, TAG_ANY)) return RLM_MODULE_NOOP;
 
+       known_good = fr_pair_find_by_da(request->control, attr_cleartext_password, TAG_ANY);
+       if (!known_good) {
+               VALUE_PAIR *new;
+
+               known_good = fr_pair_find_by_da(request->control, attr_password_with_header, TAG_ANY);
+               if (!known_good) {
+                       /*
+                        *      Likely going to be proxied.  Avoid printing
+                        *      warning message.
+                        */
+                       if (fr_pair_find_by_da(request->control, attr_realm, TAG_ANY) ||
+                           (fr_pair_find_by_da(request->control, attr_proxy_to_realm, TAG_ANY))) {
+                               return RLM_MODULE_NOOP;
+                       }
+
+                       RWDEBUG("No \"known good\" password found for the user.  Not setting Auth-Type");
+                       RWDEBUG("Authentication will fail unless a \"known good\" password is available");
+
+                       return RLM_MODULE_NOOP;
+               }
+
+               /*
+                *      Normalise Password-With-Header
+                */
+               MEM(new = password_normify_with_header(request, request, known_good,
+                                                      chap_password_header, attr_cleartext_password));
+               if (RDEBUG_ENABLED3) {
+                       RDEBUG3("Noramlized &control:%pP -> &control:%pP", known_good, new);
+               } else {
+                       RDEBUG2("Normalized &control:%s -> &control:%s", known_good->da->name, new->da->name);
+               }
+               RDEBUG2("Removing &control:%s", known_good->da->name);
+               fr_pair_delete_by_da(&request->control, attr_password_with_header);
+               fr_pair_add(&request->control, new);
+       }
+
        /*
         *      Create the CHAP-Challenge if it wasn't already in the packet.
         *
         *      This is so that the rest of the code does not need to
         *      understand CHAP.
         */
-
        vp = fr_pair_find_by_da(request->packet->vps, attr_chap_challenge, TAG_ANY);
        if (!vp) {
-               RDEBUG2("Creating CHAP-Challenge from the request authenticator");
+               RDEBUG2("Creating &%s from request authenticator", attr_chap_challenge->name);
 
                MEM(vp = fr_pair_afrom_da(request->packet, attr_chap_challenge));
                fr_pair_value_memcpy(vp, request->packet->vector, sizeof(request->packet->vector), true);
@@ -88,7 +155,6 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, UNUSED void *t
        return RLM_MODULE_OK;
 }
 
-
 /*
  *     Find the named user in this modules database.  Create the set
  *     of attribute-value pairs to check and reply with for this user
@@ -97,7 +163,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, UNUSED void *t
  */
 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, UNUSED void *thread, REQUEST *request)
 {
-       VALUE_PAIR *password, *chap;
+       VALUE_PAIR *known_good, *chap;
        uint8_t pass_str[FR_MAX_STRING_LEN];
 
        if (!request->username) {
@@ -122,8 +188,8 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, UNUS
                return RLM_MODULE_INVALID;
        }
 
-       password = fr_pair_find_by_da(request->control, attr_cleartext_password, TAG_ANY);
-       if (password == NULL) {
+       known_good = fr_pair_find_by_da(request->control, attr_cleartext_password, TAG_ANY);
+       if (known_good == NULL) {
                if (fr_pair_find_by_da(request->control, attr_user_password, TAG_ANY) != NULL){
                        REDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        REDEBUG("!!! Please update your configuration so that the \"known !!!");
@@ -138,15 +204,14 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, UNUS
                return RLM_MODULE_FAIL;
        }
 
-       fr_radius_encode_chap_password(pass_str, request->packet, chap->vp_octets[0], password);
+       fr_radius_encode_chap_password(pass_str, request->packet, chap->vp_octets[0], known_good);
 
        if (RDEBUG_ENABLED3) {
                uint8_t const   *p;
                size_t          length;
                VALUE_PAIR      *vp;
 
-               RDEBUG3("Comparing with \"known good\" &control:Cleartext-Password value \"%s\"",
-                       password->vp_strvalue);
+               RDEBUG3("Comparing with \"known good\" &control:%pP", known_good);
 
                vp = fr_pair_find_by_da(request->packet->vps, attr_chap_challenge, TAG_ANY);
                if (vp) {
index 49063c575677f2c50817dfb87ea1a326808f3a0b..a56b305d83a36c61cf0ac97d1b952c80833113be 100644 (file)
@@ -29,6 +29,7 @@ USES_APPLE_DEPRECATED_API
 #include <freeradius-devel/server/base.h>
 #include <freeradius-devel/server/crypt.h>
 #include <freeradius-devel/server/module.h>
+#include <freeradius-devel/server/password.h>
 #include <freeradius-devel/server/rad_assert.h>
 #include <freeradius-devel/tls/base.h>
 #include <freeradius-devel/util/base64.h>
@@ -73,8 +74,8 @@ static fr_dict_attr_t const *attr_auth_type;
 static fr_dict_attr_t const *attr_proxy_to_realm;
 static fr_dict_attr_t const *attr_realm;
 
-static fr_dict_attr_t const *attr_password_with_header;
 static fr_dict_attr_t const *attr_cleartext_password;
+static fr_dict_attr_t const *attr_password_with_header;
 
 static fr_dict_attr_t const *attr_md5_password;
 static fr_dict_attr_t const *attr_smd5_password;
@@ -107,8 +108,8 @@ fr_dict_attr_autoload_t rlm_pap_dict_attr[] = {
        { .out = &attr_proxy_to_realm, .name = "Proxy-To-Realm", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
        { .out = &attr_realm, .name = "Realm", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
 
-       { .out = &attr_password_with_header, .name = "Password-With-Header", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
        { .out = &attr_cleartext_password, .name = "Cleartext-Password", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
+       { .out = &attr_password_with_header, .name = "Password-With-Header", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
 
        { .out = &attr_md5_password, .name = "MD5-Password", .type = FR_TYPE_OCTETS, .dict = &dict_freeradius },
        { .out = &attr_smd5_password, .name = "SMD5-Password", .type = FR_TYPE_OCTETS, .dict = &dict_freeradius },
@@ -176,7 +177,7 @@ static const FR_NAME_NUMBER header_names[] = {
        { "{x-nthash}",         FR_NT_PASSWORD },
        { "{ns-mta-md5}",       FR_NS_MTA_MD5_PASSWORD },
        { "{x- orcllmv}",       FR_LM_PASSWORD },
-       { "{X- orclntv}",       FR_NT_PASSWORD },
+       { "X- orclntv}",        FR_NT_PASSWORD },
        { NULL, 0 }
 };
 
@@ -187,7 +188,7 @@ static const FR_NAME_NUMBER pbkdf2_crypt_names[] = {
        { "HMACSHA2+256",       FR_SSHA2_256_PASSWORD },
        { "HMACSHA2+384",       FR_SSHA2_384_PASSWORD },
        { "HMACSHA2+512",       FR_SSHA2_512_PASSWORD },
-#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+#  if OPENSSL_VERSION_NUMBER >= 0x10101000L
        { "HMACSHA3+224",       FR_SSHA3_224_PASSWORD },
        { "HMACSHA3+256",       FR_SSHA3_256_PASSWORD },
        { "HMACSHA3+384",       FR_SSHA3_384_PASSWORD },
@@ -205,276 +206,91 @@ static const FR_NAME_NUMBER pbkdf2_passlib_names[] = {
 };
 #endif
 
-/** Hex or base64 or bin auto-discovery
- *
- * Here we try and autodiscover what encoding was used for the password/hash, and
- * convert it back to binary or plaintext.
- *
- * @note Earlier versions used a 0x prefix as a hard indicator that the string was
- *       hex encoded, and would fail if the 0x was present but the string didn't
- *       consist of hexits. The base64 char set is a superset of hex, and it was
- *       observed in the wild, that occasionally base64 encoded data really could
- *       start with 0x. That's why min_len (and decodability) are used as the
- *       only heuristics now.
- *
- * @param[in] request          The current request.
- * @param[in,out] known_good   password to normify.
- * @param[in] min_len          we expect the decoded version to be.
- */
-static void normify(REQUEST *request, VALUE_PAIR *known_good, size_t min_len)
+static ssize_t pap_password_header(fr_dict_attr_t const **out, char const *header)
 {
-       uint8_t buffer[256];
-
-       if (min_len >= sizeof(buffer)) return; /* paranoia */
-
-       rad_assert((known_good->da->type == FR_TYPE_OCTETS) || (known_good->da->type == FR_TYPE_STRING));
-
-       /*
-        *      Hex encoding. Length is even, and it's greater than
-        *      twice the minimum length.
-        */
-       if (!(known_good->vp_length & 0x01) && known_good->vp_length >= (2 * min_len)) {
-               bool    tainted = known_good->data.tainted;
-               size_t  decoded;
-
-
-               decoded = fr_hex2bin(buffer, sizeof(buffer), known_good->vp_strvalue, known_good->vp_length);
-               if (decoded == (known_good->vp_length >> 1)) {
-                       RDEBUG2("Normalizing %s from hex encoding, %zu bytes -> %zu bytes",
-                               known_good->da->name, known_good->vp_length, decoded);
-                       fr_pair_value_memcpy(known_good, buffer, decoded, tainted);
-                       return;
-               }
-       }
-
-       /*
-        *      Base 64 encoding.  It's at least 4/3 the original size,
-        *      and we want to avoid division...
-        */
-       if ((known_good->vp_length * 3) >= ((min_len * 4))) {
-               ssize_t decoded;
-
-               decoded = fr_base64_decode(buffer, sizeof(buffer), known_good->vp_strvalue, known_good->vp_length);
-               if (decoded < 0) return;
-               if (decoded >= (ssize_t) min_len) {
-                       bool tainted = known_good->data.tainted;
-
-                       RDEBUG2("Normalizing %s from base64 encoding, %zu bytes -> %zu bytes",
-                               known_good->da->name, known_good->vp_length, decoded);
-                       fr_pair_value_memcpy(known_good, buffer, decoded, tainted);
-                       return;
-               }
-       }
-
-       /*
-        *      Else unknown encoding, or already binary.  Leave it.
-        */
-}
-
-/** Convert a Password-With-Header attribute to the correct type
- *
- * Attribute may be base64 encoded, in which case it will be decoded
- * first, then evaluated.
- *
- * @note The buffer for octets types\ attributes is extended by one byte
- *     and '\0' terminated, to allow it to be used as a char buff.
- *
- * @param[in] ctx              to allocate new attributes in.
- * @param[in] request          Current request.
- * @param[in] known_good       Password-With-Header attribute to convert.
- * @return
- *     - New #VALUE_PAIR on success.
- *     - NULL on error.
- */
-static VALUE_PAIR *normify_with_header(TALLOC_CTX *ctx, REQUEST *request,
-                                      VALUE_PAIR *known_good, UNUSED VALUE_PAIR const *password)
-{
-       char const      *p, *q;
-       size_t          len;
-
-       uint8_t         digest[256];
-       ssize_t         decoded;
-
-       char            buffer[256];
-
-       VALUE_PAIR      *new;
-
-       VP_VERIFY(known_good);
-
-       /*
-        *      Ensure this is only ever called with a
-        *      string type attribute.
-        */
-       rad_assert(known_good->da->type == FR_TYPE_STRING);
-
-redo:
-       p = known_good->vp_strvalue;
-       len = known_good->vp_length;
-
-       /*
-        *      Has a header {...} prefix
-        */
-       q = strchr(p, '}');
-       if (q) {
-               size_t                  hlen;
-               fr_dict_attr_t const    *da;
-
-               hlen = (q + 1) - p;
-               if (hlen >= sizeof(buffer)) {
-                       REDEBUG("Password header too long.  Got %zu bytes must be less than %zu bytes",
-                               hlen, sizeof(buffer));
-                       return NULL;
-               }
-
-               memcpy(buffer, p, hlen);
-               buffer[hlen] = '\0';
-
-               /*
-                *      The data after the '}' may be binary, so we copy it via
-                *      memcpy.  BUT it might be a string (or used as one), so
-                *      we ensure that there's a trailing zero, too.
-                */
-               switch (fr_str2int(header_names, buffer, 0)) {
-               case FR_CLEARTEXT_PASSWORD:
-                       da = attr_cleartext_password;
-                       break;
-
-               case FR_MD5_PASSWORD:
-                       da = attr_md5_password;
-                       break;
-
-               case FR_SMD5_PASSWORD:
-                       da = attr_smd5_password;
-                       break;
-
-               case FR_CRYPT_PASSWORD:
-                       da = attr_crypt_password;
-                       break;
-
-               case FR_SHA2_PASSWORD:
-                       da = attr_sha2_password;
-                       break;
-
-               case FR_SSHA2_224_PASSWORD:
-                       da = attr_ssha2_224_password;
-                       break;
-
-               case FR_SSHA2_256_PASSWORD:
-                       da = attr_ssha2_256_password;
-                       break;
-
-               case FR_SSHA2_384_PASSWORD:
-                       da = attr_ssha2_384_password;
-                       break;
-
-               case FR_SSHA2_512_PASSWORD:
-                       da = attr_ssha2_512_password;
-                       break;
+       switch (fr_str2int(header_names, header, 0)) {
+       case FR_CLEARTEXT_PASSWORD:
+               *out = attr_cleartext_password;
+               break;
 
-               case FR_SSHA3_224_PASSWORD:
-                       da = attr_ssha3_224_password;
-                       break;
+       case FR_MD5_PASSWORD:
+               *out = attr_md5_password;
+               break;
 
-               case FR_SSHA3_256_PASSWORD:
-                       da = attr_ssha3_256_password;
-                       break;
+       case FR_SMD5_PASSWORD:
+               *out = attr_smd5_password;
+               break;
 
-               case FR_SSHA3_384_PASSWORD:
-                       da = attr_ssha3_384_password;
-                       break;
+       case FR_CRYPT_PASSWORD:
+               *out = attr_crypt_password;
+               break;
 
-               case FR_SSHA3_512_PASSWORD:
-                       da = attr_ssha3_512_password;
-                       break;
+       case FR_SHA2_PASSWORD:
+               *out = attr_sha2_password;
+               break;
 
-               case FR_PBKDF2_PASSWORD:
-                       da = attr_pbkdf2_password;
-                       break;
+       case FR_SSHA2_224_PASSWORD:
+               *out = attr_ssha2_224_password;
+               break;
 
-               case FR_SHA_PASSWORD:
-                       da = attr_sha_password;
-                       break;
+       case FR_SSHA2_256_PASSWORD:
+               *out = attr_ssha2_256_password;
+               break;
 
-               case FR_SSHA_PASSWORD:
-                       da = attr_ssha_password;
-                       break;
+       case FR_SSHA2_384_PASSWORD:
+               *out = attr_ssha2_384_password;
+               break;
 
-               case FR_NS_MTA_MD5_PASSWORD:
-                       da = attr_ns_mta_md5_password;
-                       break;
+       case FR_SSHA2_512_PASSWORD:
+               *out = attr_ssha2_512_password;
+               break;
 
-               case FR_LM_PASSWORD:
-                       da = attr_lm_password;
-                       break;
+       case FR_SSHA3_224_PASSWORD:
+               *out = attr_ssha3_224_password;
+               break;
 
-               case FR_NT_PASSWORD:
-                       da = attr_nt_password;
-                       break;
+       case FR_SSHA3_256_PASSWORD:
+               *out = attr_ssha3_256_password;
+               break;
 
-               default:
-                       if (RDEBUG_ENABLED3) {
-                               RDEBUG3("Unknown header {%s} in %pP, re-writing to "
-                                       "Cleartext-Password", buffer, known_good);
-                       } else {
-                               RDEBUG2("Unknown header {%s} in Password-With-Header, re-writing to "
-                                      "Cleartext-Password", buffer);
-                       }
-                       goto unknown_header;
-               }
+       case FR_SSHA3_384_PASSWORD:
+               *out = attr_ssha3_384_password;
+               break;
 
-               new = fr_pair_afrom_da(ctx, da);
-               switch (da->type) {
-               case FR_TYPE_OCTETS:
-                       fr_pair_value_memcpy(new, (uint8_t const *)q + 1, len - hlen, true);
-                       break;
+       case FR_SSHA3_512_PASSWORD:
+               *out = attr_ssha3_512_password;
+               break;
 
-               case FR_TYPE_STRING:
-                       fr_pair_value_bstrncpy(new, (uint8_t const *)q + 1, len - hlen);
-                       break;
+       case FR_PBKDF2_PASSWORD:
+               *out = attr_pbkdf2_password;
+               break;
 
-               default:
-                       if (!fr_cond_assert(0)) return NULL;
-               }
+       case FR_SHA_PASSWORD:
+               *out = attr_sha_password;
+               break;
 
-               if (RDEBUG_ENABLED3) {
-                       RDEBUG3("Converted: &control:%pP -> &control:%pP", known_good, new);
-               } else {
-                       RDEBUG2("Converted: &control:%s -> &control:%s", known_good->da->name, new->da->name);
-               }
+       case FR_SSHA_PASSWORD:
+               *out = attr_ssha_password;
+               break;
 
-               return new;
-       }
+       case FR_NS_MTA_MD5_PASSWORD:
+               *out = attr_ns_mta_md5_password;
+               break;
 
-       /*
-        *      Doesn't have a header {...} prefix
-        *
-        *      See if it's base64, if it is, decode it and check again!
-        */
-       decoded = fr_base64_decode(digest, sizeof(digest), known_good->vp_strvalue, len);
-       if ((decoded > 0) && (digest[0] == '{') && (memchr(digest, '}', decoded) != NULL)) {
-               RDEBUG2("Normalizing %s from base64 encoding, %zu bytes -> %zu bytes",
-                       known_good->da->name, known_good->vp_length, decoded);
-               /*
-                *      Password-With-Header is a string attribute.
-                *      Even though we're handling binary data, the buffer
-                *      must be \0 terminated.
-                */
-               fr_pair_value_bstrncpy(known_good, digest, decoded);
+       case FR_LM_PASSWORD:
+               *out = attr_lm_password;
+               break;
 
-               goto redo;
-       }
+       case FR_NT_PASSWORD:
+               *out = attr_nt_password;
+               break;
 
-       if (RDEBUG_ENABLED3) {
-               RDEBUG3("No {...} in &%pP, re-writing to Cleartext-Password", known_good);
-       } else {
-               RDEBUG2("No {...} in &%s, re-writing to Cleartext-Password", known_good->da->name);
+       default:
+               *out = NULL;
+               return -1;
        }
 
-unknown_header:
-       new = fr_pair_afrom_da(request, attr_cleartext_password);
-       fr_pair_value_strcpy(new, known_good->vp_strvalue);
-
-       return new;
+       return strlen(header);
 }
 
 /*
@@ -489,6 +305,7 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, UNUSED void *t
        bool                    found_pw = false;
        VALUE_PAIR              *known_good, *password;
        fr_cursor_t             cursor;
+       size_t                  normify_min_len = 0;
 
        password = fr_pair_find_by_da(request->packet->vps, attr_user_password, TAG_ANY);
        if (!password) {
@@ -516,16 +333,22 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, UNUSED void *t
                         */
                        if (fr_pair_find_by_da(request->control, attr_cleartext_password, TAG_ANY)) {
                                RWDEBUG("Config already contains a \"known good\" password "
-                                       "(&control:Cleartext-Password).  Ignoring &control:Password-With-Header");
+                                       "(&control:%s).  Ignoring &control:%s",
+                                       attr_cleartext_password->name, known_good->da->name);
                                break;
                        }
 
-                       new = normify_with_header(request, request, known_good, password);
-                       if (new) fr_cursor_append(&cursor, new); /* inserts at the end of the list */
-
-                       RDEBUG2("Removing &control:Password-With-Header");
-                       known_good = fr_cursor_remove(&cursor); /* advances the cursor for us */
-                       talloc_free(known_good);
+                       MEM(new = password_normify_with_header(request, request, known_good,
+                                                              pap_password_header,
+                                                              attr_cleartext_password));
+                       if (RDEBUG_ENABLED3) {
+                               RDEBUG3("Normalized &control:%pP -> &control:%pP", known_good, new);
+                       } else {
+                               RDEBUG2("Normalized &control:%s -> &control:%s", known_good->da->name, new->da->name);
+                       }
+                       RDEBUG2("Removing &control:%s", known_good->da->name);
+                       fr_cursor_free_item(&cursor);                   /* advances the cursor for us */
+                       fr_cursor_append(&cursor, new);                 /* inserts at the end of the list */
 
                        found_pw = true;
 
@@ -539,41 +362,41 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, UNUSED void *t
                           (known_good->da == attr_smd5_password) ||
                           (known_good->da == attr_nt_password) ||
                           (known_good->da == attr_lm_password)) {
-                       if (inst->normify) normify(request, known_good, 16); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 16;
                        found_pw = true;
                }
 #ifdef HAVE_OPENSSL_EVP_H
                else if (known_good->da == attr_sha2_password) {
-                       if (inst->normify) normify(request, known_good, 28); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 28;
                        found_pw = true;
                } else if (known_good->da == attr_ssha2_224_password) {
-                       if (inst->normify) normify(request, known_good, 28); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 28;
                        found_pw = true;
                } else if (known_good->da == attr_ssha2_256_password) {
-                       if (inst->normify) normify(request, known_good, 32); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 32;
                        found_pw = true;
                } else if (known_good->da == attr_ssha2_384_password) {
-                       if (inst->normify) normify(request, known_good, 48); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 48;
                        found_pw = true;
                } else if (known_good->da == attr_ssha2_512_password) {
-                       if (inst->normify) normify(request, known_good, 64); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 64;
                        found_pw = true;
                }
 #  if OPENSSL_VERSION_NUMBER >= 0x10101000L
                else if (known_good->da == attr_sha3_password) {
-                       if (inst->normify) normify(request, known_good, 28); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 28;
                        found_pw = true;
                } else if (known_good->da == attr_ssha3_224_password) {
-                       if (inst->normify) normify(request, known_good, 28); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 28;
                        found_pw = true;
                } else if (known_good->da == attr_ssha3_256_password) {
-                       if (inst->normify) normify(request, known_good, 32); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 32;
                        found_pw = true;
                } else if (known_good->da == attr_ssha3_384_password) {
-                       if (inst->normify) normify(request, known_good, 48); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 48;
                        found_pw = true;
                } else if (known_good->da == attr_ssha3_512_password) {
-                       if (inst->normify) normify(request, known_good, 64); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 64;
                        found_pw = true;
                }
 #  endif
@@ -583,9 +406,22 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, UNUSED void *t
 #endif
                else if ((known_good->da == attr_sha_password) ||
                         (known_good->da == attr_ssha_password)) {
-                       if (inst->normify) normify(request, known_good, 20); /* ensure it's in the right format */
+                       if (inst->normify) normify_min_len = 20;
                        found_pw = true;
                }
+
+               if (normify_min_len > 0) {
+                       VALUE_PAIR *new;
+
+                       new = password_normify(request, request, known_good, normify_min_len);
+                       if (new) {
+                               fr_cursor_free_item(&cursor);           /* free the old pasword */
+                               fr_cursor_append(&cursor, new);         /* inserts at the end of the list */
+
+                               known_good = fr_cursor_current(&cursor);
+                               if (known_good) goto next;
+                       }
+               }
        }
 
        /*
@@ -641,12 +477,11 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_crypt(UNUSED rlm_pap_t const *inst,
 }
 #endif
 
-static rlm_rcode_t CC_HINT(nonnull) pap_auth_md5(rlm_pap_t const *inst, REQUEST *request,
+static rlm_rcode_t CC_HINT(nonnull) pap_auth_md5(UNUSED rlm_pap_t const *inst, REQUEST *request,
                                                 VALUE_PAIR *known_good, VALUE_PAIR const *password)
 {
        uint8_t digest[MD5_DIGEST_LENGTH];
 
-       if (inst->normify) normify(request, known_good, MD5_DIGEST_LENGTH);
        if (known_good->vp_length != MD5_DIGEST_LENGTH) {
                REDEBUG("\"known-good\" MD5 password has incorrect length, expected 16 got %zu", known_good->vp_length);
                return RLM_MODULE_INVALID;
@@ -666,13 +501,12 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_md5(rlm_pap_t const *inst, REQUEST
 }
 
 
-static rlm_rcode_t CC_HINT(nonnull) pap_auth_smd5(rlm_pap_t const *inst, REQUEST *request,
+static rlm_rcode_t CC_HINT(nonnull) pap_auth_smd5(UNUSED rlm_pap_t const *inst, REQUEST *request,
                                                  VALUE_PAIR *known_good, VALUE_PAIR const *password)
 {
        fr_md5_ctx_t    *md5_ctx;
        uint8_t         digest[MD5_DIGEST_LENGTH];
 
-       if (inst->normify) normify(request, known_good, MD5_DIGEST_LENGTH);
        if (known_good->vp_length <= MD5_DIGEST_LENGTH) {
                REDEBUG("\"known-good\" SMD5-Password has incorrect length, expected 16 got %zu", known_good->vp_length);
                return RLM_MODULE_INVALID;
@@ -698,14 +532,12 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_smd5(rlm_pap_t const *inst, REQUEST
        return RLM_MODULE_OK;
 }
 
-static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha(rlm_pap_t const *inst, REQUEST *request,
+static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha(UNUSED rlm_pap_t const *inst, REQUEST *request,
                                                 VALUE_PAIR *known_good, VALUE_PAIR const *password)
 {
        fr_sha1_ctx     sha1_context;
        uint8_t         digest[SHA1_DIGEST_LENGTH];
 
-       if (inst->normify) normify(request, known_good, SHA1_DIGEST_LENGTH);
-
        if (known_good->vp_length != SHA1_DIGEST_LENGTH) {
                REDEBUG("\"known-good\" SHA1-password has incorrect length, expected 20 got %zu", known_good->vp_length);
                return RLM_MODULE_INVALID;
@@ -726,14 +558,12 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha(rlm_pap_t const *inst, REQUEST
        return RLM_MODULE_OK;
 }
 
-static rlm_rcode_t CC_HINT(nonnull) pap_auth_ssha(rlm_pap_t const *inst, REQUEST *request,
+static rlm_rcode_t CC_HINT(nonnull) pap_auth_ssha(UNUSED rlm_pap_t const *inst, REQUEST *request,
                                                  VALUE_PAIR *known_good, VALUE_PAIR const *password)
 {
        fr_sha1_ctx     sha1_context;
        uint8_t         digest[SHA1_DIGEST_LENGTH];
 
-       if (inst->normify) normify(request, known_good, SHA1_DIGEST_LENGTH);
-
        if (known_good->vp_length <= SHA1_DIGEST_LENGTH) {
                REDEBUG("\"known-good\" SSHA-Password has incorrect length, expected > 20 got %zu", known_good->vp_length);
                return RLM_MODULE_INVALID;
@@ -759,7 +589,7 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_ssha(rlm_pap_t const *inst, REQUEST
 }
 
 #ifdef HAVE_OPENSSL_EVP_H
-static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha_evp(rlm_pap_t const *inst, REQUEST *request,
+static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha_evp(UNUSED rlm_pap_t const *inst, REQUEST *request,
                                                     VALUE_PAIR *known_good, VALUE_PAIR const *password)
 {
        EVP_MD_CTX      *ctx;
@@ -768,8 +598,6 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha_evp(rlm_pap_t const *inst, REQU
        uint8_t         digest[EVP_MAX_MD_SIZE];
        unsigned int    digest_len;
 
-       if (inst->normify) normify(request, known_good, SHA224_DIGEST_LENGTH);
-
        if (known_good->da == attr_sha2_password) {
                /*
                 *      All the SHA-2 algorithms produce digests of different lengths,
@@ -868,7 +696,7 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha_evp(rlm_pap_t const *inst, REQU
        return RLM_MODULE_OK;
 }
 
-static rlm_rcode_t CC_HINT(nonnull) pap_auth_ssha_evp(rlm_pap_t const *inst, REQUEST *request,
+static rlm_rcode_t CC_HINT(nonnull) pap_auth_ssha_evp(UNUSED rlm_pap_t const *inst, REQUEST *request,
                                                      VALUE_PAIR *known_good, VALUE_PAIR const *password)
 {
        EVP_MD_CTX      *ctx;
@@ -918,13 +746,6 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_ssha_evp(rlm_pap_t const *inst, REQ
                return RLM_MODULE_INVALID;
        }
 
-       /*
-        *      Unlike plain SHA2/3 we already know what length
-        *      to expect, so can be more specific with the
-        *      minimum digest length.
-        */
-       if (inst->normify) normify(request, known_good, min_len + 1);
-
        if (known_good->vp_length <= min_len) {
                REDEBUG("\"known-good\" %s-Password has incorrect length, got %zu bytes, need at least %u bytes",
                        name, known_good->vp_length, min_len + 1);
@@ -1238,7 +1059,7 @@ static inline rlm_rcode_t CC_HINT(nonnull) pap_auth_pbkdf2(UNUSED rlm_pap_t cons
 }
 #endif
 
-static rlm_rcode_t CC_HINT(nonnull) pap_auth_nt(rlm_pap_t const *inst, REQUEST *request,
+static rlm_rcode_t CC_HINT(nonnull) pap_auth_nt(UNUSED rlm_pap_t const *inst, REQUEST *request,
                                                VALUE_PAIR *known_good, VALUE_PAIR const *password)
 {
        ssize_t len;
@@ -1250,8 +1071,6 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_nt(rlm_pap_t const *inst, REQUEST *
        rad_assert(password != NULL);
        rad_assert(password->da == attr_user_password);
 
-       if (inst->normify) normify(request, known_good, MD4_DIGEST_LENGTH);
-
        if (known_good->vp_length != MD4_DIGEST_LENGTH) {
                REDEBUG("\"known good\" NT-Password has incorrect length, expected 16 got %zu", known_good->vp_length);
                return RLM_MODULE_INVALID;
@@ -1276,7 +1095,7 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_nt(rlm_pap_t const *inst, REQUEST *
        return RLM_MODULE_OK;
 }
 
-static rlm_rcode_t CC_HINT(nonnull) pap_auth_lm(rlm_pap_t const *inst, REQUEST *request,
+static rlm_rcode_t CC_HINT(nonnull) pap_auth_lm(UNUSED rlm_pap_t const *inst, REQUEST *request,
                                                VALUE_PAIR *known_good, UNUSED VALUE_PAIR const *password)
 {
        uint8_t digest[MD4_DIGEST_LENGTH];
@@ -1285,8 +1104,6 @@ static rlm_rcode_t CC_HINT(nonnull) pap_auth_lm(rlm_pap_t const *inst, REQUEST *
 
        RDEBUG2("Comparing with \"known-good\" LM-Password");
 
-       if (inst->normify) normify(request, known_good, MD4_DIGEST_LENGTH);
-
        if (known_good->vp_length != MD4_DIGEST_LENGTH) {
                REDEBUG("\"known good\" LM-Password has incorrect length, expected 16 got %zu", known_good->vp_length);
                return RLM_MODULE_INVALID;