]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
Use call_env to remove xlat_eval from mschap password change
authorNick Porter <nick@portercomputing.co.uk>
Wed, 13 Mar 2024 19:50:17 +0000 (19:50 +0000)
committerNick Porter <nick@portercomputing.co.uk>
Fri, 29 Mar 2024 12:40:54 +0000 (12:40 +0000)
src/modules/rlm_mschap/rlm_mschap.c
src/modules/rlm_mschap/rlm_mschap.h

index 71a32bd4ba41441c61fa29376171843b685f0856..89e3b6bfd1c314576415e27ad467e302daa8c7c3 100644 (file)
@@ -42,7 +42,7 @@ RCSID("$Id$")
 #include <freeradius-devel/util/misc.h>
 #include <freeradius-devel/util/sha1.h>
 
-#include <freeradius-devel/unlang/call_env.h>
+#include <freeradius-devel/unlang/function.h>
 #include <freeradius-devel/unlang/xlat_func.h>
 
 #include <sys/wait.h>
@@ -83,9 +83,6 @@ unlang_action_t od_mschap_auth(rlm_rcode_t *p_result, request_t *request, fr_pai
 
 static const conf_parser_t passchange_config[] = {
        { FR_CONF_OFFSET_FLAGS("ntlm_auth", CONF_FLAG_XLAT, rlm_mschap_t, ntlm_cpw) },
-       { FR_CONF_OFFSET_FLAGS("ntlm_auth_username", CONF_FLAG_XLAT, rlm_mschap_t, ntlm_cpw_username) },
-       { FR_CONF_OFFSET_FLAGS("ntlm_auth_domain", CONF_FLAG_XLAT, rlm_mschap_t, ntlm_cpw_domain) },
-       { FR_CONF_OFFSET_FLAGS("local_cpw", CONF_FLAG_XLAT, rlm_mschap_t, local_cpw) },
        CONF_PARSER_TERMINATOR
 };
 
@@ -185,6 +182,13 @@ static const call_env_method_t mschap_auth_method_env = {
        FR_CALL_ENV_METHOD_OUT(mschap_auth_call_env_t),
        .env = (call_env_parser_t[]){ \
                { FR_CALL_ENV_SUBSECTION("attributes", NULL, CALL_ENV_FLAG_REQUIRED, auth_call_env) },
+               { FR_CALL_ENV_SUBSECTION("passchange", NULL, CALL_ENV_FLAG_SUBSECTION,
+                       ((call_env_parser_t[]) {
+                               { FR_CALL_ENV_PARSE_ONLY_OFFSET("ntlm_auth_username", FR_TYPE_STRING, CALL_ENV_FLAG_NONE, mschap_auth_call_env_t, ntlm_cpw_username) },
+                               { FR_CALL_ENV_PARSE_ONLY_OFFSET("ntlm_auth_domain", FR_TYPE_STRING, CALL_ENV_FLAG_NONE, mschap_auth_call_env_t, ntlm_cpw_domain) },
+                               { FR_CALL_ENV_PARSE_ONLY_OFFSET("local_cpw", FR_TYPE_STRING, CALL_ENV_FLAG_NONE, mschap_auth_call_env_t, local_cpw) },
+                               CALL_ENV_TERMINATOR
+                       }))},
                { FR_CALL_ENV_SUBSECTION("winbind", NULL, CALL_ENV_FLAG_NONE,
                        ((call_env_parser_t[]) {
                                { FR_CALL_ENV_OFFSET("username", FR_TYPE_STRING, CALL_ENV_FLAG_NONE, mschap_auth_call_env_t, wb_username) },
@@ -210,6 +214,26 @@ static const call_env_parser_t autz_call_env[] = {
 
 MSCHAP_CALL_ENV(autz);
 
+typedef struct {
+       fr_value_box_list_t     cpw_user;
+       fr_value_box_list_t     cpw_domain;
+       fr_value_box_list_t     local_cpw_result;
+       uint8_t                 new_nt_encrypted[516];
+       uint8_t                 old_nt_hash[NT_DIGEST_LENGTH];
+       fr_pair_t               *new_hash;
+} mschap_cpw_ctx_t;
+
+typedef struct {
+       char const              *name;
+       rlm_mschap_t const      *inst;
+       mschap_auth_call_env_t  *env_data;
+       MSCHAP_AUTH_METHOD      method;
+       fr_pair_t               *nt_password;
+       fr_pair_t               *smb_ctrl;
+       fr_pair_t               *cpw;
+       mschap_cpw_ctx_t        *cpw_ctx;
+} mschap_auth_ctx_t;
+
 static fr_dict_t const *dict_freeradius;
 static fr_dict_t const *dict_radius;
 
@@ -828,18 +852,13 @@ static int write_all(int fd, char const *buf, int len) {
  * Perform an MS-CHAP2 password change
  */
 
-static int CC_HINT(nonnull (1, 2, 4, 5)) do_mschap_cpw(rlm_mschap_t const *inst,
-                                                      request_t *request,
-#ifdef WITH_TLS
-                                                      fr_pair_t *nt_password,
-#else
-                                                      UNUSED fr_pair_t *nt_password,
-#endif
-                                                      uint8_t *new_nt_password,
-                                                      uint8_t *old_nt_hash,
-                                                      MSCHAP_AUTH_METHOD method)
+static int CC_HINT(nonnull) do_mschap_cpw(rlm_mschap_t const *inst, request_t *request, mschap_auth_ctx_t *auth_ctx,
+                                         uint8_t *new_nt_password, uint8_t *old_nt_hash)
 {
-       if (inst->ntlm_cpw && method != AUTH_INTERNAL) {
+       mschap_cpw_ctx_t        *cpw_ctx = auth_ctx->cpw_ctx;
+       fr_value_box_t          *vb;
+
+       if (inst->ntlm_cpw && auth_ctx->method != AUTH_INTERNAL) {
                /*
                 * we're going to run ntlm_auth in helper-mode
                 * we're expecting to use the ntlm-change-password-1 protocol
@@ -863,13 +882,11 @@ static int CC_HINT(nonnull (1, 2, 4, 5)) do_mschap_cpw(rlm_mschap_t const *inst,
                 * Password-Change-Error: blah
                 */
 
-               int to_child=-1;
-               int from_child=-1;
-               pid_t pid, child_pid;
-               int status, len;
-               char buf[2048];
-               char *pmsg;
-               char const *emsg;
+               int             status, len, to_child=-1, from_child=-1;
+               pid_t           pid, child_pid;
+               char            buf[2048];
+               char            *pmsg;
+               char const      *emsg;
 
                RDEBUG2("Doing MS-CHAPv2 password change via ntlm_auth helper");
 
@@ -887,31 +904,24 @@ static int CC_HINT(nonnull (1, 2, 4, 5)) do_mschap_cpw(rlm_mschap_t const *inst,
                 * write the stuff to the client
                 */
 
-               if (inst->ntlm_cpw_username) {
-                       len = xlat_eval(buf, sizeof(buf) - 2, request, inst->ntlm_cpw_username, NULL, NULL);
-                       if (len < 0) {
-                               goto ntlm_auth_err;
-                       }
+               vb = fr_value_box_list_head(&cpw_ctx->cpw_user);
+               if (!vb) goto ntlm_auth_err;
 
-                       if (write_all(to_child, buf, len) != len) {
-                               REDEBUG("Failed to write username to child");
-                               goto ntlm_auth_err;
-                       }
-               } else {
-                       RWDEBUG2("No ntlm_auth username set, passchange will definitely fail!");
+               if (write_all(to_child, vb->vb_strvalue, vb->vb_length) != (int)vb->vb_length) {
+                       REDEBUG("Failed to write username to child");
+                       goto ntlm_auth_err;
                }
 
-               if (inst->ntlm_cpw_domain) {
-                       len = xlat_eval(buf, sizeof(buf) - 2, request, inst->ntlm_cpw_domain, NULL, NULL);
-                       if (len < 0) {
-                               goto ntlm_auth_err;
-                       }
+               if (auth_ctx->env_data->ntlm_cpw_domain) {
+                       vb = fr_value_box_list_head(&cpw_ctx->cpw_domain);
+                       if (!vb) goto no_domain;
 
-                       if (write_all(to_child, buf, len) != len) {
+                       if (write_all(to_child, vb->vb_strvalue, vb->vb_length) != (int)vb->vb_length) {
                                REDEBUG("Failed to write domain to child");
                                goto ntlm_auth_err;
                        }
                } else {
+               no_domain:
                        RWDEBUG2("No ntlm_auth domain set, username must be full-username to work");
                }
 
@@ -1011,180 +1021,33 @@ ntlm_auth_err:
                 *  %exec('/path/to', %mschap(User-Name), %{MS-CHAP-New-Password})"
                 *
                 */
-       } else if (inst->local_cpw) {
 #ifdef WITH_TLS
-               fr_pair_t       *new_pass, *new_hash;
-               uint8_t         *p, *q;
-               char            *x;
-               size_t          i;
-               size_t          passlen;
-               ssize_t         result_len;
-               char            result[253];
-               uint8_t         nt_pass_decrypted[516], old_nt_hash_expected[NT_DIGEST_LENGTH];
-               size_t          len = 0;
-               EVP_CIPHER_CTX  *evp_ctx;
-               int             ntlen = sizeof(nt_pass_decrypted);
-
-               if (!nt_password) {
-                       RDEBUG2("Local MS-CHAPv2 password change requires Password.NT attribute");
-                       return -1;
-               } else {
-                       RDEBUG2("Doing MS-CHAPv2 password change locally");
-               }
-
-               MEM(evp_ctx = EVP_CIPHER_CTX_new());
-
-               if (unlikely(EVP_CIPHER_CTX_set_key_length(evp_ctx, nt_password->vp_length)) != 1) {
-                       fr_tls_strerror_printf(NULL);
-                       RPERROR("Failed setting key length");
-                       return -1;
-               }
-
-               if (unlikely(EVP_EncryptInit_ex(evp_ctx, EVP_rc4(), NULL, nt_password->vp_octets, NULL) != 1)) {
-                       fr_tls_strerror_printf(NULL);
-                       RPERROR("Failed initialising RC4 ctx");
-                       return -1;
-               }
-
-
-               if (unlikely(EVP_EncryptUpdate(evp_ctx, nt_pass_decrypted, &ntlen, new_nt_password, ntlen) != 1)) {
-                       fr_tls_strerror_printf(NULL);
-                       RPERROR("Failed ingesting new password");
-                       return -1;
-               }
-
-               EVP_CIPHER_CTX_free(evp_ctx);
-
-               /*
-                *  pwblock is
-                *  512-N bytes random pad
-                *  N bytes password as utf-16-le
-                *  4 bytes - N as big-endian int
-                */
-               passlen = nt_pass_decrypted[512];
-               passlen += nt_pass_decrypted[513] << 8;
-               if ((nt_pass_decrypted[514] != 0) ||
-                   (nt_pass_decrypted[515] != 0)) {
-                       REDEBUG("Decrypted new password blob claims length > 65536, "
-                               "probably an invalid Password.NT");
-                       return -1;
-               }
-
-               /*
-                *  Sanity check - passlen positive and <= 512 if not, crypto has probably gone wrong
-                */
-               if (passlen > 512) {
-                       REDEBUG("Decrypted new password blob claims length %zu > 512, "
-                               "probably an invalid Password.NT", passlen);
-                       return -1;
-               }
-
-               p = nt_pass_decrypted + 512 - passlen;
+       } else if (auth_ctx->env_data->local_cpw) {
+               RDEBUG2("Doing MS-CHAPv2 password change locally");
 
-               /*
-                *  The new NT hash - this should be preferred over the
-                *  cleartext password as it avoids unicode hassles.
-                */
-               MEM(pair_update_request(&new_hash, attr_ms_chap_new_nt_password) >= 0);
-               MEM(fr_pair_value_mem_alloc(new_hash, &q, NT_DIGEST_LENGTH, false) == 0);
-               fr_md4_calc(q, p, passlen);
+               vb = fr_value_box_list_head(&cpw_ctx->local_cpw_result);
 
-               /*
-                *  Check that nt_password encrypted with new_hash
-                *  matches the old_hash value from the client.
-                */
-               smbhash(old_nt_hash_expected, nt_password->vp_octets, q);
-               smbhash(old_nt_hash_expected + 8, nt_password->vp_octets + 8, q + 7);
-               if (memcmp(old_nt_hash_expected, old_nt_hash, NT_DIGEST_LENGTH)!=0) {
-                       REDEBUG("Old NT hash value from client does not match our value");
+               if (!vb){
                        return -1;
-               }
-
-               /*
-                *  The new cleartext password, which is utf-16 do some unpleasant vileness
-                *  to turn it into utf8 without pulling in libraries like iconv.
-                *
-                *  First pass: get the length of the converted string.
-                */
-               MEM(pair_update_request(&new_pass, attr_ms_chap_new_cleartext_password) >= 0);
-               new_pass->vp_length = 0;
-
-               i = 0;
-               len = 0;
-               while (i < passlen) {
-                       int c;
-
-                       c = p[i++];
-                       c += p[i++] << 8;
-
-                       /*
-                        *  Gah. nasty. maybe we should just pull in iconv?
-                        */
-                       if (c < 0x7f) {
-                               len++;
-                       } else if (c < 0x7ff) {
-                               len += 2;
-                       } else {
-                               len += 3;
-                       }
-               }
-
-               MEM(fr_pair_value_bstr_alloc(new_pass, &x, len, true) == 0);
-
-               /*
-                *      Second pass: convert the characters from UTF-16 to UTF-8.
-                */
-               i = 0;
-               while (i < passlen) {
-                       int c;
-
-                       c = p[i++];
-                       c += p[i++] << 8;
-
-                       /*
-                        *  Gah. nasty. maybe we should just pull in iconv?
-                        */
-                       if (c < 0x7f) {
-                               *x++ = c;
-
-                       } else if (c < 0x7ff) {
-                               *x++ = 0xc0 + (c >> 6);
-                               *x++ = 0x80 + (c & 0x3f);
-
-                       } else {
-                               *x++ = 0xe0 + (c >> 12);
-                               *x++ = 0x80 + ((c>>6) & 0x3f);
-                               *x++ = 0x80 + (c & 0x3f);
-                       }
-               }
-
-               *x = '\0';
-
-               /* Perform the xlat */
-               result_len = xlat_eval(result, sizeof(result), request, inst->local_cpw, NULL, NULL);
-               if (result_len < 0){
-                       return -1;
-               } else if (result_len == 0) {
-                       REDEBUG("Local MS-CHAPv2 password change - xlat didn't give any result, assuming failure");
+               } else if (vb->vb_length == 0) {
+                       REDEBUG("Local MS-CHAPv2 password change - didn't give any result, assuming failure");
                        return -1;
                }
 
-               RDEBUG2("MS-CHAPv2 password change succeeded: %s", result);
+               RDEBUG2("MS-CHAPv2 password change succeeded: %pV", vb);
 
                /*
                 *  Update the Password.NT attribute with the new hash this lets us
                 *  fall through to the authentication code using the new hash,
                 *  not the old one.
                 */
-               fr_pair_value_memdup(nt_password, new_hash->vp_octets, new_hash->vp_length, false);
+               fr_pair_value_memdup(auth_ctx->nt_password, auth_ctx->cpw_ctx->new_hash->vp_octets,
+                                    auth_ctx->cpw_ctx->new_hash->vp_length, false);
 
                /*
                 *  Rock on! password change succeeded.
                 */
                return 0;
-#else
-               REDEBUG("Local MS-CHAPv2 password changes require OpenSSL support");
-               return -1;
 #endif
        } else {
                REDEBUG("MS-CHAPv2 password change not configured");
@@ -1639,10 +1502,7 @@ static unlang_action_t mschap_error(rlm_rcode_t *p_result, rlm_mschap_t const *i
 
 /** Find a Password.NT value, or create one from a Password.Cleartext, or Password.With-Header attribute
  *
- * @param[out] ephemeral       Whether we created a new password
- *                             attribute.  Usually the caller will
- *                             either want to insert this into a
- *                             list or free it.
+ * @param[in] ctx              to allocate ephemeral passwords in.
  * @param[out] out             Our new Password.NT.
  * @param[in] inst             Module configuration.
  * @param[in] request          The current request.
@@ -1650,15 +1510,16 @@ static unlang_action_t mschap_error(rlm_rcode_t *p_result, rlm_mschap_t const *i
  *     - 0 on success.
  *     - -1 on failure.
  */
-static int CC_HINT(nonnull(1, 2, 3)) nt_password_find(bool *ephemeral, fr_pair_t **out,
+static int CC_HINT(nonnull(1, 2, 3)) nt_password_find(TALLOC_CTX *ctx, fr_pair_t **out,
                                                      rlm_mschap_t const *inst, request_t *request)
 {
        fr_pair_t               *password;
        fr_dict_attr_t const    *allowed_passwords[] = { attr_cleartext_password, attr_nt_password };
+       bool                    ephemeral;
 
        *out = NULL;            /* Init output pointer */
 
-       password = password_find(ephemeral, request, request,
+       password = password_find(&ephemeral, ctx, request,
                                 allowed_passwords, NUM_ELEMENTS(allowed_passwords), inst->normify);
        if (!password) {
                if (inst->method == AUTH_INTERNAL) {
@@ -1668,7 +1529,7 @@ static int CC_HINT(nonnull(1, 2, 3)) nt_password_find(bool *ephemeral, fr_pair_t
                         *      When EAP-MSCHAPv2 supports sections.
                         */
                        if (request->parent) {
-                               password = password_find(ephemeral, request->parent, request->parent,
+                               password = password_find(&ephemeral, ctx, request->parent,
                                                         allowed_passwords,
                                                         NUM_ELEMENTS(allowed_passwords), inst->normify);
                                if (password) goto found_password;
@@ -1695,14 +1556,14 @@ found_password:
                int             ret;
                fr_pair_t       *nt_password;
 
-               MEM(nt_password = fr_pair_afrom_da(request->request_ctx, attr_nt_password));
+               MEM(nt_password = fr_pair_afrom_da(ctx, attr_nt_password));
                MEM(fr_pair_value_mem_alloc(nt_password, &p, NT_DIGEST_LENGTH, false) == 0);
                ret = mschap_nt_password_hash(p, password->vp_strvalue);
 
                if (ret < 0) {
                        RERROR("Failed generating Password.NT");
                        talloc_free(nt_password);
-                       if (*ephemeral) TALLOC_FREE(password);
+                       if (ephemeral) TALLOC_FREE(password);
                        return -1;
                }
 
@@ -1713,9 +1574,8 @@ found_password:
                        RDEBUG2("Hashed &control.%s to create %s", attr_nt_password->name, password->da->name);
                }
 
-               if (*ephemeral) TALLOC_FREE(password);
+               if (ephemeral) TALLOC_FREE(password);
 
-               *ephemeral = true;      /* We generated a temporary password */
                *out = nt_password;
 
                return 0;
@@ -1740,13 +1600,42 @@ found_password:
 static unlang_action_t CC_HINT(nonnull) mschap_process_cpw_request(rlm_rcode_t *p_result,
                                                                   rlm_mschap_t const *inst,
                                                                   request_t *request,
-                                                                  fr_pair_t *cpw,
-                                                                  fr_pair_t *nt_password,
-                                                                  mschap_auth_call_env_t *env_data)
+                                                                  mschap_auth_ctx_t *auth_ctx)
 {
-       uint8_t         new_nt_encrypted[516], old_nt_encrypted[NT_DIGEST_LENGTH];
-       fr_pair_t       *nt_enc=NULL;
-       int             seq, new_nt_enc_len;
+       mschap_auth_call_env_t  *env_data = auth_ctx->env_data;
+       mschap_cpw_ctx_t        *cpw_ctx = auth_ctx->cpw_ctx;
+
+       /*
+        *      Perform the actual password change
+        */
+       if (do_mschap_cpw(inst, request, auth_ctx, cpw_ctx->new_nt_encrypted, cpw_ctx->old_nt_hash) < 0) {
+               char buffer[128];
+
+               REDEBUG("Password change failed");
+
+               if (env_data->chap_error) {
+                       snprintf(buffer, sizeof(buffer), "E=709 R=0 M=Password change failed");
+                       mschap_add_reply(request, auth_ctx->cpw->vp_octets[1],
+                                        tmpl_attr_tail_da(env_data->chap_error), buffer, strlen(buffer));
+               }
+
+               RETURN_MODULE_REJECT;
+       }
+
+       RDEBUG2("Password change successful");
+
+       RETURN_MODULE_OK;
+}
+
+/** Validate data required for change password requests.
+ *
+ */
+static int mschap_cpw_prepare(request_t *request, mschap_auth_ctx_t *auth_ctx)
+{
+       mschap_auth_call_env_t  *env_data = auth_ctx->env_data;
+       mschap_cpw_ctx_t        *cpw_ctx;
+       fr_pair_t               *nt_enc = NULL;
+       int                     seq, new_nt_enc_len;
 
        /*
         *      mschap2 password change request.
@@ -1758,16 +1647,19 @@ static unlang_action_t CC_HINT(nonnull) mschap_process_cpw_request(rlm_rcode_t *
         */
        RDEBUG2("MS-CHAPv2 password change request received");
 
-       if (cpw->vp_length != 68) {
-               REDEBUG("%s has the wrong format: length %zu != 68", env_data->chap2_cpw->name, cpw->vp_length);
-               RETURN_MODULE_INVALID;
+       if (auth_ctx->cpw->vp_length != 68) {
+               REDEBUG("%s has the wrong format: length %zu != 68", env_data->chap2_cpw->name, auth_ctx->cpw->vp_length);
+               return -1;
        }
 
-       if (cpw->vp_octets[0] != 7) {
-               REDEBUG("%s has the wrong format: code %d != 7", env_data->chap2_cpw->name, cpw->vp_octets[0]);
-               RETURN_MODULE_INVALID;
+       if (auth_ctx->cpw->vp_octets[0] != 7) {
+               REDEBUG("%s has the wrong format: code %d != 7", env_data->chap2_cpw->name, auth_ctx->cpw->vp_octets[0]);
+               return -1;
        }
 
+       MEM(auth_ctx->cpw_ctx = talloc_zero(auth_ctx, mschap_cpw_ctx_t));
+       cpw_ctx = auth_ctx->cpw_ctx;
+
        /*
         *      Look for the new (encrypted) password.
         *
@@ -1785,12 +1677,12 @@ static unlang_action_t CC_HINT(nonnull) mschap_process_cpw_request(rlm_rcode_t *
                                                           tmpl_attr_tail_da(env_data->chap_nt_enc_pw)))) {
                        if (nt_enc->vp_length < 4) {
                                REDEBUG("%s with invalid format", env_data->chap_nt_enc_pw->name);
-                               RETURN_MODULE_INVALID;
+                               return -1;
                        }
 
                        if (nt_enc->vp_octets[0] != 6) {
                                REDEBUG("%s with invalid format", env_data->chap_nt_enc_pw->name);
-                               RETURN_MODULE_INVALID;
+                               return -1;
                        }
 
                        if ((nt_enc->vp_octets[2] == 0) && (nt_enc->vp_octets[3] == seq)) {
@@ -1801,21 +1693,21 @@ static unlang_action_t CC_HINT(nonnull) mschap_process_cpw_request(rlm_rcode_t *
 
                if (!found) {
                        REDEBUG("Could not find %s with sequence number %d", env_data->chap_nt_enc_pw->name, seq);
-                       RETURN_MODULE_INVALID;
+                       return -1;
                }
 
-               if ((new_nt_enc_len + nt_enc->vp_length - 4) > sizeof(new_nt_encrypted)) {
+               if ((new_nt_enc_len + nt_enc->vp_length - 4) > sizeof(cpw_ctx->new_nt_encrypted)) {
                        REDEBUG("Unpacked %s length > 516", env_data->chap_nt_enc_pw->name);
-                       RETURN_MODULE_INVALID;
+                       return -1;
                }
 
-               memcpy(new_nt_encrypted + new_nt_enc_len, nt_enc->vp_octets + 4, nt_enc->vp_length - 4);
+               memcpy(cpw_ctx->new_nt_encrypted + new_nt_enc_len, nt_enc->vp_octets + 4, nt_enc->vp_length - 4);
                new_nt_enc_len += nt_enc->vp_length - 4;
        }
 
        if (new_nt_enc_len != 516) {
-               REDEBUG("Unpacked %s length != 516", env_data->chap_nt_enc_pw->name);
-               RETURN_MODULE_INVALID;
+               REDEBUG("Unpacked %s length is %d - should be 516", env_data->chap_nt_enc_pw->name, new_nt_enc_len);
+               return -1;
        }
 
        /*
@@ -1832,27 +1724,10 @@ static unlang_action_t CC_HINT(nonnull) mschap_process_cpw_request(rlm_rcode_t *
         *      2 octets  - flags (ignored)
         */
 
-       memcpy(old_nt_encrypted, cpw->vp_octets + 2, sizeof(old_nt_encrypted));
+       memcpy(cpw_ctx->old_nt_hash, auth_ctx->cpw->vp_octets + 2, sizeof(cpw_ctx->old_nt_hash));
 
        RDEBUG2("Password change payload valid");
-
-       /*
-        *      Perform the actual password change
-        */
-       if (do_mschap_cpw(inst, request, nt_password, new_nt_encrypted, old_nt_encrypted, inst->method) < 0) {
-               char buffer[128];
-
-               REDEBUG("Password change failed");
-
-               snprintf(buffer, sizeof(buffer), "E=709 R=0 M=Password change failed");
-               mschap_add_reply(request, cpw->vp_octets[1], tmpl_attr_tail_da(env_data->chap_error), buffer, strlen(buffer));
-
-               RETURN_MODULE_REJECT;
-       }
-
-       RDEBUG2("Password change successful");
-
-       RETURN_MODULE_OK;
+       return 0;
 }
 
 static CC_HINT(nonnull(1,2,3,4,5,8,9)) unlang_action_t mschap_process_response(rlm_rcode_t *p_result,
@@ -2066,104 +1941,28 @@ static unlang_action_t CC_HINT(nonnull(1,2,3,4,5,8,9)) mschap_process_v2_respons
                RETURN_MODULE_OK;
 }
 
-/*
- *     mod_authenticate() - authenticate user based on given
- *     attributes and configuration.
- *     We will try to find out password in configuration
- *     or in configured passwd file.
- *     If one is found we will check paraneters given by NAS.
+/** Complete mschap authentication after any tmpls have been expanded.
  *
- *     If SMB-Account-Ctrl is not set to ACB_PWNOTREQ we must have
- *     one of:
- *             PAP:      User-Password or
- *             MS-CHAP:  MS-CHAP-Challenge and MS-CHAP-Response or
- *             MS-CHAP2: MS-CHAP-Challenge and MS-CHAP2-Response
- *     In case of password mismatch or locked account we MAY return
- *     MS-CHAP-Error for MS-CHAP or MS-CHAP v2
- *     If MS-CHAP2 succeeds we MUST return MS-CHAP2-Success
  */
-static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request)
+static unlang_action_t mod_authenticate_resume(rlm_rcode_t *p_result, UNUSED int *priority, request_t *request, void *uctx)
 {
-       rlm_mschap_t const      *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_mschap_t);
-       mschap_auth_call_env_t  *env_data = talloc_get_type_abort(mctx->env_data, mschap_auth_call_env_t);
+       mschap_auth_ctx_t       *auth_ctx = talloc_get_type_abort(uctx, mschap_auth_ctx_t);
+       mschap_auth_call_env_t  *env_data = talloc_get_type_abort(auth_ctx->env_data, mschap_auth_call_env_t);
+       rlm_mschap_t const      *inst = talloc_get_type_abort_const(auth_ctx->inst, rlm_mschap_t);
        fr_pair_t               *challenge = NULL;
        fr_pair_t               *response = NULL;
-       fr_pair_t               *cpw = NULL;
        fr_pair_t               *parent;
-       fr_pair_t               *nt_password = NULL, *smb_ctrl;
        uint8_t                 nthashhash[NT_DIGEST_LENGTH];
        int                     mschap_version = 0;
-
-       MSCHAP_AUTH_METHOD      method;
-       bool                    ephemeral = false;
        rlm_rcode_t             rcode = RLM_MODULE_OK;
 
-       /*
-        *      If we have ntlm_auth configured, use it unless told
-        *      otherwise
-        */
-       method = inst->method;
-
-       /*
-        *      If we have an ntlm_auth configuration, then we may
-        *      want to suppress it.
-        */
-       if (method != AUTH_INTERNAL) {
-               fr_pair_t *vp = fr_pair_find_by_da(&request->control_pairs, NULL, attr_ms_chap_use_ntlm_auth);
-               if (vp && (vp->vp_uint8 <= AUTH_AUTO)) method = vp->vp_uint8;
-       }
-
-       /*
-        *      Find the SMB-Account-Ctrl attribute, or the
-        *      SMB-Account-Ctrl-Text attribute.
-        */
-       smb_ctrl = fr_pair_find_by_da(&request->control_pairs, NULL, attr_smb_account_ctrl);
-       if (!smb_ctrl) {
-               fr_pair_t *smb_account_ctrl_text;
-
-               smb_account_ctrl_text = fr_pair_find_by_da(&request->control_pairs, NULL, attr_smb_account_ctrl_text);
-               if (smb_account_ctrl_text) {
-                       MEM(pair_append_control(&smb_ctrl, attr_smb_account_ctrl) >= 0);
-                       smb_ctrl->vp_uint32 = pdb_decode_acct_ctrl(smb_account_ctrl_text->vp_strvalue);
-               }
-       }
-
-       /*
-        *      We're configured to do MS-CHAP authentication.
-        *      and account control information exists.  Enforce it.
-        */
-       if (smb_ctrl) {
-               /*
-                *      Password is not required.
-                */
-               if ((smb_ctrl->vp_uint32 & ACB_PWNOTREQ) != 0) {
-                       RDEBUG2("SMB-Account-Ctrl says no password is required");
-                       RETURN_MODULE_OK;
-               }
-       }
-
-       /*
-        *      Look for or create an Password.NT
-        *
-        *      Password.NT can be NULL here if we didn't find an
-        *      input attribute, and we're calling out to an
-        *      external password store.
-        */
-       if (nt_password_find(&ephemeral, &nt_password, mctx->inst->data, request) < 0) RETURN_MODULE_FAIL;
-
-       /*
-        *      Check to see if this is a change password request, and process
-        *      it accordingly if so.
-        */
-       if (env_data->chap2_cpw) cpw = fr_pair_find_by_da_nested(&request->request_pairs, NULL,
-                                                                tmpl_attr_tail_da(env_data->chap2_cpw));
-       if (cpw) {
+       if (auth_ctx->cpw) {
                uint8_t         *p;
 
                /*
                 *      Password change does require the NT password
                 */
-               if (!nt_password) {
+               if (!auth_ctx->nt_password) {
                        REDEBUG("Missing Password.NT - required for change password request");
                        RETURN_MODULE_FAIL;
                }
@@ -2172,7 +1971,7 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result,
                        RETURN_MODULE_INVALID;
                }
 
-               mschap_process_cpw_request(&rcode, mctx->inst->data, request, cpw, nt_password, env_data);
+               mschap_process_cpw_request(&rcode, inst, request, auth_ctx);
                if (rcode != RLM_MODULE_OK) goto finish;
 
                /*
@@ -2180,9 +1979,9 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result,
                 *      obviously the password change action will need
                 *      to have cleared this bit in the config/SQL/wherever.
                 */
-               if (smb_ctrl && smb_ctrl->vp_uint32 & ACB_FR_EXPIRED) {
+               if (auth_ctx->smb_ctrl && auth_ctx->smb_ctrl->vp_uint32 & ACB_FR_EXPIRED) {
                        RDEBUG2("Clearing expiry bit in SMB-Acct-Ctrl to allow authentication");
-                       smb_ctrl->vp_uint32 &= ~ACB_FR_EXPIRED;
+                       auth_ctx->smb_ctrl->vp_uint32 &= ~ACB_FR_EXPIRED;
                }
 
                /*
@@ -2191,19 +1990,19 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result,
                 *      continue with the authentication.
                 */
                MEM(pair_update_request(&response, tmpl_attr_tail_da(env_data->chap2_response)) >= 0);
-               MEM(fr_pair_value_mem_alloc(response, &p, 50, cpw->vp_tainted) == 0);
+               MEM(fr_pair_value_mem_alloc(response, &p, 50, auth_ctx->cpw->vp_tainted) == 0);
 
                /* ident & flags */
-               p[0] = cpw->vp_octets[1];
+               p[0] = auth_ctx->cpw->vp_octets[1];
                p[1] = 0;
                /* peer challenge and client NT response */
-               memcpy(p + 2, cpw->vp_octets + 18, 48);
+               memcpy(p + 2, auth_ctx->cpw->vp_octets + 18, 48);
        }
 
        challenge = fr_pair_find_by_da_nested(&request->request_pairs, NULL, tmpl_attr_tail_da(env_data->chap_challenge));
        if (!challenge) {
                REDEBUG("&control.Auth-Type = %s set for a request that does not contain &%s",
-                       mctx->inst->name, env_data->chap_challenge->name);
+                       auth_ctx->name, env_data->chap_challenge->name);
                rcode = RLM_MODULE_INVALID;
                goto finish;
        }
@@ -2221,21 +2020,21 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result,
                mschap_process_response(&rcode,
                                        &mschap_version, nthashhash,
                                        inst, request,
-                                       smb_ctrl, nt_password,
+                                       auth_ctx->smb_ctrl, auth_ctx->nt_password,
                                        challenge, response,
-                                       method, env_data);
+                                       auth_ctx->method, auth_ctx->env_data);
                if (rcode != RLM_MODULE_OK) goto finish;
        } else if ((response = fr_pair_find_by_da_nested(&parent->vp_group, NULL, tmpl_attr_tail_da(env_data->chap2_response)))) {
                mschap_process_v2_response(&rcode,
                                           &mschap_version, nthashhash,
                                           inst, request,
-                                          smb_ctrl, nt_password,
+                                          auth_ctx->smb_ctrl, auth_ctx->nt_password,
                                           challenge, response,
-                                          method, env_data);
+                                          auth_ctx->method, auth_ctx->env_data);
                if (rcode != RLM_MODULE_OK) goto finish;
        } else {                /* Neither CHAPv1 or CHAPv2 response: die */
                REDEBUG("&control.Auth-Type = %s set for a request that does not contain &%s or &%s attributes",
-                       mctx->inst->name, env_data->chap_response->name, env_data->chap2_response->name);
+                       auth_ctx->name, env_data->chap_response->name, env_data->chap2_response->name);
                rcode = RLM_MODULE_INVALID;
                goto finish;
        }
@@ -2288,11 +2087,301 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result,
        } /* else we weren't asked to use MPPE */
 
 finish:
-       if (ephemeral) TALLOC_FREE(nt_password);
-
        RETURN_MODULE_RCODE(rcode);
 }
 
+/** When changing passwords using the ntlm_auth helper, evaluate the domain tmpl
+ *
+ */
+static unlang_action_t mod_authenticate_domain_tmpl_push(rlm_rcode_t *p_result, UNUSED int *priority,
+                                                        request_t *request, void *uctx)
+{
+       mschap_auth_ctx_t       *auth_ctx = talloc_get_type_abort(uctx, mschap_auth_ctx_t);
+       mschap_auth_call_env_t  *env_data = talloc_get_type_abort(auth_ctx->env_data, mschap_auth_call_env_t);
+
+       fr_value_box_list_init(&auth_ctx->cpw_ctx->cpw_domain);
+       if (unlang_tmpl_push(auth_ctx, &auth_ctx->cpw_ctx->cpw_domain, request,
+                            env_data->ntlm_cpw_domain, NULL) < 0) RETURN_MODULE_FAIL;
+
+       return UNLANG_ACTION_PUSHED_CHILD;
+}
+
+#ifdef WITH_TLS
+/** Decrypt the new cleartext password when handling change password requests
+ *
+ */
+static int mschap_new_pass_decrypt(request_t *request, mschap_auth_ctx_t *auth_ctx)
+{
+       EVP_CIPHER_CTX  *evp_ctx;
+       uint8_t         nt_pass_decrypted[516], old_nt_hash_expected[NT_DIGEST_LENGTH];
+       int             c, ntlen = sizeof(nt_pass_decrypted);
+       size_t          passlen, i = 0, len = 0;
+       char            *x;
+       uint8_t         *p, *q;
+       fr_pair_t       *new_pass;
+
+       MEM(evp_ctx = EVP_CIPHER_CTX_new());
+
+       if (unlikely(EVP_EncryptInit_ex(evp_ctx, EVP_rc4(), NULL, auth_ctx->nt_password->vp_octets, NULL) != 1)) {
+               fr_tls_strerror_printf(NULL);
+               RPERROR("Failed initialising RC4 ctx");
+               return -1;
+       }
+
+       if (unlikely(EVP_CIPHER_CTX_set_key_length(evp_ctx, auth_ctx->nt_password->vp_length)) != 1) {
+               fr_tls_strerror_printf(NULL);
+               RPERROR("Failed setting key length");
+               return -1;
+       }
+
+       if (unlikely(EVP_EncryptUpdate(evp_ctx, nt_pass_decrypted, &ntlen, auth_ctx->cpw_ctx->new_nt_encrypted, ntlen) != 1)) {
+               fr_tls_strerror_printf(NULL);
+               RPERROR("Failed ingesting new password");
+               return -1;
+       }
+
+       EVP_CIPHER_CTX_free(evp_ctx);
+
+       /*
+        *  pwblock is
+        *  512-N bytes random pad
+        *  N bytes password as utf-16-le
+        *  4 bytes - N as big-endian int
+        */
+       passlen = nt_pass_decrypted[512];
+       passlen += nt_pass_decrypted[513] << 8;
+       if ((nt_pass_decrypted[514] != 0) ||
+           (nt_pass_decrypted[515] != 0)) {
+               REDEBUG("Decrypted new password blob claims length > 65536, probably an invalid Password.NT", nt_pass_decrypted);
+               return -1;
+       }
+
+       /*
+        *  Sanity check - passlen positive and <= 512 if not, crypto has probably gone wrong
+        */
+       if (passlen > 512) {
+               REDEBUG("Decrypted new password blob claims length %zu > 512, "
+                       "probably an invalid Password.NT", passlen);
+               return -1;
+       }
+
+       p = nt_pass_decrypted + 512 - passlen;
+
+       /*
+        *  The new NT hash - this should be preferred over the
+        *  cleartext password as it avoids unicode hassles.
+        */
+       MEM(pair_update_request(&auth_ctx->cpw_ctx->new_hash, attr_ms_chap_new_nt_password) >= 0);
+       MEM(fr_pair_value_mem_alloc(auth_ctx->cpw_ctx->new_hash, &q, NT_DIGEST_LENGTH, false) == 0);
+       fr_md4_calc(q, p, passlen);
+
+       /*
+        *  Check that nt_password encrypted with new_hash
+        *  matches the old_hash value from the client.
+        */
+       smbhash(old_nt_hash_expected, auth_ctx->nt_password->vp_octets, q);
+       smbhash(old_nt_hash_expected + 8, auth_ctx->nt_password->vp_octets + 8, q + 7);
+       if (memcmp(old_nt_hash_expected, auth_ctx->cpw_ctx->old_nt_hash, NT_DIGEST_LENGTH)!=0) {
+               REDEBUG("Old NT hash value from client does not match our value");
+               RHEXDUMP1(old_nt_hash_expected, NT_DIGEST_LENGTH, "expected");
+               RHEXDUMP1(auth_ctx->cpw_ctx->old_nt_hash, NT_DIGEST_LENGTH, "got");
+               return -1;
+       }
+
+       /*
+        *  The new cleartext password, which is utf-16 do some unpleasant vileness
+        *  to turn it into utf8 without pulling in libraries like iconv.
+        *
+        *  First pass: get the length of the converted string.
+        */
+       MEM(pair_update_request(&new_pass, attr_ms_chap_new_cleartext_password) >= 0);
+       new_pass->vp_length = 0;
+
+       while (i < passlen) {
+               c = p[i++];
+               c += p[i++] << 8;
+
+               /*
+                *  Gah. nasty. maybe we should just pull in iconv?
+                */
+               if (c < 0x7f) {
+                       len++;
+               } else if (c < 0x7ff) {
+                       len += 2;
+               } else {
+                       len += 3;
+               }
+       }
+
+       MEM(fr_pair_value_bstr_alloc(new_pass, &x, len, true) == 0);
+
+       /*
+        *      Second pass: convert the characters from UTF-16 to UTF-8.
+        */
+       i = 0;
+       while (i < passlen) {
+               c = p[i++];
+               c += p[i++] << 8;
+
+               /*
+                *  Gah. nasty. maybe we should just pull in iconv?
+                */
+               if (c < 0x7f) {
+                       *x++ = c;
+
+               } else if (c < 0x7ff) {
+                       *x++ = 0xc0 + (c >> 6);
+                       *x++ = 0x80 + (c & 0x3f);
+
+               } else {
+                       *x++ = 0xe0 + (c >> 12);
+                       *x++ = 0x80 + ((c>>6) & 0x3f);
+                       *x++ = 0x80 + (c & 0x3f);
+               }
+       }
+
+       *x = '\0';
+       return 0;
+}
+#endif
+
+/*
+ *     mod_authenticate() - authenticate user based on given
+ *     attributes and configuration.
+ *     We will try to find out password in configuration
+ *     or in configured passwd file.
+ *     If one is found we will check paraneters given by NAS.
+ *
+ *     If SMB-Account-Ctrl is not set to ACB_PWNOTREQ we must have
+ *     one of:
+ *             PAP:      User-Password or
+ *             MS-CHAP:  MS-CHAP-Challenge and MS-CHAP-Response or
+ *             MS-CHAP2: MS-CHAP-Challenge and MS-CHAP2-Response
+ *     In case of password mismatch or locked account we MAY return
+ *     MS-CHAP-Error for MS-CHAP or MS-CHAP v2
+ *     If MS-CHAP2 succeeds we MUST return MS-CHAP2-Success
+ */
+static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request)
+{
+       rlm_mschap_t const      *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_mschap_t);
+       mschap_auth_call_env_t  *env_data = talloc_get_type_abort(mctx->env_data, mschap_auth_call_env_t);
+       mschap_auth_ctx_t       *auth_ctx;
+
+       MEM(auth_ctx = talloc_zero(unlang_interpret_frame_talloc_ctx(request), mschap_auth_ctx_t));
+
+       /*
+        *      If we have ntlm_auth configured, use it unless told
+        *      otherwise
+        */
+       *auth_ctx = (mschap_auth_ctx_t) {
+               .name = mctx->inst->name,
+               .inst = inst,
+               .method = inst->method,
+               .env_data = env_data,
+       };
+
+       /*
+        *      If we have an ntlm_auth configuration, then we may
+        *      want to suppress it.
+        */
+       if (auth_ctx->method != AUTH_INTERNAL) {
+               fr_pair_t *vp = fr_pair_find_by_da(&request->control_pairs, NULL, attr_ms_chap_use_ntlm_auth);
+               if (vp && (vp->vp_uint8 <= AUTH_AUTO)) auth_ctx->method = vp->vp_uint8;
+       }
+
+       /*
+        *      Find the SMB-Account-Ctrl attribute, or the
+        *      SMB-Account-Ctrl-Text attribute.
+        */
+       auth_ctx->smb_ctrl = fr_pair_find_by_da(&request->control_pairs, NULL, attr_smb_account_ctrl);
+       if (!auth_ctx->smb_ctrl) {
+               fr_pair_t *smb_account_ctrl_text;
+
+               smb_account_ctrl_text = fr_pair_find_by_da(&request->control_pairs, NULL, attr_smb_account_ctrl_text);
+               if (smb_account_ctrl_text) {
+                       MEM(pair_append_control(&auth_ctx->smb_ctrl, attr_smb_account_ctrl) >= 0);
+                       auth_ctx->smb_ctrl->vp_uint32 = pdb_decode_acct_ctrl(smb_account_ctrl_text->vp_strvalue);
+               }
+       }
+
+       /*
+        *      We're configured to do MS-CHAP authentication.
+        *      and account control information exists.  Enforce it.
+        */
+       if (auth_ctx->smb_ctrl) {
+               /*
+                *      Password is not required.
+                */
+               if ((auth_ctx->smb_ctrl->vp_uint32 & ACB_PWNOTREQ) != 0) {
+                       RDEBUG2("SMB-Account-Ctrl says no password is required");
+                       RETURN_MODULE_OK;
+               }
+       }
+
+       /*
+        *      Look for or create an Password.NT
+        *
+        *      Password.NT can be NULL here if we didn't find an
+        *      input attribute, and we're calling out to an
+        *      external password store.
+        */
+       if (nt_password_find(auth_ctx, &auth_ctx->nt_password, mctx->inst->data, request) < 0) RETURN_MODULE_FAIL;
+
+       /*
+        *      Check to see if this is a change password request, and process
+        *      it accordingly if so.
+        */
+       if (env_data->chap2_cpw) auth_ctx->cpw = fr_pair_find_by_da_nested(&request->request_pairs, NULL,
+                                                                          tmpl_attr_tail_da(env_data->chap2_cpw));
+       if (auth_ctx->cpw) {
+               /*
+                *      Password change does require the NT password
+                */
+               if (!auth_ctx->nt_password) {
+                       REDEBUG("Missing Password.NT - required for change password request");
+                       RETURN_MODULE_FAIL;
+               }
+
+               if (mschap_cpw_prepare(request, auth_ctx) < 0) RETURN_MODULE_FAIL;
+
+               switch (auth_ctx->method) {
+               case AUTH_INTERNAL:
+#ifdef WITH_TLS
+                       if (mschap_new_pass_decrypt(request, auth_ctx) < 0) RETURN_MODULE_FAIL;
+                       if (unlang_function_push(request, NULL,  mod_authenticate_resume, NULL, 0,
+                                                UNLANG_SUB_FRAME, auth_ctx) < 0) RETURN_MODULE_FAIL;
+
+                       fr_value_box_list_init(&auth_ctx->cpw_ctx->local_cpw_result);
+                       if (unlang_tmpl_push(auth_ctx, &auth_ctx->cpw_ctx->local_cpw_result, request,
+                                            env_data->local_cpw, NULL) < 0) RETURN_MODULE_FAIL;
+                       break;
+#else
+                       REDEBUG("Local MS-CHAPv2 password changes require OpenSSL support");
+                       RETURN_MODULE_INVALID;
+#endif
+
+               default:
+                       if (!env_data->ntlm_cpw_username) {
+                               REDEBUG("No ntlm_auth username set, passchange will definitely fail!");
+                               RETURN_MODULE_FAIL;
+                       }
+
+                       if (unlang_function_push(request, env_data->ntlm_cpw_domain ? mod_authenticate_domain_tmpl_push : NULL,
+                                                mod_authenticate_resume, NULL, 0,
+                                                UNLANG_SUB_FRAME, auth_ctx) < 0) RETURN_MODULE_FAIL;
+
+                       fr_value_box_list_init(&auth_ctx->cpw_ctx->cpw_user);
+                       if (unlang_tmpl_push(auth_ctx, &auth_ctx->cpw_ctx->cpw_user, request,
+                                            env_data->ntlm_cpw_username, NULL) < 0) RETURN_MODULE_FAIL;
+                       break;
+               }
+
+               return UNLANG_ACTION_PUSHED_CHILD;
+       }
+
+       return mod_authenticate_resume(p_result, NULL, request, auth_ctx);
+}
+
 /*
  *     Create instance for our module. Allocate space for
  *     instance structure and read configuration parameters
index 6231a816e726e4ce8681eacccbe412e41d17e045..21ccb9790db569957f79dadd7d50677ab2c7854d 100644 (file)
@@ -51,9 +51,6 @@ typedef struct {
        char const              *ntlm_auth;
        fr_time_delta_t         ntlm_auth_timeout;
        char const              *ntlm_cpw;
-       char const              *ntlm_cpw_username;
-       char const              *ntlm_cpw_domain;
-       char const              *local_cpw;
 
        bool                    allow_retry;
        char const              *retry_msg;
@@ -84,4 +81,7 @@ typedef struct {
        tmpl_t const    *chap_nt_enc_pw;
        fr_value_box_t  wb_username;
        fr_value_box_t  wb_domain;
+       tmpl_t const    *ntlm_cpw_username;
+       tmpl_t const    *ntlm_cpw_domain;
+       tmpl_t const    *local_cpw;
 } mschap_auth_call_env_t;