From d9c5387208cfee87c845261e7cdc68da4f3ec88b Mon Sep 17 00:00:00 2001 From: Joseph Sutton Date: Fri, 29 Apr 2022 12:24:26 +1200 Subject: [PATCH 1/6] s4:kdc: Don't generate random keys for accounts requiring smartcard login Signed-off-by: Joseph Sutton --- source4/kdc/db-glue.c | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/source4/kdc/db-glue.c b/source4/kdc/db-glue.c index d421e6ead61..c50ea0706ba 100644 --- a/source4/kdc/db-glue.c +++ b/source4/kdc/db-glue.c @@ -412,31 +412,6 @@ out: return ret; } - -static int samba_kdc_set_random_keys(krb5_context context, - uint32_t supported_enctypes, - struct sdb_keys *keys) -{ - struct ldb_val secret_val; - uint8_t secretbuffer[32]; - - /* - * Fake keys until we have a better way to reject - * non-pkinit requests. - * - * We just need to indicate which encryption types are - * supported. - */ - generate_secret_buffer(secretbuffer, sizeof(secretbuffer)); - - secret_val = data_blob_const(secretbuffer, - sizeof(secretbuffer)); - return samba_kdc_set_fixed_keys(context, - &secret_val, - supported_enctypes, - keys); -} - struct samba_kdc_user_keys { struct sdb_keys *skeys; uint32_t kvno; @@ -629,17 +604,6 @@ krb5_error_code samba_kdc_message2entry_keys(krb5_context context, } } - if ((ent_type == SAMBA_KDC_ENT_TYPE_CLIENT) - && (userAccountControl & UF_SMARTCARD_REQUIRED)) { - ret = samba_kdc_set_random_keys(context, - supported_enctypes, - &entry->keys); - - *supported_enctypes_out = supported_enctypes & ENC_ALL_TYPES; - - goto out; - } - current_kvno = ldb_msg_find_attr_as_int(msg, "msDS-KeyVersionNumber", 0); if (current_kvno > 1) { old_kvno = current_kvno - 1; -- 2.39.1 From 509b1eaa063de5cf6d9bf3f1affd84c7f8423fdc Mon Sep 17 00:00:00 2001 From: Joseph Sutton Date: Tue, 17 May 2022 20:31:39 +1200 Subject: [PATCH 2/6] s4:kdc: Remove unused parameters from samba_kdc_message2entry_keys() Signed-off-by: Joseph Sutton --- source4/auth/ntlm/auth_sam.c | 7 ------- source4/dsdb/samdb/ldb_modules/password_hash.c | 2 -- source4/kdc/db-glue.c | 11 ++--------- source4/kdc/db-glue.h | 3 --- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/source4/auth/ntlm/auth_sam.c b/source4/auth/ntlm/auth_sam.c index 7b65b3084f8..b4b231a8ca8 100644 --- a/source4/auth/ntlm/auth_sam.c +++ b/source4/auth/ntlm/auth_sam.c @@ -317,7 +317,6 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con DATA_BLOB salt_data = data_blob_null; struct smb_krb5_context *smb_krb5_context = NULL; const struct ldb_val *sc_val; - uint32_t userAccountControl = 0; uint32_t current_kvno = 0; bool am_rodc; @@ -341,10 +340,6 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con return nt_status; } - userAccountControl = ldb_msg_find_attr_as_uint(msg, - "userAccountControl", - 0); - sc_val = ldb_msg_find_ldb_val(msg, "supplementalCredentials"); if (nt_pwd == NULL && sc_val == NULL) { @@ -401,7 +396,6 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con krb5_ret = dsdb_extract_aes_256_key(smb_krb5_context->krb5_context, tmp_ctx, msg, - userAccountControl, NULL, /* kvno */ ¤t_kvno, /* kvno_out */ &_aes_256_key, @@ -552,7 +546,6 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con krb5_ret = dsdb_extract_aes_256_key(smb_krb5_context->krb5_context, tmp_ctx, msg, - userAccountControl, &request_kvno, /* kvno */ NULL, /* kvno_out */ &_aes_256_key, diff --git a/source4/dsdb/samdb/ldb_modules/password_hash.c b/source4/dsdb/samdb/ldb_modules/password_hash.c index 8baf5085215..39df97d88ed 100644 --- a/source4/dsdb/samdb/ldb_modules/password_hash.c +++ b/source4/dsdb/samdb/ldb_modules/password_hash.c @@ -3165,7 +3165,6 @@ static int check_password_restrictions(struct setup_password_fields_io *io, WERR 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, @@ -4064,7 +4063,6 @@ static int setup_io(struct ph_context *ac, 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, diff --git a/source4/kdc/db-glue.c b/source4/kdc/db-glue.c index c50ea0706ba..5377a41e428 100644 --- a/source4/kdc/db-glue.c +++ b/source4/kdc/db-glue.c @@ -558,8 +558,6 @@ krb5_error_code samba_kdc_message2entry_keys(krb5_context context, const struct ldb_message *msg, bool is_krbtgt, bool is_rodc, - uint32_t userAccountControl, - enum samba_kdc_ent_type ent_type, unsigned flags, krb5_kvno requested_kvno, struct sdb_entry *entry, @@ -1559,8 +1557,7 @@ static krb5_error_code samba_kdc_message2entry(krb5_context context, /* Get keys from the db */ ret = samba_kdc_message2entry_keys(context, p, msg, is_krbtgt, is_rodc, - userAccountControl, - ent_type, flags, kvno, entry, + flags, kvno, entry, supported_enctypes, &available_enctypes); if (ret) { @@ -1585,8 +1582,7 @@ static krb5_error_code samba_kdc_message2entry(krb5_context context, supported_enctypes = ENC_RC4_HMAC_MD5; ret = samba_kdc_message2entry_keys(context, p, msg, is_krbtgt, is_rodc, - userAccountControl, - ent_type, flags, kvno, entry, + flags, kvno, entry, supported_enctypes, &available_enctypes); if (ret) { @@ -3642,7 +3638,6 @@ NTSTATUS samba_kdc_setup_db_ctx(TALLOC_CTX *mem_ctx, struct samba_kdc_base_conte krb5_error_code dsdb_extract_aes_256_key(krb5_context context, TALLOC_CTX *mem_ctx, const struct ldb_message *msg, - uint32_t user_account_control, const uint32_t *kvno, uint32_t *kvno_out, DATA_BLOB *aes_256_key, @@ -3662,8 +3657,6 @@ krb5_error_code dsdb_extract_aes_256_key(krb5_context context, msg, false, /* is_krbtgt */ false, /* is_rodc */ - user_account_control, - SAMBA_KDC_ENT_TYPE_CLIENT, flags, (kvno != NULL) ? *kvno : 0, &sentry, diff --git a/source4/kdc/db-glue.h b/source4/kdc/db-glue.h index f37e6e96731..76ecde2cd3a 100644 --- a/source4/kdc/db-glue.h +++ b/source4/kdc/db-glue.h @@ -45,8 +45,6 @@ krb5_error_code samba_kdc_message2entry_keys(krb5_context context, const struct ldb_message *msg, bool is_krbtgt, bool is_rodc, - uint32_t userAccountControl, - enum samba_kdc_ent_type ent_type, unsigned flags, krb5_kvno requested_kvno, struct sdb_entry *entry, @@ -104,7 +102,6 @@ NTSTATUS samba_kdc_setup_db_ctx(TALLOC_CTX *mem_ctx, struct samba_kdc_base_conte krb5_error_code dsdb_extract_aes_256_key(krb5_context context, TALLOC_CTX *mem_ctx, const struct ldb_message *msg, - uint32_t user_account_control, const uint32_t *kvno, uint32_t *kvno_out, DATA_BLOB *aes_256_key, -- 2.39.1 From d395e77ae86f2d69adcd0ef6738cc4754d3ebb29 Mon Sep 17 00:00:00 2001 From: Joseph Sutton Date: Tue, 24 May 2022 18:31:34 +1200 Subject: [PATCH 3/6] dsdb: Extract the AES256 key even if the account requires a smartcard for login Having this early return makes things inconsistent, since we'll fall back to NT hashes in check_password_restrictions(); that will fail iff NTLM authentication is disabled. Either we should disable password changes entirely with smartcard_required, or always allow them -- irrespective of NTLM authentication. Signed-off-by: Joseph Sutton --- source4/dsdb/samdb/ldb_modules/password_hash.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/source4/dsdb/samdb/ldb_modules/password_hash.c b/source4/dsdb/samdb/ldb_modules/password_hash.c index 39df97d88ed..b2142b0b985 100644 --- a/source4/dsdb/samdb/ldb_modules/password_hash.c +++ b/source4/dsdb/samdb/ldb_modules/password_hash.c @@ -4048,14 +4048,6 @@ static int setup_io(struct ph_context *ac, } } - /* - * 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. -- 2.39.1 From 559df3d7e26128dac7bad21347fc2be7c4b3f203 Mon Sep 17 00:00:00 2001 From: Joseph Sutton Date: Tue, 24 May 2022 20:00:39 +1200 Subject: [PATCH 4/6] [TODO] tests/krb5: Add tests for accounts requiring a smartcard for logon Signed-off-by: Joseph Sutton --- python/samba/tests/krb5/kpasswd_tests.py | 4 - python/samba/tests/krb5/raw_testcase.py | 4 + python/samba/tests/krb5/smartcard_tests.py | 448 +++++++++++++++++++++ source4/selftest/tests.py | 7 + 4 files changed, 459 insertions(+), 4 deletions(-) create mode 100755 python/samba/tests/krb5/smartcard_tests.py diff --git a/python/samba/tests/krb5/kpasswd_tests.py b/python/samba/tests/krb5/kpasswd_tests.py index 961feeac243..7c63d81c96d 100755 --- a/python/samba/tests/krb5/kpasswd_tests.py +++ b/python/samba/tests/krb5/kpasswd_tests.py @@ -90,10 +90,6 @@ class KpasswdTests(KDCBaseTest): return creds - def get_kpasswd_sname(self): - return self.PrincipalName_create(name_type=NT_PRINCIPAL, - names=['kadmin', 'changepw']) - def get_ticket_lifetime(self, ticket): enc_part = ticket.ticket_private diff --git a/python/samba/tests/krb5/raw_testcase.py b/python/samba/tests/krb5/raw_testcase.py index 3ba0b9df08b..78579103e29 100644 --- a/python/samba/tests/krb5/raw_testcase.py +++ b/python/samba/tests/krb5/raw_testcase.py @@ -5963,6 +5963,10 @@ class RawKerberosTest(TestCase): return krbtgt_sname + def get_kpasswd_sname(self): + return self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['kadmin', 'changepw']) + def add_requester_sid(self, pac, sid): pac_buffers = pac.buffers diff --git a/python/samba/tests/krb5/smartcard_tests.py b/python/samba/tests/krb5/smartcard_tests.py new file mode 100755 index 00000000000..6ec70246cf0 --- /dev/null +++ b/python/samba/tests/krb5/smartcard_tests.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import os +import subprocess +import sys + +import ldb + +from samba import generate_random_password, ntstatus +from samba.dcerpc import netlogon +from samba.dsdb import UF_SMARTCARD_REQUIRED +from samba.samdb import SamDB + +from samba.tests.krb5 import kcrypto +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_POLICY, + KDC_ERR_PREAUTH_REQUIRED, + KPASSWD_SUCCESS, + NT_PRINCIPAL, + NT_SRV_INST, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +global_asn1_print = False +global_hexdump = False + + +class SmartcardTests(KDCBaseTest): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + samdb = self.get_samdb() + + # Get the old 'dSHeuristics' if it was set + dsheuristics = samdb.get_dsheuristics() + + # Reset the 'dSHeuristics' as they were before + self.addCleanup(samdb.set_dsheuristics, dsheuristics) + + # Set the 'dSHeuristics' to activate the correct 'userPassword' + # behaviour + samdb.set_dsheuristics('000000001') + + # Get the old 'minPwdAge' + minPwdAge = samdb.get_minPwdAge() + + # Reset the 'minPwdAge' as it was before + self.addCleanup(samdb.set_minPwdAge, minPwdAge) + + # Set it temporarily to '0' + samdb.set_minPwdAge('0') + + def _get_creds(self, ntlm=False): + opts = { + 'kerberos_enabled': not ntlm, + } + + # Create the account. + creds = self.get_cached_creds(account_type=self.AccountType.USER, + opts=opts, + use_cache=False) + + return creds + + def require_smartcard(self, creds): + samdb = self.get_samdb() + dn = creds.get_dn() + + res = samdb.search(dn, scope=ldb.SCOPE_BASE, + attrs=['userAccountControl']) + uac = int(res[0].get('userAccountControl', idx=0)) + + # Set the SMARTCARD_REQUIRED bit on the account. + uac |= UF_SMARTCARD_REQUIRED + + msg = ldb.Message(dn) + msg['userAccountControl'] = ldb.MessageElement( + str(uac), + ldb.FLAG_MOD_REPLACE, + 'userAccountControl') + samdb.modify(msg) + + # The account's keys have been randomized, so set the password to + # something we know. + + new_password = generate_random_password(32, 32) + + msg = ldb.Message(dn) + msg['userPassword'] = ldb.MessageElement(new_password, + ldb.FLAG_MOD_REPLACE, + 'userPassword') + samdb.modify(msg) + + creds.update_password(new_password) + + def test_ntlm(self): + creds = self._get_creds(ntlm=True) + + if self.expect_nt_hash: + error = None + else: + error = f'{ntstatus.NT_STATUS_NTLM_BLOCKED & 0xffff:x}' + self._connect(creds, simple_bind=False, expect_error=error) + + def test_ntlm_smartcard(self): + creds = self._get_creds(ntlm=True) + self.require_smartcard(creds) + + if self.expect_nt_hash: + error = None + else: + error = f'{ntstatus.NT_STATUS_NTLM_BLOCKED & 0xffff:x}' + self._connect(creds, simple_bind=False, expect_error=error) + + def test_simple_bind(self): + creds = self._get_creds() + + self._connect(creds, simple_bind=True) + + def test_simple_bind_smartcard(self): + creds = self._get_creds() + self.require_smartcard(creds) + + error = f'{ntstatus.NT_STATUS_SMARTCARD_LOGON_REQUIRED & 0xffff:x}' + self._connect(creds, simple_bind=True, expect_error=error) + + def test_get_tgt(self): + creds = self._get_creds() + + self._check_tgt(creds) + + def test_get_tgt_smartcard(self): + creds = self._get_creds() + self.require_smartcard(creds) + + self._check_tgt(creds, expect_error=True) + + def _check_tgt(self, creds, expect_error=False): + user_name = creds.get_username() + + realm = creds.get_realm() + + salt = creds.get_salt() + + etype = AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5 + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=user_name.split('/')) + sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=['krbtgt', realm]) + + till = self.get_KerberosTime(offset=36000) + + krbtgt_creds = self.get_krbtgt_creds() + ticket_decryption_key = ( + self.TicketDecryptionKey_from_creds(krbtgt_creds)) + + expected_etypes = krbtgt_creds.tgs_supported_enctypes + + kdc_options = ('forwardable,' + 'renewable,' + 'canonicalize,' + 'renewable-ok') + kdc_options = krb5_asn1.KDCOptions(kdc_options) + + pac_request = True + pac_options = '1' # supports claims + + rep, kdc_exchange_dict = self._test_as_exchange( + cname=cname, + realm=realm, + sname=sname, + till=till, + client_as_etypes=etype, + expected_error_mode=KDC_ERR_PREAUTH_REQUIRED, + expected_crealm=realm, + expected_cname=cname, + expected_srealm=realm, + expected_sname=sname, + expected_salt=salt, + expected_supported_etypes=expected_etypes, + etypes=etype, + padata=None, + kdc_options=kdc_options, + preauth_key=None, + ticket_decryption_key=ticket_decryption_key, + pac_request=pac_request, + pac_options=pac_options) + self.check_error_rep(rep, KDC_ERR_PREAUTH_REQUIRED) + + preauth_key = self.PasswordKey_from_creds(creds, + kcrypto.Enctype.AES256) + + ts_enc_padata = self.get_enc_timestamp_pa_data_from_key(preauth_key) + + padata = [ts_enc_padata] + + if expect_error: + expected_error_mode = KDC_ERR_POLICY + # TODO: the status code is not currently checked + expect_status = True + expected_status = ntstatus.NT_STATUS_SMARTCARD_LOGON_REQUIRED + else: + expected_error_mode = 0 + expect_status = False + expected_status = None + + rep, kdc_exchange_dict = self._test_as_exchange( + cname=cname, + realm=realm, + sname=sname, + till=till, + client_as_etypes=etype, + expected_error_mode=expected_error_mode, + expect_status=expect_status, + expected_status=expected_status, + expected_crealm=realm, + expected_cname=cname, + expected_srealm=realm, + expected_sname=sname, + expected_salt=salt, + expected_supported_etypes=expected_etypes, + etypes=etype, + padata=padata, + kdc_options=kdc_options, + preauth_key=preauth_key, + ticket_decryption_key=ticket_decryption_key, + pac_request=pac_request, + pac_options=pac_options) + if expected_error_mode: + self.check_error_rep(rep, expected_error_mode) + else: + self.check_as_reply(rep) + + def test_kpasswd_set_smartcard(self): + # Create an account for testing. + creds = self._get_creds() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + # Require a smartcard for login. + self.require_smartcard(creds) + + expected_code = KPASSWD_SUCCESS + expected_msg = b'Password changed' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + def test_kpasswd_change_smartcard(self): + # Create an account for testing. + creds = self._get_creds() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + # Require a smartcard for login. + self.require_smartcard(creds) + + expected_code = KPASSWD_SUCCESS + expected_msg = b'Password changed' + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + def test_kpasswd_client_set_account(self): + creds = self._get_creds() + tgt = self.get_tgt(creds) + + self._kpasswd_set_account(creds, tgt, expect_error=False) + + def test_kpasswd_client_set_account_smartcard(self): + creds = self._get_creds() + tgt = self.get_tgt(creds) + self.require_smartcard(creds) + + self._kpasswd_set_account(creds, tgt, expect_error=True) + + def test_samr_change(self): + creds = self._get_creds() + + self._test_samr_change_password( + creds, + expect_error=ntstatus.NT_STATUS_WRONG_PASSWORD) + + def test_samr_change_smartcard(self): + creds = self._get_creds() + self.require_smartcard(creds) + + self._test_samr_change_password( + creds, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + def test_ldap_change(self): + creds = self._get_creds() + tgt = self.get_tgt(creds) + + self._ldap_change(creds, tgt) + + def test_ldap_change_smartcard(self): + creds = self._get_creds() + tgt = self.get_tgt(creds) + self.require_smartcard(creds) + + self._ldap_change(creds, tgt) + + def _ldap_change(self, creds, tgt): + dc_creds = self.get_dc_creds() + + host = self.get_samdb().host_dns_name() + + ticket = self.get_service_ticket(tgt, dc_creds, + service='ldap', + target_name=host) + ccache_creds, cachefile = self.create_ccache_with_ticket(creds, ticket) + # Remove the cached credentials file. + self.addCleanup(os.remove, cachefile.name) + + samdb = SamDB(url=f'ldap://{host}', + credentials=ccache_creds, + lp=self.get_lp()) + + new_password = generate_random_password(32, 32) + + msg = ldb.Message(creds.get_dn()) + msg['0'] = ldb.MessageElement(creds.get_password(), + ldb.FLAG_MOD_DELETE, + 'userPassword') + msg['1'] = ldb.MessageElement(new_password, + ldb.FLAG_MOD_ADD, + 'userPassword') + samdb.modify(msg) + + def _kpasswd_set_account(self, creds, tgt, expect_error): + ccache_creds, cachefile = self.create_ccache_with_ticket(creds, + ticket=tgt) + + # Remove the cached credentials file. + self.addCleanup(os.remove, cachefile.name) + + username = creds.get_username() + realm = creds.get_realm() + password = creds.get_password() + + new_password = generate_random_password(32, 32) + # Avoid problems providing passwords to texpect + new_password = new_password.replace('#', '+') + + script = f'''expect {username}@{realm}'s Password: +send {password}\\n +''' + if expect_error: + script += ('expect samba4kpasswd: ' + 'krb5_get_init_creds: KDC policy rejects request') + else: + script += f'''expect New password for {username}@{realm}: +send {new_password}\\n +expect Verify password - New password for {username}@{realm}: +send {new_password}\\n +expect Success +''' + + cmd = ['bin/texpect', '-t', '5', '/dev/stdin', + 'bin/samba4kpasswd', + username, + ] + + result = subprocess.run(cmd, + input=script.encode('utf-8'), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=10) + self.assertEqual(int(expect_error), result.returncode, + ('expected success:', + result.stdout.decode('utf-8'), + result.stderr.decode('utf-8'))) + + # Test interactive SamLogon. + def test_samlogon_interactive(self): + client_creds = self._get_creds(ntlm=True) + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + # Test interactive SamLogon when a smartcard is required. + def test_samlogon_interactive_smartcard(self): + client_creds = self._get_creds(ntlm=True) + self.require_smartcard(client_creds) + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_SMARTCARD_LOGON_REQUIRED) + + # Test network SamLogon. + def test_samlogon_network(self): + client_creds = self._get_creds(ntlm=True) + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + # Test network SamLogon when a smartcard is required. + def test_samlogon_network_smartcard(self): + client_creds = self._get_creds(ntlm=True) + self.require_smartcard(client_creds) + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index e41f4677553..67ddc5b605f 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -1969,6 +1969,13 @@ for env, nt_hash in [("ad_dc:local", True), **krb5_environ, 'EXPECT_NT_HASH': int(nt_hash), }) + planoldpythontestsuite( + env, + 'samba.tests.krb5.smartcard_tests', + environ={ + **krb5_environ, + 'EXPECT_NT_HASH': int(nt_hash), + }) planoldpythontestsuite( 'ad_dc', 'samba.tests.krb5.kpasswd_tests', -- 2.39.1 From 132b16c28d684fdd6344cae5c6c6512c89edac21 Mon Sep 17 00:00:00 2001 From: Joseph Sutton Date: Thu, 26 May 2022 20:53:47 +1200 Subject: [PATCH 5/6] TODO: smartcard --- python/samba/tests/krb5/kdc_base_test.py | 32 ++++++++++++++++ python/samba/tests/krb5/kpasswd_tests.py | 44 ++++++++++++++++++++++ python/samba/tests/krb5/smartcard_tests.py | 32 ---------------- 3 files changed, 76 insertions(+), 32 deletions(-) diff --git a/python/samba/tests/krb5/kdc_base_test.py b/python/samba/tests/krb5/kdc_base_test.py index 6d47b3feca7..d1a585702fe 100644 --- a/python/samba/tests/krb5/kdc_base_test.py +++ b/python/samba/tests/krb5/kdc_base_test.py @@ -80,6 +80,7 @@ from samba.dsdb import ( UF_NOT_DELEGATED, UF_PARTIAL_SECRETS_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, + UF_SMARTCARD_REQUIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION ) from samba.dcerpc.misc import ( @@ -3030,6 +3031,37 @@ class KDCBaseTest(TestCaseInTempDir, RawKerberosTest): msg[name] = ldb.MessageElement([], flag, name) samdb.modify(msg) + def require_smartcard(self, creds): + samdb = self.get_samdb() + dn = creds.get_dn() + + res = samdb.search(dn, scope=ldb.SCOPE_BASE, + attrs=['userAccountControl']) + uac = int(res[0].get('userAccountControl', idx=0)) + + # Set the SMARTCARD_REQUIRED bit on the account. + uac |= UF_SMARTCARD_REQUIRED + + msg = ldb.Message(dn) + msg['userAccountControl'] = ldb.MessageElement( + str(uac), + ldb.FLAG_MOD_REPLACE, + 'userAccountControl') + samdb.modify(msg) + + # The account's keys have been randomized, so set the password to + # something we know. + + new_password = generate_random_password(32, 32) + + msg = ldb.Message(dn) + msg['userPassword'] = ldb.MessageElement(new_password, + ldb.FLAG_MOD_REPLACE, + 'userPassword') + samdb.modify(msg) + + creds.update_password(new_password) + def create_ccache(self, cname, ticket, enc_part): """ Lay out a version 4 on-disk credentials cache, to be read using the FILE: protocol. diff --git a/python/samba/tests/krb5/kpasswd_tests.py b/python/samba/tests/krb5/kpasswd_tests.py index 7c63d81c96d..b50dd633d2d 100755 --- a/python/samba/tests/krb5/kpasswd_tests.py +++ b/python/samba/tests/krb5/kpasswd_tests.py @@ -934,6 +934,50 @@ class KpasswdTests(KDCBaseTest): expected_msg, mode=self.KpasswdMode.CHANGE) + def test_kpasswd_set_smartcard(self): + # Create an account for testing. + creds = self._get_creds() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + # Require a smartcard for login. + self.require_smartcard(creds) + + expected_code = KPASSWD_SUCCESS + expected_msg = b'Password changed' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + def test_kpasswd_change_smartcard(self): + # Create an account for testing. + creds = self._get_creds() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + # Require a smartcard for login. + self.require_smartcard(creds) + + expected_code = KPASSWD_SUCCESS + expected_msg = b'Password changed' + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + if __name__ == '__main__': global_asn1_print = False diff --git a/python/samba/tests/krb5/smartcard_tests.py b/python/samba/tests/krb5/smartcard_tests.py index 6ec70246cf0..c8af3b924b5 100755 --- a/python/samba/tests/krb5/smartcard_tests.py +++ b/python/samba/tests/krb5/smartcard_tests.py @@ -24,7 +24,6 @@ import ldb from samba import generate_random_password, ntstatus from samba.dcerpc import netlogon -from samba.dsdb import UF_SMARTCARD_REQUIRED from samba.samdb import SamDB from samba.tests.krb5 import kcrypto @@ -87,37 +86,6 @@ class SmartcardTests(KDCBaseTest): return creds - def require_smartcard(self, creds): - samdb = self.get_samdb() - dn = creds.get_dn() - - res = samdb.search(dn, scope=ldb.SCOPE_BASE, - attrs=['userAccountControl']) - uac = int(res[0].get('userAccountControl', idx=0)) - - # Set the SMARTCARD_REQUIRED bit on the account. - uac |= UF_SMARTCARD_REQUIRED - - msg = ldb.Message(dn) - msg['userAccountControl'] = ldb.MessageElement( - str(uac), - ldb.FLAG_MOD_REPLACE, - 'userAccountControl') - samdb.modify(msg) - - # The account's keys have been randomized, so set the password to - # something we know. - - new_password = generate_random_password(32, 32) - - msg = ldb.Message(dn) - msg['userPassword'] = ldb.MessageElement(new_password, - ldb.FLAG_MOD_REPLACE, - 'userPassword') - samdb.modify(msg) - - creds.update_password(new_password) - def test_ntlm(self): creds = self._get_creds(ntlm=True) -- 2.39.1 From 529d93c754c5be2f9bba82c39fd4a56ff433765e Mon Sep 17 00:00:00 2001 From: Joseph Sutton Date: Fri, 29 Apr 2022 12:25:08 +1200 Subject: [PATCH 6/6] s4-auth: Disable password authentication for accounts requiring smartcard login Signed-off-by: Joseph Sutton --- auth/auth_log.c | 27 +-------------------------- auth/auth_util.c | 25 +++++++++++++++++++++++++ auth/auth_util.h | 17 +++++++++++++++++ source4/auth/auth.h | 1 + source4/auth/ntlm/auth_sam.c | 3 +++ 5 files changed, 47 insertions(+), 26 deletions(-) diff --git a/auth/auth_log.c b/auth/auth_log.c index 9a110fd0b48..87765535889 100644 --- a/auth/auth_log.c +++ b/auth/auth_log.c @@ -52,6 +52,7 @@ #include "includes.h" #include "../lib/tsocket/tsocket.h" +#include "auth_util.h" #include "common_auth.h" #include "lib/util/util_str_escape.h" #include "libcli/security/dom_sid.h" @@ -62,7 +63,6 @@ #include "lib/util/server_id_db.h" #include "lib/param/param.h" #include "librpc/ndr/libndr.h" -#include "librpc/gen_ndr/windows_event_ids.h" #include "lib/audit_logging/audit_logging.h" /* @@ -96,31 +96,6 @@ static void log_json(struct imessaging_context *msg_ctx, } } -/* - * Determine the Windows logon type for the current authorisation attempt. - * - * Currently Samba only supports - * - * 2 Interactive A user logged on to this computer. - * 3 Network A user or computer logged on to this computer from - * the network. - * 8 NetworkCleartext A user logged on to this computer from the network. - * The user's password was passed to the authentication - * package in its unhashed form. - * - */ -static enum event_logon_type get_logon_type( - const struct auth_usersupplied_info *ui) -{ - if ((ui->logon_parameters & MSV1_0_CLEARTEXT_PASSWORD_SUPPLIED) - || (ui->password_state == AUTH_PASSWORD_PLAIN)) { - return EVT_LOGON_NETWORK_CLEAR_TEXT; - } else if (ui->flags & USER_INFO_INTERACTIVE_LOGON) { - return EVT_LOGON_INTERACTIVE; - } - return EVT_LOGON_NETWORK; -} - /* * Write a machine parsable json formatted authentication log entry. * diff --git a/auth/auth_util.c b/auth/auth_util.c index ec9094d0f15..b91dff378ba 100644 --- a/auth/auth_util.c +++ b/auth/auth_util.c @@ -69,3 +69,28 @@ struct auth_session_info *copy_session_info(TALLOC_CTX *mem_ctx, TALLOC_FREE(frame); return dst; } + +/* + * Determine the Windows logon type for the current authorisation attempt. + * + * Currently Samba only supports + * + * 2 Interactive A user logged on to this computer. + * 3 Network A user or computer logged on to this computer from + * the network. + * 8 NetworkCleartext A user logged on to this computer from the network. + * The user's password was passed to the authentication + * package in its unhashed form. + * + */ +enum event_logon_type get_logon_type( + const struct auth_usersupplied_info *ui) +{ + if ((ui->logon_parameters & MSV1_0_CLEARTEXT_PASSWORD_SUPPLIED) + || (ui->password_state == AUTH_PASSWORD_PLAIN)) { + return EVT_LOGON_NETWORK_CLEAR_TEXT; + } else if (ui->flags & USER_INFO_INTERACTIVE_LOGON) { + return EVT_LOGON_INTERACTIVE; + } + return EVT_LOGON_NETWORK; +} diff --git a/auth/auth_util.h b/auth/auth_util.h index 0af6ff5ec6c..ebd95621dbd 100644 --- a/auth/auth_util.h +++ b/auth/auth_util.h @@ -24,9 +24,26 @@ #include "replace.h" #include #include "librpc/gen_ndr/auth.h" +#include "librpc/gen_ndr/windows_event_ids.h" +#include "common_auth.h" struct auth_session_info *copy_session_info( TALLOC_CTX *mem_ctx, const struct auth_session_info *src); +/* + * Determine the Windows logon type for the current authorisation attempt. + * + * Currently Samba only supports + * + * 2 Interactive A user logged on to this computer. + * 3 Network A user or computer logged on to this computer from + * the network. + * 8 NetworkCleartext A user logged on to this computer from the network. + * The user's password was passed to the authentication + * package in its unhashed form. + * + */ +enum event_logon_type get_logon_type(const struct auth_usersupplied_info *ui); + #endif diff --git a/source4/auth/auth.h b/source4/auth/auth.h index 1ea4f11d581..14842e45cac 100644 --- a/source4/auth/auth.h +++ b/source4/auth/auth.h @@ -23,6 +23,7 @@ #include "librpc/gen_ndr/ndr_krb5pac.h" #include "librpc/gen_ndr/auth.h" +#include "../auth/auth_util.h" #include "../auth/common_auth.h" extern const char *krbtgt_attrs[]; diff --git a/source4/auth/ntlm/auth_sam.c b/source4/auth/ntlm/auth_sam.c index b4b231a8ca8..ebb46ef6e67 100644 --- a/source4/auth/ntlm/auth_sam.c +++ b/source4/auth/ntlm/auth_sam.c @@ -857,6 +857,9 @@ static NTSTATUS authsam_authenticate(struct auth4_context *auth_context, TALLOC_FREE(tmp_ctx); return NT_STATUS_NO_SUCH_USER; } + } + + if (get_logon_type(user_info) != EVT_LOGON_NETWORK) { if (acct_flags & ACB_SMARTCARD_REQUIRED) { if (acct_flags & ACB_DISABLED) { DEBUG(2,("authsam_authenticate: Account for user '%s' " -- 2.39.1