]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
python/samba/tests/krb5: PKINIT tests of passwords that are naturally expired
authorAndrew Bartlett <abartlet@samba.org>
Fri, 10 May 2024 04:51:27 +0000 (16:51 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Mon, 10 Jun 2024 04:27:30 +0000 (04:27 +0000)
The tests of passwords that will expire in the TGT lifetime fail against
windows, we do not see the rotation in that case.

Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Jo Sutton <josutton@catalyst.net.nz>
python/samba/tests/krb5/pkinit_tests.py
selftest/knownfail_heimdal_kdc
selftest/knownfail_mit_kdc_1_20

index 7a1435ceb7e2115e6f4d3465d787d3b8403d756d..e8e88126613042eb73bdea64c7757e0a63ed16cc 100755 (executable)
@@ -24,6 +24,7 @@ sys.path.insert(0, 'bin/python')
 os.environ['PYTHONUNBUFFERED'] = '1'
 
 from datetime import datetime, timedelta
+import time
 
 from pyasn1.type import univ
 
@@ -37,7 +38,11 @@ from cryptography.x509.oid import NameOID
 import ldb
 import samba.tests
 from samba import credentials, generate_random_password, ntstatus
+from samba.nt_time import (nt_time_delta_from_timedelta,
+                           nt_now, string_from_nt_time)
 from samba.dcerpc import security, netlogon
+from samba.dsdb import UF_PASSWORD_EXPIRED, UF_DONT_EXPIRE_PASSWD
+from samba.tests.pso import PasswordSettings
 from samba.tests.krb5 import kcrypto
 from samba.tests.krb5.kdc_base_test import KDCBaseTest
 from samba.tests.krb5.raw_testcase import PkInit, RawKerberosTest
@@ -95,16 +100,23 @@ class PkInitTests(KDCBaseTest):
         self.do_asn1_print = global_asn1_print
         self.do_hexdump = global_hexdump
 
-    def _get_creds(self, account_type=KDCBaseTest.AccountType.USER, use_cache=False, smartcard_required=False):
+    def _get_creds(self,
+                   account_type=KDCBaseTest.AccountType.USER,
+                   use_cache=False,
+                   smartcard_required=False,
+                   assigned_policy=None):
         """Return credentials with an account having a UPN for performing
         PK-INIT."""
         samdb = self.get_samdb()
         realm = samdb.domain_dns_name().upper()
 
+        opts={'upn': f'{{account}}.{realm}@{realm}',
+              'smartcard_required': smartcard_required}
+        if assigned_policy is not None:
+            opts['assigned_policy'] = str(assigned_policy.dn)
         return self.get_cached_creds(
             account_type=account_type,
-            opts={'upn': f'{{account}}.{realm}@{realm}',
-                  'smartcard_required': smartcard_required},
+            opts=opts,
             use_cache=use_cache)
 
     def test_pkinit_no_des3(self):
@@ -1062,6 +1074,174 @@ class PkInitTests(KDCBaseTest):
            This variant DISABLES the enabling attribute for auto-rotation."""
         self._test_pkinit_smartcard_required_must_change_now(False)
 
+    def _test_pkinit_smartcard_required_must_change(self, short_tgt_lifetime=False,
+                                                    short_pw_lifetime=True,
+                                                    expired=False):
+        """Test public-key PK-INIT to get the user's NT hash for an account
+           that is restricted by UF_SMARTCARD_REQUIRED rotates if it expires before the TGT lifetime.
+
+        This test is of 'natural' expiry, not just reset pwdLastSet to 0"""
+
+        samdb = self.get_samdb()
+        msgs = samdb.search(base=samdb.get_default_basedn(),
+                            scope=ldb.SCOPE_BASE,
+                            attrs=["msDS-ExpirePasswordsOnSmartCardOnlyAccounts"])
+        msg = msgs[0]
+
+        try:
+            old_ExpirePasswordsOnSmartCardOnlyAccounts = msg["msDS-ExpirePasswordsOnSmartCardOnlyAccounts"]
+        except KeyError:
+            old_ExpirePasswordsOnSmartCardOnlyAccounts = None
+
+        self.addCleanup(set_ExpirePasswordsOnSmartCardOnlyAccounts,
+                        samdb, old_ExpirePasswordsOnSmartCardOnlyAccounts)
+
+        # Enable auto-rotation for this test
+        set_ExpirePasswordsOnSmartCardOnlyAccounts(samdb, True)
+
+        if expired:
+            password_age_max = 4
+            expect_rotate=True
+        elif short_pw_lifetime:
+            password_age_max = 16
+            if short_tgt_lifetime:
+                # TGT will expire before password
+                expect_rotate = False
+            else:
+                # TGT expires after password, rotate
+                expect_rotate = True
+        else:
+            password_age_max = 111
+
+            # After sleep, won't be half-way though lifetime
+            expect_rotate=False
+
+        tgt_life = 10*60*60
+
+        if short_tgt_lifetime:
+            # Create an authentication policy with a TGT lifetime set.
+            # This is less than the short_pw_lifetime
+            # password_age_max (16) set above, minus the sleep (8) below, to
+            # show that we can be half-way though the life, but if the
+            # TGT to expire in that time, we should not rotate
+            tgt_life = 1
+            policy = self.create_authn_policy(enforced=True,
+                                              user_tgt_lifetime=tgt_life)
+
+            client_creds = self._get_creds(smartcard_required=True, assigned_policy=policy)
+        else:
+            client_creds = self._get_creds(smartcard_required=True)
+
+        userdn = str(client_creds.get_dn())
+
+        client_creds.set_kerberos_state(credentials.AUTO_USE_KERBEROS)
+
+        nt_hash_remote = client_creds.get_nt_hash()
+        newpass = client_creds.get_password()
+        samdb.setpassword("(distinguishedName=%s)" % ldb.binary_encode(userdn),
+                          newpass)
+
+        # Sleep enough to expire 4 sec passwords and be half-way to expiry of 16sec passwords, but not the 111sec passwords
+        time.sleep(8)
+
+        # create a PSO setting password_age_max, which depending on
+        # the above may be shorter or longer than the TGT time in
+        # tgt_life, to test the interaction.
+        #
+        # The first parameter is not a username, just a new unique name for the PSO
+        short_expiry_pso = PasswordSettings(self.get_new_username(), samdb,
+                                            precedence=200,
+                                            password_age_max=password_age_max)
+        self.addCleanup(samdb.delete, short_expiry_pso.dn)
+        short_expiry_pso.apply_to(userdn)
+
+        krbtgt_creds = self.get_krbtgt_creds()
+
+        freshness_token = self.create_freshness_token()
+
+        # Get initial pwdLastSet
+        res = samdb.search(base=client_creds.get_dn(),
+                           scope=ldb.SCOPE_BASE,
+                           attrs=["pwdLastSet",
+                                  "msDS-UserPasswordExpiryTimeComputed",
+                                  "msDS-User-Account-Control-Computed",
+                                  "userAccountControl"
+                           ])
+        self.assertEqual((int(res[0]['userAccountControl'][0])
+                          & UF_DONT_EXPIRE_PASSWD), 0)
+
+        server_uac_expired = (int(res[0]['msDS-User-Account-Control-Computed'][0])
+                              & UF_PASSWORD_EXPIRED) == UF_PASSWORD_EXPIRED
+
+        self.assertEqual(expired, server_uac_expired)
+
+        pwd_last_set = int(res[0]["pwdLastSet"][0])
+        self.assertGreater(pwd_last_set, 0)
+
+        # This just checks the value is sensible
+        self.assertAlmostEqual(pwd_last_set, nt_now(), delta=nt_time_delta_from_timedelta(timedelta(seconds=300)),
+                               msg=f"pwdLastSet {string_from_nt_time(pwd_last_set)} unreasonable, should be close to {string_from_nt_time(nt_now())}")
+        new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
+        calculated_expiry = pwd_last_set + nt_time_delta_from_timedelta(timedelta(seconds=password_age_max))
+
+        # Assert that the PSO applied
+        self.assertEqual(calculated_expiry, new_expiry)
+
+        kdc_exchange_dict = self._pkinit_req(client_creds, krbtgt_creds,
+                                             freshness_token=freshness_token,
+                                             expect_matching_nt_hash_in_pac=not expect_rotate)
+
+        nt_hash_from_pac = kdc_exchange_dict['nt_hash_from_pac']
+        tgt = kdc_exchange_dict['rep_ticket_creds']
+
+        # Check (as well as via expect_matching_nt_hash_in_pac) that
+        # the password was or was not rotated.
+
+        res2 = samdb.search(base=client_creds.get_dn(),
+                            scope=ldb.SCOPE_BASE,
+                            attrs=["pwdLastSet"])
+
+        if expect_rotate:
+            self.assertGreater(int(res2[0]["pwdLastSet"][0]), int(res[0]["pwdLastSet"][0]))
+            self.assertNotEqual(nt_hash_remote, bytes(nt_hash_from_pac.hash))
+
+            # We are checking we now got a full-length ticket
+            if short_tgt_lifetime:
+                self.check_ticket_times(tgt, expected_life=tgt_life)
+            else:
+                delta=300
+                # delta is for any clock skew, Windows seems to take any clock skew off the ticket life
+                self.check_ticket_times(tgt, expected_life=tgt_life, delta=delta)
+
+        else:
+            self.assertEqual(int(res2[0]["pwdLastSet"][0]), int(res[0]["pwdLastSet"][0]))
+            self.assertEqual(nt_hash_remote, bytes(nt_hash_from_pac.hash))
+
+            if short_tgt_lifetime:
+                # Not rotated and should be the TGT lifetime from the policy.
+                self.check_ticket_times(tgt, expected_life=tgt_life)
+
+            # Otherwise should be either the remaining password time (Windows) or the TGT time (Samba).
+
+
+    def test_pkinit_smartcard_required_must_change_before_tgt_expiry(self):
+        return self._test_pkinit_smartcard_required_must_change(short_tgt_lifetime=False, short_pw_lifetime=False)
+
+    def test_pkinit_smartcard_required_must_change_expired(self):
+        return self._test_pkinit_smartcard_required_must_change(expired=True)
+
+    def test_pkinit_smartcard_required_must_change_soon(self):
+        return self._test_pkinit_smartcard_required_must_change()
+
+    def test_pkinit_smartcard_required_must_change_soon_after_tgt(self):
+        return self._test_pkinit_smartcard_required_must_change(short_tgt_lifetime=True, short_pw_lifetime=False)
+
+    def test_pkinit_smartcard_required_must_change_short_tgt(self):
+        return self._test_pkinit_smartcard_required_must_change(short_tgt_lifetime=True)
+
+    def test_pkinit_smartcard_required_must_change_expired_short_tgt(self):
+        return self._test_pkinit_smartcard_required_must_change(short_tgt_lifetime=True, expired=True)
+
     def test_pkinit_kpasswd_change(self):
         """Test public-key PK-INIT to get an initial ticket to change the user's own password."""
         client_creds = self._get_creds()
index 69fb4eb7bdcc52e6055a14e9f4a0e3c2a077b2bb..d913c6459b0db0a1b390ac8b8968b87a9eefafca 100644 (file)
 ^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_ntlm_from_pac_smartcard_required
 ^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_samlogon_smartcard_required
 ^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_now
+^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_before_tgt_expiry
+^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_expired
+^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_short_tgt
+^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_soon
 #
 # Windows 2000 PK-INIT tests
 #
index d01a49041a965fc44d405ad96a9d6a38f0c9c297..31f2cbb8efbeb196f7cf5d01e8be887573570fdc 100644 (file)
 ^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_kpasswd_change
 ^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_samlogon_smartcard_required
 ^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_now
+^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_before_tgt_expiry
+^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_expired
+^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_short_tgt
+^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_soon
 #
 # PK-INIT Freshness tests
 #