dsdb: Allow password history and password changes without an NT hash
authorAndrew Bartlett <abartlet@samba.org>
Mon, 31 Jan 2022 01:08:13 +0000 (14:08 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Sun, 26 Jun 2022 22:10:29 +0000 (22:10 +0000)
We now allow this to be via the ENCTYPE_AES256_CTS_HMAC_SHA1_96 hash instead
which allows us to decouple Samba from the unsalted NT hash for
organisations that are willing to take this step (for user accounts).

(History checking is limited to the last three passwords only, as
ntPwdHistory is limited to NT hash values, and the PrimaryKerberosCtr4
package only stores three sets of keys.)

Since we don't store a salt per-key, but only a single salt, the check
will fail for a previous password if the account was renamed prior to a
newer password being set.

Pair-Programmed-With: Stefan Metzmacher <metze@samba.org>

Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Signed-off-by: Stefan Metzmacher <metze@samba.org>
Reviewed-by: Stefan Metzmacher <metze@samba.org>
12 files changed:
docs-xml/smbdotconf/security/nt_hash_store.xml [new file with mode: 0644]
docs-xml/smbdotconf/security/ntlmauth.xml
lib/param/loadparm.c
lib/param/loadparm.h
lib/param/param_table.c
selftest/knownfail.d/nt-hash-support-gone
selftest/knownfail.d/password_settings [deleted file]
selftest/target/Samba4.pm
source3/param/loadparm.c
source4/dsdb/samdb/ldb_modules/password_hash.c
source4/dsdb/samdb/ldb_modules/wscript_build_server
source4/dsdb/tests/python/password_settings.py

diff --git a/docs-xml/smbdotconf/security/nt_hash_store.xml b/docs-xml/smbdotconf/security/nt_hash_store.xml
new file mode 100644 (file)
index 0000000..d7ed705
--- /dev/null
@@ -0,0 +1,70 @@
+<samba:parameter name="nt hash store"
+                 context="G"
+                 type="enum"
+                 enumlist="enum_nt_hash_store"
+                 xmlns:samba="http://www.samba.org/samba/DTD/samba-doc">
+<description>
+    <para>This parameter determines whether or not <citerefentry><refentrytitle>samba</refentrytitle>
+    <manvolnum>8</manvolnum></citerefentry> will, as an AD DC, attempt to
+    store the NT password hash used in NTLM and NTLMv2 authentication for
+    users in this domain. </para>
+
+    <para>If so configured, the Samba Active Directory Domain Controller,
+    will, except for trust accounts (computers, domain
+    controllers and inter-domain trusts) the
+    <emphasis>NOT store the NT hash</emphasis>
+    for new and changed accounts in the sam.ldb database.</para>
+
+    <para>This avoids the storage of an unsalted hash for these
+    user-created passwords.  As a consequence the
+    <constant>arcfour-hmac-md5</constant> Kerberos key type is
+    also unavailable in the KDC for these users - thankfully
+    <emphasis>modern clients will select an AES based key
+    instead.</emphasis></para>
+
+    <para>NOTE: As the password history in Active Directory is
+    stored as an NT hash (and thus unavailable), a workaround is
+    used, relying instead on Kerberos password hash values.
+    This stores three passwords, the current, previous and second previous
+    password.  This allows some checking against reuse. </para>
+
+    <para>However as these values are salted, changing the
+    sAMAccountName, userAccountControl or userPrincipalName of
+    an account will cause the salt to change.  After the rare
+    combination of both a rename and a password change only the
+    current password will be recognised for password history
+    purposes.
+    </para>
+    <para>The available settings are:</para>
+
+    <itemizedlist>
+        <listitem>
+          <para><constant>always</constant> - Always store the NT hash
+         (as machine accounts will also always store an NT hash,
+         a hash will be stored for all accounts).</para>
+
+         <para>This setting may be useful if <parameter
+         moreinfo="none">ntlm auth</parameter> is set to <constant>disabled</constant>
+         for a trial period</para>
+
+        </listitem>
+
+        <listitem>
+          <para><constant>never</constant> - Never store the NT hash
+         for user accounts, only for machine accounts</para>
+        </listitem>
+
+        <listitem>
+          <para><constant>auto</constant> - Store an NT hash if <parameter
+         moreinfo="none">ntlm auth</parameter> is not set to <constant>disabled</constant>.
+         </para>
+
+        </listitem>
+
+    </itemizedlist>
+
+</description>
+
+<related>ntlm auth</related>
+<value type="default">always</value>
+</samba:parameter>
index 8d31c98eb0595ad6382ac670de781c47c8d31694..d7c84ccaf85dce2b10a9bc98db8f710e1c140da0 100644 (file)
          authentication to forward to a full DC.  Setting this option
          to <constant>disabled</constant> will cause these forwarded
          authentications to fail.</para>
+
+         <para>Additionally, for Samba acting as an Active Directory
+         Domain Controller, for user accounts, if <parameter moreinfo="none">nt hash store</parameter>
+         is set to the default setting of <constant>auto</constant>,
+         the <emphasis>NT hash will not be stored</emphasis>
+         in the sam.ldb database for new users and after a
+         password change.</para>
+
         </listitem>
 
     </itemizedlist>
@@ -72,6 +80,7 @@
     behaviour is unchanged.</para>
 </description>
 
+<related>nt hash store</related>
 <related>lanman auth</related>
 <related>raw NTLMv2 auth</related>
 <value type="default">ntlmv2-only</value>
index 599c4b00966dc21132d8585900395fdd5567a84b..d8646120e6bd773e94870fa2b93213d18d89a945 100644 (file)
@@ -2651,6 +2651,7 @@ struct loadparm_context *loadparm_init(TALLOC_CTX *mem_ctx)
        lpcfg_do_global_parameter(lp_ctx, "ClientNTLMv2Auth", "True");
        lpcfg_do_global_parameter(lp_ctx, "LanmanAuth", "False");
        lpcfg_do_global_parameter(lp_ctx, "NTLMAuth", "ntlmv2-only");
+       lpcfg_do_global_parameter(lp_ctx, "NT hash store", "always");
        lpcfg_do_global_parameter(lp_ctx, "RawNTLMv2Auth", "False");
        lpcfg_do_global_parameter(lp_ctx, "client use spnego principal", "False");
 
index a3331436229a135d2cb323d7ed689063bf81fdb8..b1641ba88d28c1461ca011e0a51997853c33985c 100644 (file)
@@ -262,6 +262,13 @@ enum samba_weak_crypto {
        SAMBA_WEAK_CRYPTO_DISALLOWED,
 };
 
+/* Controlling the storage of the NT password has on the AD DC */
+enum store_nt_hash {
+       NT_HASH_STORE_AUTO,
+       NT_HASH_STORE_NEVER,
+       NT_HASH_STORE_ALWAYS
+};
+
 /*
  * Default passwd chat script.
  */
index 9fac73ef113b3c1b13c7f29147a878bcabfe7d78..3ffa4bcc411b159c2ccc63d74ef882fe60f616ee 100644 (file)
@@ -403,6 +403,13 @@ static const struct enum_list enum_ntlm_auth[] = {
        {-1, NULL}
 };
 
+static const struct enum_list enum_nt_hash_store[] = {
+       {NT_HASH_STORE_AUTO, "auto"},
+       {NT_HASH_STORE_NEVER, "never"},
+       {NT_HASH_STORE_ALWAYS, "always"},
+};
+
+
 static const struct enum_list enum_spotlight_backend[] = {
        {SPOTLIGHT_BACKEND_NOINDEX, "noindex"},
        {SPOTLIGHT_BACKEND_TRACKER, "tracker"},
index 1192a6e408f9d7f0e048997651442585a3226bb4..bad1bc1e029b2bc477bce021bd973833d04197a0 100644 (file)
@@ -1,6 +1,7 @@
-^samba.tests.krb5.nt_hash_tests.samba.tests.krb5.nt_hash_tests.NtHashTests.test_nt_hash.ad_dc_no_ntlm:local
 ^samba.tests.samba_tool.user.samba.tests.samba_tool.user.UserCmdTestCase.test_setpassword.ad_dc_no_ntlm:local
 ^samba4.ldap.login_basics.python.ad_dc_no_ntlm..__main__.BasicUserAuthTests.test_login_basics_ntlm.ad_dc_no_ntlm
+^samba4.ldap.passwords.python.ad_dc_no_ntlm..__main__.PasswordTests.test_old_password_rename_attempt_reuse_2.ad_dc_no_ntlm
+^samba4.ldap.passwords.python.ad_dc_no_ntlm..__main__.PasswordTests.test_old_password_rename_simple_bind_2.ad_dc_no_ntlm
 ^samba4.ldap.passwords.python.fl2003dc..__main__.PasswordTests.test_old_password_attempt_reuse.fl2003dc
 ^samba4.ldap.passwords.python.fl2003dc..__main__.PasswordTests.test_old_password_rename_attempt_reuse.fl2003dc
 ^samba4.ldap.passwords.python.fl2003dc..__main__.PasswordTests.test_old_password_rename_attempt_reuse_2.fl2003dc
diff --git a/selftest/knownfail.d/password_settings b/selftest/knownfail.d/password_settings
deleted file mode 100644 (file)
index 6e52189..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-# highlights a minor corner-case discrepancy between Windows and Samba
-samba4.ldap.passwordsettings.python.password_settings.PasswordSettingsTestCase.test_domain_pwd_history_zero\(ad_dc_default_smb1\)
index 4c263f55de4dbf684e59c5e6354ab71371e19ef0..1762ae4ae79b44ee7f43eab72f821f8365ae2891 100755 (executable)
@@ -2707,7 +2707,7 @@ sub setup_ad_dc_no_ntlm
                                         "ADNONTLMDOMAIN",
                                         "adnontlmdom.samba.example.com",
                                         undef,
-                                        "ntlm auth = disabled",
+                                        "ntlm auth = disabled\nnt hash store = never",
                                         undef);
        unless ($env) {
                return undef;
index 2b6e0bb248c50d94313b067d8a0c8cdac49cf0ff..43838575f3bfed7de35536a1555d233db75a4fce 100644 (file)
@@ -705,6 +705,7 @@ static void init_globals(struct loadparm_context *lp_ctx, bool reinit_globals)
        Globals.client_plaintext_auth = false;  /* Do NOT use a plaintext password even if is requested by the server */
        Globals._lanman_auth = false;   /* Do NOT use the LanMan hash, even if it is supplied */
        Globals.ntlm_auth = NTLM_AUTH_NTLMV2_ONLY;      /* Do NOT use NTLMv1 if it is supplied by the client (otherwise NTLMv2) */
+       Globals.nt_hash_store = NT_HASH_STORE_ALWAYS;   /* Fill in NT hash when setting password */
        Globals.raw_ntlmv2_auth = false; /* Reject NTLMv2 without NTLMSSP */
        Globals.client_ntlmv2_auth = true; /* Client should always use use NTLMv2, as we can't tell that the server supports it, but most modern servers do */
        /* Note, that we will also use NTLM2 session security (which is different), if it is available */
index 0ba0d9a884c0ac577fff81779f0e92c5cc1ca5d0..e90482df63ea5929abdb1bd84201b9ad90771649 100644 (file)
@@ -52,6 +52,8 @@
 #include "lib/crypto/gnutls_helpers.h"
 #include <gnutls/crypto.h>
 
+#include "kdc/db-glue.h"
+
 #ifdef ENABLE_GPGME
 #undef class
 #include <gpgme.h>
@@ -149,13 +151,26 @@ struct setup_password_fields_io {
                bool is_krbtgt;
                uint32_t restrictions;
                struct dom_sid *account_sid;
+               bool store_nt_hash;
        } u;
 
        /* new credentials and old given credentials */
        struct setup_password_fields_given {
                const struct ldb_val *cleartext_utf8;
                const struct ldb_val *cleartext_utf16;
+
                struct samr_Password *nt_hash;
+
+               /*
+                * The AES256 kerberos key to confirm the previous password was
+                * not reused (for n) and to prove the old password was known
+                * (for og).
+                *
+                * We don't have any old salts, so we won't catch password reuse
+                * if said password was used prior to an account rename and
+                * another password change.
+                */
+               DATA_BLOB aes_256;
        } n, og;
 
        /* old credentials */
@@ -165,6 +180,15 @@ struct setup_password_fields_io {
                struct samr_Password *nt_history;
                const struct ldb_val *supplemental;
                struct supplementalCredentialsBlob scb;
+
+               /*
+                * The AES256 kerberos key as stored in the DB.
+                * Used to confirm the given password was correct
+                * and in case the previous password was reused.
+                */
+               DATA_BLOB aes_256;
+               DATA_BLOB salt;
+               uint32_t kvno;
        } o;
 
        /* generated credentials */
@@ -618,17 +642,34 @@ static int password_hash_bypass(struct ldb_module *module, struct ldb_request *r
 
 static int setup_nt_fields(struct setup_password_fields_io *io)
 {
-       struct ldb_context *ldb;
+       struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
        uint32_t i;
-
-       io->g.nt_hash = io->n.nt_hash;
-       ldb = ldb_module_get_ctx(io->ac->module);
+       if (io->u.store_nt_hash) {
+               io->g.nt_hash = io->n.nt_hash;
+       }
 
        if (io->ac->status->domain_data.pwdHistoryLength == 0) {
                return LDB_SUCCESS;
        }
 
        /* We might not have an old NT password */
+
+       if (io->g.nt_hash == NULL) {
+               /*
+                * If there was not an NT hash specified, then don't
+                * store the NT password history.
+                *
+                * While the NTLM code on a Windows DC will cope with
+                * a missing unicodePwd, if it finds a last password
+                * in the ntPwdHistory, even if the bytes are zero ,
+                * it will (quite reasonably) treat it as a valid NT
+                * hash.  NTLM logins with the previous password are
+                * allowed for a short time after the password is
+                * changed to allow for password propagation delays.
+                */
+               return LDB_SUCCESS;
+       }
+
        io->g.nt_history = talloc_array(io->ac,
                                        struct samr_Password,
                                        io->ac->status->domain_data.pwdHistoryLength);
@@ -642,15 +683,7 @@ static int setup_nt_fields(struct setup_password_fields_io *io)
        }
        io->g.nt_history_len = i + 1;
 
-       if (io->g.nt_hash) {
-               io->g.nt_history[0] = *io->g.nt_hash;
-       } else {
-               /* 
-                * TODO: is this correct?
-                * the simular behavior is correct for the lm history case
-                */
-               E_md4hash("", io->g.nt_history[0].hash);
-       }
+       io->g.nt_history[0] = *io->g.nt_hash;
 
        return LDB_SUCCESS;
 }
@@ -784,6 +817,63 @@ static int setup_kerberos_keys(struct setup_password_fields_io *io)
        return LDB_SUCCESS;
 }
 
+static int setup_kerberos_key_hash(struct setup_password_fields_io *io,
+                                  struct setup_password_fields_given *g)
+{
+       struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
+       krb5_error_code krb5_ret;
+       krb5_data salt;
+       krb5_keyblock key;
+       krb5_data cleartext_data;
+
+       if (io->ac->search_res == NULL) {
+               /* No old data so nothing to do */
+               return LDB_SUCCESS;
+       }
+
+       if (io->o.salt.data == NULL) {
+               /* We didn't fetch the salt in setup_io(), so nothing to do */
+               return LDB_SUCCESS;
+       }
+
+       salt.data = (char *)io->o.salt.data;
+       salt.length = io->o.salt.length;
+
+       cleartext_data.data = (char *)g->cleartext_utf8->data;
+       cleartext_data.length = g->cleartext_utf8->length;
+
+       /*
+        * create ENCTYPE_AES256_CTS_HMAC_SHA1_96 key out of the salt
+        * and the cleartext password
+        */
+       krb5_ret = smb_krb5_create_key_from_string(io->smb_krb5_context->krb5_context,
+                                                  NULL,
+                                                  &salt,
+                                                  &cleartext_data,
+                                                  ENCTYPE_AES256_CTS_HMAC_SHA1_96,
+                                                  &key);
+       if (krb5_ret) {
+               ldb_asprintf_errstring(ldb,
+                                      "setup_kerberos_key_hash: "
+                                      "generation of a aes256-cts-hmac-sha1-96 key failed: %s",
+                                      smb_get_krb5_error_message(io->smb_krb5_context->krb5_context,
+                                                                 krb5_ret, io->ac));
+               return LDB_ERR_OPERATIONS_ERROR;
+       }
+
+       g->aes_256 = data_blob_talloc(io->ac,
+                                     KRB5_KEY_DATA(&key),
+                                     KRB5_KEY_LENGTH(&key));
+       krb5_free_keyblock_contents(io->smb_krb5_context->krb5_context, &key);
+       if (g->aes_256.data == NULL) {
+               return ldb_oom(ldb);
+       }
+
+       talloc_keep_secret(g->aes_256.data);
+
+       return LDB_SUCCESS;
+}
+
 static int setup_primary_kerberos(struct setup_password_fields_io *io,
                                  const struct supplementalCredentialsBlob *old_scb,
                                  struct package_PrimaryKerberosBlob *pkb)
@@ -1599,11 +1689,21 @@ static int setup_primary_userPassword(
         * used in preference to the NT password hash
         */
        if (io->g.nt_hash == NULL) {
-               ldb_asprintf_errstring(ldb,
-                       "No NT Hash, unable to calculate userPassword hashes");
-                       return LDB_ERR_UNWILLING_TO_PERFORM;
+               /*
+                * When the NT hash is not available, we use this field to store
+                * the first 16 bytes of the AES256 key instead. This allows
+                * 'samba-tool user' to verify that the user's password is in
+                * sync with the userPassword package.
+                */
+               uint8_t hash_len = MIN(16, io->g.aes_256.length);
+
+               ZERO_STRUCT(p_userPassword_b->current_nt_hash);
+               memcpy(p_userPassword_b->current_nt_hash.hash,
+                      io->g.aes_256.data,
+                      hash_len);
+       } else {
+               p_userPassword_b->current_nt_hash = *io->g.nt_hash;
        }
-       p_userPassword_b->current_nt_hash = *io->g.nt_hash;
 
        /*
         * Determine the number of hashes
@@ -2360,9 +2460,7 @@ static int setup_last_set_field(struct setup_password_fields_io *io)
 static int setup_given_passwords(struct setup_password_fields_io *io,
                                 struct setup_password_fields_given *g)
 {
-       struct ldb_context *ldb;
-
-       ldb = ldb_module_get_ctx(io->ac->module);
+       struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
 
        if (g->cleartext_utf8) {
                struct ldb_val *cleartext_utf16_blob;
@@ -2437,6 +2535,24 @@ static int setup_given_passwords(struct setup_password_fields_io *io,
                       g->cleartext_utf16->length);
        }
 
+       /*
+        * We need to build one more hash, so we can compare with what might
+        * have been stored in the old password (for the LDAP password change)
+        *
+        * We don't have any old salts, so we won't catch password reuse if said
+        * password was used prior to an account rename and another password
+        * change.
+        *
+        * We don't have to store the 'opaque' (string2key iterations)
+        * as Heimdal doesn't allow that to be changed.
+        */
+       if (g->cleartext_utf8 != NULL) {
+               int ret = setup_kerberos_key_hash(io, g);
+               if (ret != LDB_SUCCESS) {
+                       return ret;
+               }
+       }
+
        return LDB_SUCCESS;
 }
 
@@ -2526,6 +2642,11 @@ static int setup_password_fields(struct setup_password_fields_io *io)
                }
        }
 
+       /*
+        * This relies on setup_kerberos_keys to make a NT-hash-like
+        * value for password history purposes
+        */
+
        ret = setup_nt_fields(io);
        if (ret != LDB_SUCCESS) {
                return ret;
@@ -2722,6 +2843,7 @@ static int check_password_restrictions(struct setup_password_fields_io *io, WERR
 {
        struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
        int ret;
+       uint32_t i;
        struct loadparm_context *lp_ctx =
                talloc_get_type(ldb_get_opaque(ldb, "loadparm"),
                                struct loadparm_context);
@@ -2739,12 +2861,13 @@ static int check_password_restrictions(struct setup_password_fields_io *io, WERR
         */
        if (!io->ac->pwd_reset && !(io->ac->change
                                    && io->ac->change->old_password_checked == DSDB_PASSWORD_CHECKED_AND_CORRECT)) {
+               bool hash_checked = false;
                /*
                 * we need the old nt hash given by the client (this
                 * is for the plaintext over LDAP password change,
                 * Kpasswd and SAMR supply the control)
                 */
-               if (!io->og.nt_hash) {
+               if (io->og.nt_hash == NULL && io->og.aes_256.length == 0) {
                        ldb_asprintf_errstring(ldb,
                                "check_password_restrictions: "
                                "You need to provide the old password in order "
@@ -2752,9 +2875,21 @@ static int check_password_restrictions(struct setup_password_fields_io *io, WERR
                        return LDB_ERR_UNWILLING_TO_PERFORM;
                }
 
+               /*
+                * First compare the ENCTYPE_AES256_CTS_HMAC_SHA1_96 password and see if we have a match
+                */
+
+               if (io->og.aes_256.length > 0 && io->o.aes_256.length) {
+                       hash_checked = data_blob_equal_const_time(&io->og.aes_256, &io->o.aes_256);
+               }
+
                /* The password modify through the NT hash is encouraged and
                   has no problems at all */
-               if (!io->o.nt_hash || !mem_equal_const_time(io->og.nt_hash->hash, io->o.nt_hash->hash, 16)) {
+               if (!hash_checked && io->og.nt_hash && io->o.nt_hash) {
+                       hash_checked = mem_equal_const_time(io->og.nt_hash->hash, io->o.nt_hash->hash, 16);
+               }
+
+               if (!hash_checked) {
                        return make_error_and_update_badPwdCount(io, werror);
                }
        }
@@ -2837,10 +2972,37 @@ static int check_password_restrictions(struct setup_password_fields_io *io, WERR
                return LDB_SUCCESS;
        }
 
-       if (io->n.nt_hash) {
-               uint32_t i;
+       /*
+        * This check works by using the current Kerberos password to
+        * make up a password history.  We already did the salted hash
+        * creation to pass the password change check.
+        *
+        * We check the pwdHistoryLength to ensure we honour the
+        * policy on if the history should be checked
+        */
+       if (io->ac->status->domain_data.pwdHistoryLength > 0
+           && io->g.aes_256.length && io->o.aes_256.length)
+       {
+               bool equal = data_blob_equal_const_time(&io->g.aes_256,
+                                                       &io->o.aes_256);
+               if (equal) {
+                       ret = LDB_ERR_CONSTRAINT_VIOLATION;
+                       *werror = WERR_PASSWORD_RESTRICTION;
+                       ldb_asprintf_errstring(ldb,
+                                              "%08X: %s - check_password_restrictions: "
+                                              "the password was already used (previous password)!",
+                                              W_ERROR_V(*werror),
+                                              ldb_strerror(ret));
+                       io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY;
+                       return ret;
+               }
+       }
 
-               /* checks the NT hash password history */
+       if (io->n.nt_hash) {
+               /*
+                * checks the NT hash password history, against the
+                * generated NT hash
+                */
                for (i = 0; i < io->o.nt_history_len; i++) {
                        bool pw_cmp = mem_equal_const_time(io->n.nt_hash, io->o.nt_history[i].hash, 16);
                        if (pw_cmp) {
@@ -2857,6 +3019,90 @@ static int check_password_restrictions(struct setup_password_fields_io *io, WERR
                }
        }
 
+       /*
+        * This check works by using the old Kerberos passwords
+        * (old and older) to make up a password history.
+        *
+        * We check the pwdHistoryLength to ensure we honour the
+        * policy on if the history should be checked
+        */
+       for (i = 1;
+            i <= io->o.kvno && i < MIN(3, io->ac->status->domain_data.pwdHistoryLength);
+            i++)
+       {
+               krb5_error_code krb5_ret;
+               const uint32_t request_kvno = io->o.kvno - i;
+               DATA_BLOB db_key_blob;
+               bool pw_equal;
+
+               if (io->n.cleartext_utf8 == NULL) {
+                       /*
+                        * No point checking history if we don't have
+                        * a cleartext password.
+                        */
+                       break;
+               }
+
+               if (io->ac->search_res == NULL) {
+                       /*
+                        * This is an ADD, no existing history to check
+                        */
+                       break;
+               }
+
+               /*
+                * If this account requires a smartcard for login, we don't
+                * attempt a comparison with the old password.
+                */
+               if (io->u.userAccountControl & UF_SMARTCARD_REQUIRED) {
+                       break;
+               }
+
+               /*
+                * Extract the old ENCTYPE_AES256_CTS_HMAC_SHA1_96 value from
+                * the supplementalCredentials.
+                */
+               krb5_ret = dsdb_extract_aes_256_key(io->smb_krb5_context->krb5_context,
+                                                   io->ac,
+                                                   io->ac->search_res->message,
+                                                   io->u.userAccountControl,
+                                                   &request_kvno, /* kvno */
+                                                   NULL, /* kvno_out */
+                                                   &db_key_blob,
+                                                   NULL); /* salt */
+               if (krb5_ret == ENOENT) {
+                       /*
+                        * If there is no old AES hash (perhaps an imported DB with
+                        * just unicodePwd) then we just wont have an old
+                        * password to compare to if there is no NT hash
+                        */
+                       break;
+               } else if (krb5_ret) {
+                       ldb_asprintf_errstring(ldb,
+                                              "check_password_restrictions: "
+                                              "extraction of old[%u - %d = %d] aes256-cts-hmac-sha1-96 key failed: %s",
+                                              io->o.kvno, i, io->o.kvno - i,
+                                              smb_get_krb5_error_message(io->smb_krb5_context->krb5_context,
+                                                                         krb5_ret, io->ac));
+                       return LDB_ERR_OPERATIONS_ERROR;
+               }
+
+               /* This is the actual history check */
+               pw_equal = data_blob_equal_const_time(&io->n.aes_256,
+                                                     &db_key_blob);
+               if (pw_equal) {
+                       ret = LDB_ERR_CONSTRAINT_VIOLATION;
+                       *werror = WERR_PASSWORD_RESTRICTION;
+                       ldb_asprintf_errstring(ldb,
+                                              "%08X: %s - check_password_restrictions: "
+                                              "the password was already used (in history)!",
+                                              W_ERROR_V(*werror),
+                                              ldb_strerror(ret));
+                       io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY;
+                       return ret;
+               }
+       }
+
        /* are all password changes disallowed? */
        if (io->ac->status->domain_data.pwdProperties & DOMAIN_REFUSE_PASSWORD_CHANGE) {
                ret = LDB_ERR_CONSTRAINT_VIOLATION;
@@ -3176,6 +3422,8 @@ static int setup_io(struct ph_context *ac,
        struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
        struct loadparm_context *lp_ctx = talloc_get_type(
                ldb_get_opaque(ldb, "loadparm"), struct loadparm_context);
+       enum store_nt_hash store_hash_setting =
+               lpcfg_nt_hash_store(lp_ctx);
        int ret;
        const struct ldb_message *info_msg = NULL;
        struct dom_sid *account_sid = NULL;
@@ -3296,6 +3544,30 @@ static int setup_io(struct ph_context *ac,
                        MAX(io->ac->status->domain_data.pwdHistoryLength, 3);
        }
 
+       /*
+        * Machine accounts need the NT hash to operate the NETLOGON
+        * ServerAuthenticate{,2,3} logic
+        */
+       if (!(io->u.userAccountControl & UF_NORMAL_ACCOUNT)) {
+               store_hash_setting = NT_HASH_STORE_ALWAYS;
+       }
+
+       switch (store_hash_setting) {
+       case NT_HASH_STORE_ALWAYS:
+               io->u.store_nt_hash = true;
+               break;
+       case NT_HASH_STORE_NEVER:
+               io->u.store_nt_hash = false;
+               break;
+       case NT_HASH_STORE_AUTO:
+               if (lpcfg_ntlm_auth(lp_ctx) == NTLM_AUTH_DISABLED) {
+                       io->u.store_nt_hash = false;
+                       break;
+               }
+               io->u.store_nt_hash = true;
+               break;
+       }
+
        if (ac->userPassword) {
                ret = msg_find_old_and_new_pwd_val(client_msg, "userPassword",
                                                   ac->req->operation,
@@ -3612,6 +3884,10 @@ static int setup_io(struct ph_context *ac,
 
        if (existing_msg != NULL) {
                NTSTATUS status;
+               krb5_error_code krb5_ret;
+               DATA_BLOB key_blob;
+               DATA_BLOB salt_blob;
+               uint32_t kvno;
 
                if (ac->pwd_reset) {
                        /* Get the old password from the database */
@@ -3664,6 +3940,47 @@ static int setup_io(struct ph_context *ac,
                                return LDB_ERR_OPERATIONS_ERROR;
                        }
                }
+
+               /*
+                * If this account requires a smartcard for login, we don't
+                * attempt a comparison with the old password.
+                */
+               if (io->u.userAccountControl & UF_SMARTCARD_REQUIRED) {
+                       return LDB_SUCCESS;
+               }
+
+               /*
+                * Extract the old ENCTYPE_AES256_CTS_HMAC_SHA1_96
+                * value from the supplementalCredentials.
+                */
+               krb5_ret = dsdb_extract_aes_256_key(io->smb_krb5_context->krb5_context,
+                                                   io->ac,
+                                                   existing_msg,
+                                                   io->u.userAccountControl,
+                                                   NULL, /* kvno */
+                                                   &kvno, /* kvno_out */
+                                                   &key_blob,
+                                                   &salt_blob);
+               if (krb5_ret == ENOENT) {
+                       /*
+                        * If there is no old AES hash (perhaps an imported DB with
+                        * just unicodePwd) then we just wont have an old
+                        * password to compare to if there is no NT hash
+                        */
+                       return LDB_SUCCESS;
+               }
+               if (krb5_ret) {
+                       ldb_asprintf_errstring(ldb,
+                                              "setup_io: "
+                                              "extraction of salt for old aes256-cts-hmac-sha1-96 key failed: %s",
+                                              smb_get_krb5_error_message(io->smb_krb5_context->krb5_context,
+                                                                         krb5_ret, io->ac));
+                       return LDB_ERR_OPERATIONS_ERROR;
+               }
+
+               io->o.salt = salt_blob;
+               io->o.aes_256 = key_blob;
+               io->o.kvno = kvno;
        }
 
        return LDB_SUCCESS;
@@ -4689,6 +5006,7 @@ static int password_hash_mod_search_self(struct ph_context *ac)
                                              "badPasswordTime",
                                              "badPwdCount",
                                              "lockoutTime",
+                                             "msDS-KeyVersionNumber",
                                              "msDS-SecondaryKrbTgtNumber",
                                              NULL };
        struct ldb_request *search_req;
index 4d0febc715298f89df870c19ad3df44444493c3a..7a63f43726b0f8aff4882b6f9678fa99f5f29241 100644 (file)
@@ -195,7 +195,7 @@ bld.SAMBA_MODULE('ldb_password_hash',
        init_function='ldb_password_hash_module_init',
        module_init_name='ldb_init_module',
        internal_module=False,
-       deps='talloc samdb LIBCLI_AUTH NDR_DRSBLOBS authkrb5 krb5 gpgme DSDB_MODULE_HELPERS crypt'
+       deps='talloc samdb LIBCLI_AUTH NDR_DRSBLOBS authkrb5 krb5 gpgme DSDB_MODULE_HELPERS crypt db-glue'
        )
 
 
index e1c49d7bffb2c44552a6660c488d8abf45504f1c..bac89f3e3c884839092152b85bb2e5102e1c806c 100644 (file)
@@ -869,11 +869,8 @@ unicodePwd:: %s
         # we can set the exact same password again because there's no history
         self.assert_password_valid(user, "NewPwd12#")
 
-        # There is a difference in behaviour here between Windows and Samba.
         # When going from zero to non-zero password-history, Windows treats
         # the current user's password as invalid (even though the password has
-        # not been altered since the setting changed). Whereas Samba accepts
-        # the current password (because it's not in the history until the
-        # *next* time the user's password changes.
+        # not been altered since the setting changed).
         self.set_domain_pwdHistoryLength("1")
         self.assert_password_invalid(user, "NewPwd12#")