diff options
Diffstat (limited to 'python/samba/tests/krb5')
43 files changed, 55285 insertions, 0 deletions
diff --git a/python/samba/tests/krb5/alias_tests.py b/python/samba/tests/krb5/alias_tests.py new file mode 100755 index 0000000..a6a3d03 --- /dev/null +++ b/python/samba/tests/krb5/alias_tests.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2021 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +import ldb + +from samba.tests import delete_force +import samba.tests.krb5.kcrypto as 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_TGT_REVOKED, + NT_PRINCIPAL, +) + +global_asn1_print = False +global_hexdump = False + + +class AliasTests(KDCBaseTest): + def test_dc_alias_rename(self): + self._run_dc_alias(action='rename') + + def test_dc_alias_delete(self): + self._run_dc_alias(action='delete') + + def _run_dc_alias(self, action=None): + target_creds = self.get_dc_creds() + target_name = target_creds.get_username()[:-1] + + self._run_alias(target_name, lambda: target_creds, action=action) + + def test_create_alias_rename(self): + self._run_create_alias(action='rename') + + def test_create_alias_delete(self): + self._run_create_alias(action='delete') + + def _run_create_alias(self, action=None): + target_name = self.get_new_username() + + def create_target(): + samdb = self.get_samdb() + + realm = samdb.domain_dns_name().lower() + + hostname = f'{target_name}.{realm}' + spn = f'ldap/{hostname}' + + details = { + 'dNSHostName': hostname + } + + creds, fn = self.create_account( + samdb, + target_name, + account_type=self.AccountType.COMPUTER, + spn=spn, + additional_details=details) + + return creds + + self._run_alias(target_name, create_target, action=action) + + def _run_alias(self, target_name, target_creds_fn, action=None): + samdb = self.get_samdb() + + mach_name = self.get_new_username() + + # Create a machine account. + mach_creds, mach_dn = self.create_account( + samdb, mach_name, account_type=self.AccountType.COMPUTER) + self.addCleanup(delete_force, samdb, mach_dn) + + mach_sid = mach_creds.get_sid() + realm = mach_creds.get_realm() + + # The account salt doesn't change when the account is renamed. + old_salt = mach_creds.get_salt() + mach_creds.set_forced_salt(old_salt) + + # Rename the account to alias with the target account. + msg = ldb.Message(ldb.Dn(samdb, mach_dn)) + msg['sAMAccountName'] = ldb.MessageElement(target_name, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + mach_creds.set_username(target_name) + + # Get a TGT for the machine account. + tgt = self.get_tgt(mach_creds, kdc_options='0', fresh=True) + + # Check the PAC. + pac_data = self.get_pac_data(tgt.ticket_private['authorization-data']) + + upn = f'{target_name}@{realm.lower()}' + + self.assertEqual(target_name, str(pac_data.account_name)) + self.assertEqual(mach_sid, pac_data.account_sid) + self.assertEqual(target_name, pac_data.logon_name) + self.assertEqual(upn, pac_data.upn) + self.assertEqual(realm, pac_data.domain_name) + + # Rename or delete the machine account. + if action == 'rename': + mach_name2 = self.get_new_username() + + msg = ldb.Message(ldb.Dn(samdb, mach_dn)) + msg['sAMAccountName'] = ldb.MessageElement(mach_name2, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + elif action == 'delete': + samdb.delete(mach_dn) + else: + self.fail(action) + + # Get the credentials for the target account. + target_creds = target_creds_fn() + + # Look up the DNS host name of the target account. + target_dn = target_creds.get_dn() + res = samdb.search(target_dn, + scope=ldb.SCOPE_BASE, + attrs=['dNSHostName']) + target_hostname = str(res[0].get('dNSHostName', idx=0)) + + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['ldap', target_hostname]) + target_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[target_name]) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + authenticator_subkey = self.RandomKey(kcrypto.Enctype.AES256) + + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + padata = self.PA_S4U2Self_create(name=target_cname, + realm=realm, + tgt_session_key=tgt.session_key, + ctype=None) + return [padata], req_body + + expected_error_mode = KDC_ERR_TGT_REVOKED + + # Make a request using S4U2Self. The request should fail. + kdc_exchange_dict = self.tgs_exchange_dict( + expected_crealm=realm, + expected_cname=target_cname, + expected_srealm=realm, + expected_sname=sname, + ticket_decryption_key=target_decryption_key, + generate_padata_fn=generate_s4u2self_padata, + expected_error_mode=expected_error_mode, + check_error_fn=self.generic_check_kdc_error, + check_kdc_private_fn=self.generic_check_kdc_private, + tgt=tgt, + authenticator_subkey=authenticator_subkey, + kdc_options='0', + expect_pac=True, + expect_edata=False) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=realm, + sname=sname, + etypes=etypes) + self.check_error_rep(rep, expected_error_mode) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/as_canonicalization_tests.py b/python/samba/tests/krb5/as_canonicalization_tests.py new file mode 100755 index 0000000..dd94cb6 --- /dev/null +++ b/python/samba/tests/krb5/as_canonicalization_tests.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# +# Copyright (C) Catalyst IT Ltd. 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from enum import Enum, unique +import pyasn1 + +from samba.tests.krb5.kdc_base_test import KDCBaseTest +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +from samba.credentials import DONT_USE_KERBEROS +from samba.dcerpc import krb5pac +from samba.dcerpc.misc import SEC_CHAN_WKSTA +from samba.ndr import ndr_unpack +from samba.tests import DynamicTestCase +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + AES128_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_PREAUTH_REQUIRED, + KRB_AS_REP, + KU_AS_REP_ENC_PART, + KRB_ERROR, + KU_PA_ENC_TIMESTAMP, + KU_TICKET, + PADATA_ENC_TIMESTAMP, + NT_ENTERPRISE_PRINCIPAL, + NT_PRINCIPAL, + NT_SRV_INST, +) + +global_asn1_print = False +global_hexdump = False + + +@unique +class TestOptions(Enum): + Canonicalize = 1 + Enterprise = 2 + UpperRealm = 4 + UpperUserName = 8 + NetbiosRealm = 16 + UPN = 32 + RemoveDollar = 64 + AsReqSelf = 128 + Last = 256 + + def is_set(self, x): + return self.value & x + + +@unique +class CredentialsType(Enum): + User = 1 + Machine = 2 + + def is_set(self, x): + return self.value & x + + +class TestData: + + def __init__(self, options, creds): + self.options = options + self.user_creds = creds + self.user_name = self._get_username(options, creds) + self.realm = self._get_realm(options, creds) + + if TestOptions.Enterprise.is_set(options): + client_name_type = NT_ENTERPRISE_PRINCIPAL + else: + client_name_type = NT_PRINCIPAL + + self.cname = KDCBaseTest.PrincipalName_create( + name_type=client_name_type, names=[self.user_name]) + if TestOptions.AsReqSelf.is_set(options): + self.sname = self.cname + else: + self.sname = KDCBaseTest.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", self.realm]) + self.canonicalize = TestOptions.Canonicalize.is_set(options) + + def _get_realm(self, options, creds): + realm = creds.get_realm() + if TestOptions.NetbiosRealm.is_set(options): + realm = creds.get_domain() + if TestOptions.UpperRealm.is_set(options): + realm = realm.upper() + else: + realm = realm.lower() + return realm + + def _get_username(self, options, creds): + name = creds.get_username() + if TestOptions.RemoveDollar.is_set(options) and name.endswith("$"): + name = name[:-1] + if TestOptions.Enterprise.is_set(options): + realm = creds.get_realm() + name = "{0}@{1}".format(name, realm) + if TestOptions.UpperUserName.is_set(options): + name = name.upper() + return name + + def __repr__(self): + rep = "Test Data: " + rep += "options = '" + "{:08b}".format(self.options) + "'" + rep += "user name = '" + self.user_name + "'" + rep += ", realm = '" + self.realm + "'" + rep += ", cname = '" + str(self.cname) + "'" + rep += ", sname = '" + str(self.sname) + "'" + return rep + + +MACHINE_NAME = "tstkrb5cnnmch" +USER_NAME = "tstkrb5cnnusr" + + +@DynamicTestCase +class KerberosASCanonicalizationTests(KDCBaseTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.user_creds = None + cls.machine_creds = None + + @classmethod + def setUpDynamicTestCases(cls): + + def skip(ct, options): + """ Filter out any mutually exclusive test options """ + if ct != CredentialsType.Machine and\ + TestOptions.RemoveDollar.is_set(options): + return True + if ct != CredentialsType.Machine and\ + TestOptions.AsReqSelf.is_set(options): + return True + return False + + def build_test_name(ct, options): + name = "%sCredentials" % ct.name + for opt in TestOptions: + if opt.is_set(options): + name += ("_%s" % opt.name) + return name + + for ct in CredentialsType: + for x in range(TestOptions.Last.value): + if skip(ct, x): + continue + name = build_test_name(ct, x) + cls.generate_dynamic_test("test", name, x, ct) + + def user_account_creds(self): + if self.user_creds is None: + samdb = self.get_samdb() + type(self).user_creds, _ = self.create_account(samdb, USER_NAME) + + return self.user_creds + + def machine_account_creds(self): + if self.machine_creds is None: + samdb = self.get_samdb() + type(self).machine_creds, _ = self.create_account( + samdb, + MACHINE_NAME, + account_type=self.AccountType.COMPUTER) + self.machine_creds.set_secure_channel_type(SEC_CHAN_WKSTA) + self.machine_creds.set_kerberos_state(DONT_USE_KERBEROS) + + return self.machine_creds + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _test_with_args(self, x, ct): + if ct == CredentialsType.User: + creds = self.user_account_creds() + elif ct == CredentialsType.Machine: + creds = self.machine_account_creds() + else: + raise Exception("Unexpected credential type") + data = TestData(x, creds) + + try: + (rep, as_rep) = self.as_req(data) + except pyasn1.error.PyAsn1Error as e: + import traceback + self.fail("ASN1 Error, Options {0:08b}:{1} {2}".format( + data.options, + traceback.format_exc(), + e)) + # If as_req triggered an expected server error response + # No need to test the response data. + if rep is not None: + # The kvno is optional, heimdal includes it + # MIT does not. + if 'kvno' in rep['enc-part']: + kvno = rep['enc-part']['kvno'] + self.check_kvno(kvno, data) + + cname = rep['cname'] + self.check_cname(cname, data) + + crealm = rep['crealm'].decode('ascii') + self.check_crealm(crealm, data) + + sname = as_rep['sname'] + self.check_sname(sname, data) + + srealm = as_rep['srealm'].decode('ascii') + self.check_srealm(srealm, data) + + if TestOptions.AsReqSelf.is_set(data.options): + ticket_creds = creds + else: + ticket_creds = self.get_krbtgt_creds() + ticket_key = self.TicketDecryptionKey_from_creds(ticket_creds) + + ticket_encpart = rep['ticket']['enc-part'] + self.assertElementEqual(ticket_encpart, 'etype', + ticket_key.etype) + self.assertElementEqual(ticket_encpart, 'kvno', + ticket_key.kvno) + ticket_decpart = ticket_key.decrypt(KU_TICKET, + ticket_encpart['cipher']) + ticket_private = self.der_decode( + ticket_decpart, + asn1Spec=krb5_asn1.EncTicketPart()) + + pac_data = self.get_pac(ticket_private['authorization-data']) + pac = ndr_unpack(krb5pac.PAC_DATA, pac_data) + + for pac_buffer in pac.buffers: + if pac_buffer.type == krb5pac.PAC_TYPE_LOGON_NAME: + if TestOptions.Canonicalize.is_set(data.options): + expected = data.user_creds.get_username() + else: + expected = data.user_name + + self.assertEqual(expected, pac_buffer.info.account_name) + break + else: + self.fail('PAC_TYPE_LOGON_NAME not found') + + def as_req(self, data): + user_creds = data.user_creds + realm = data.realm + + cname = data.cname + sname = data.sname + + till = self.get_KerberosTime(offset=36000) + + kdc_options = "0" + if data.canonicalize: + kdc_options = str(krb5_asn1.KDCOptions('canonicalize')) + + padata = None + + # Set the allowable encryption types + etypes = ( + AES256_CTS_HMAC_SHA1_96, + AES128_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5) + + req = self.AS_REQ_create(padata=padata, + kdc_options=kdc_options, + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + # + # Check the protocol version, should be 5 + self.assertEqual( + rep['pvno'], 5, "Data {0}".format(str(data))) + + self.assertEqual( + rep['msg-type'], KRB_ERROR, "Data {0}".format(str(data))) + + self.assertEqual( + rep['error-code'], + KDC_ERR_PREAUTH_REQUIRED, + "Error code {0}, Data {1}".format(rep['error-code'], str(data))) + + rep_padata = self.der_decode( + rep['e-data'], asn1Spec=krb5_asn1.METHOD_DATA()) + + for pa in rep_padata: + if pa['padata-type'] == 19: + etype_info2 = pa['padata-value'] + break + + etype_info2 = self.der_decode( + etype_info2, asn1Spec=krb5_asn1.ETYPE_INFO2()) + + key = self.PasswordKey_from_etype_info2(user_creds, etype_info2[0]) + + (patime, pausec) = self.get_KerberosTimeWithUsec() + pa_ts = self.PA_ENC_TS_ENC_create(patime, pausec) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + pa_ts = self.EncryptedData_create(key, KU_PA_ENC_TIMESTAMP, pa_ts) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.EncryptedData()) + + pa_ts = self.PA_DATA_create(PADATA_ENC_TIMESTAMP, pa_ts) + + kdc_options = "0" + if data.canonicalize: + kdc_options = str(krb5_asn1.KDCOptions('canonicalize')) + padata = [pa_ts] + + req = self.AS_REQ_create(padata=padata, + kdc_options=kdc_options, + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + # + # Check the protocol version, should be 5 + self.assertEqual( + rep['pvno'], 5, "Data {0}".format(str(data))) + + msg_type = rep['msg-type'] + # Should not have got an error. + # If we did, fail and print the error code to help debugging + self.assertNotEqual( + msg_type, + KRB_ERROR, + "Error code {0}, Data {1}".format( + rep.get('error-code', ''), + str(data))) + + self.assertEqual(msg_type, KRB_AS_REP, "Data {0}".format(str(data))) + + # Decrypt and decode the EncKdcRepPart + enc = key.decrypt(KU_AS_REP_ENC_PART, rep['enc-part']['cipher']) + if enc[0] == 0x7A: + # MIT Kerberos Tags the EncASRepPart as a EncKDCRepPart + # i.e. tag number 26 instead of tag number 25 + as_rep = self.der_decode(enc, asn1Spec=krb5_asn1.EncTGSRepPart()) + else: + as_rep = self.der_decode(enc, asn1Spec=krb5_asn1.EncASRepPart()) + + return (rep, as_rep) + + def check_cname(self, cname, data): + if TestOptions.Canonicalize.is_set(data.options): + expected_name_type = NT_PRINCIPAL + elif TestOptions.Enterprise.is_set(data.options): + expected_name_type = NT_ENTERPRISE_PRINCIPAL + else: + expected_name_type = NT_PRINCIPAL + + name_type = cname['name-type'] + self.assertEqual( + expected_name_type, + name_type, + "cname name-type, Options {0:08b}".format(data.options)) + + ns = cname['name-string'] + name = ns[0].decode('ascii') + + expected = data.user_name + if TestOptions.Canonicalize.is_set(data.options): + expected = data.user_creds.get_username() + self.assertEqual( + expected, + name, + "cname principal, Options {0:08b}".format(data.options)) + + def check_crealm(self, crealm, data): + realm = data.user_creds.get_realm() + self.assertEqual( + realm, crealm, "crealm, Options {0:08b}".format(data.options)) + + def check_sname(self, sname, data): + nt = sname['name-type'] + ns = sname['name-string'] + name = ns[0].decode('ascii') + + if TestOptions.AsReqSelf.is_set(data.options): + expected_name_type = NT_PRINCIPAL + if not TestOptions.Canonicalize.is_set(data.options)\ + and TestOptions.Enterprise.is_set(data.options): + + expected_name_type = NT_ENTERPRISE_PRINCIPAL + + self.assertEqual( + expected_name_type, + nt, + "sname name-type, Options {0:08b}".format(data.options)) + expected = data.user_name + if TestOptions.Canonicalize.is_set(data.options): + expected = data.user_creds.get_username() + self.assertEqual( + expected, + name, + "sname principal, Options {0:08b}".format(data.options)) + else: + self.assertEqual( + NT_SRV_INST, + nt, + "sname name-type, Options {0:08b}".format(data.options)) + self.assertEqual( + 'krbtgt', + name, + "sname principal, Options {0:08b}".format(data.options)) + + realm = ns[1].decode('ascii') + expected = data.realm + if TestOptions.Canonicalize.is_set(data.options): + expected = data.user_creds.get_realm().upper() + self.assertEqual( + expected, + realm, + "sname realm, Options {0:08b}".format(data.options)) + + def check_srealm(self, srealm, data): + realm = data.user_creds.get_realm() + self.assertEqual( + realm, srealm, "srealm, Options {0:08b}".format(data.options)) + + def check_kvno(self, kvno, data): + self.assertEqual( + 1, kvno, "kvno, Options {0:08b}".format(data.options)) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + + unittest.main() diff --git a/python/samba/tests/krb5/as_req_tests.py b/python/samba/tests/krb5/as_req_tests.py new file mode 100755 index 0000000..4d0940c --- /dev/null +++ b/python/samba/tests/krb5/as_req_tests.py @@ -0,0 +1,606 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba import ntstatus +from samba.tests import DynamicTestCase +from samba.tests.krb5.kdc_base_test import KDCBaseTest +import samba.tests.krb5.kcrypto as kcrypto +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +from samba.tests.krb5.rfc4120_constants import ( + KDC_ERR_CLIENT_REVOKED, + KDC_ERR_C_PRINCIPAL_UNKNOWN, + KDC_ERR_S_PRINCIPAL_UNKNOWN, + KDC_ERR_ETYPE_NOSUPP, + KDC_ERR_PREAUTH_REQUIRED, + KU_PA_ENC_TIMESTAMP, + NT_ENTERPRISE_PRINCIPAL, + NT_PRINCIPAL, + NT_SRV_INST, + PADATA_ENC_TIMESTAMP +) + +global_asn1_print = False +global_hexdump = False + + +class AsReqBaseTest(KDCBaseTest): + def _run_as_req_enc_timestamp(self, client_creds, client_account=None, + expected_cname=None, sname=None, + name_type=NT_PRINCIPAL, etypes=None, + expected_error=None, expect_edata=None, + expected_pa_error=None, expect_pa_edata=None, + expect_status=None, + expect_pa_status=None, + kdc_options=None, till=None): + user_name = client_creds.get_username() + if client_account is None: + client_account = user_name + client_kvno = client_creds.get_kvno() + krbtgt_creds = self.get_krbtgt_creds(require_strongest_key=True) + krbtgt_account = krbtgt_creds.get_username() + krbtgt_supported_etypes = krbtgt_creds.tgs_supported_enctypes + realm = krbtgt_creds.get_realm() + + cname = self.PrincipalName_create(name_type=name_type, + names=client_account.split('/')) + if sname is None: + sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=[krbtgt_account, realm]) + + expected_crealm = realm + if expected_cname is None: + expected_cname = cname + expected_srealm = realm + expected_sname = sname + expected_salt = client_creds.get_salt() + + if till is None: + till = self.get_KerberosTime(offset=36000) + + if etypes is None: + etypes = self.get_default_enctypes(client_creds) + if kdc_options is None: + kdc_options = krb5_asn1.KDCOptions('forwardable') + if expected_error is not None: + initial_error_mode = expected_error + else: + initial_error_mode = KDC_ERR_PREAUTH_REQUIRED + + rep, kdc_exchange_dict = self._test_as_exchange( + cname, + realm, + sname, + till, + initial_error_mode, + expected_crealm, + expected_cname, + expected_srealm, + expected_sname, + expected_salt, + etypes, + None, + kdc_options, + creds=client_creds, + expected_supported_etypes=krbtgt_supported_etypes, + expected_account_name=user_name, + pac_request=True, + expect_edata=expect_edata, + expected_status=expect_status) + + if rep['error-code'] != KDC_ERR_PREAUTH_REQUIRED: + return None + + etype_info2 = kdc_exchange_dict['preauth_etype_info2'] + self.assertIsNotNone(etype_info2) + + preauth_key = self.PasswordKey_from_etype_info2(client_creds, + etype_info2[0], + kvno=client_kvno) + + (patime, pausec) = self.get_KerberosTimeWithUsec() + pa_ts = self.PA_ENC_TS_ENC_create(patime, pausec) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + enc_pa_ts_usage = KU_PA_ENC_TIMESTAMP + pa_ts = self.EncryptedData_create(preauth_key, enc_pa_ts_usage, pa_ts) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.EncryptedData()) + + pa_ts = self.PA_DATA_create(PADATA_ENC_TIMESTAMP, pa_ts) + + preauth_padata = [pa_ts] + preauth_error_mode = 0 # AS-REP + if expected_pa_error is not None: + preauth_error_mode = expected_pa_error + + krbtgt_decryption_key = ( + self.TicketDecryptionKey_from_creds(krbtgt_creds)) + + as_rep, kdc_exchange_dict = self._test_as_exchange( + cname, + realm, + sname, + till, + preauth_error_mode, + expected_crealm, + expected_cname, + expected_srealm, + expected_sname, + expected_salt, + etypes, + preauth_padata, + kdc_options, + expected_supported_etypes=krbtgt_supported_etypes, + expected_account_name=user_name, + expect_edata=expect_pa_edata, + expected_status=expect_pa_status, + preauth_key=preauth_key, + ticket_decryption_key=krbtgt_decryption_key, + pac_request=True) + self.assertIsNotNone(as_rep) + + return etype_info2 + + +@DynamicTestCase +class AsReqKerberosTests(AsReqBaseTest): + + @classmethod + def setUpDynamicTestCases(cls): + for (name, idx) in cls.etype_test_permutation_name_idx(): + for pac in [None, True, False]: + tname = "%s_pac_%s" % (name, pac) + targs = (idx, pac) + cls.generate_dynamic_test("test_as_req_no_preauth", tname, *targs) + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _test_as_req_nopreauth(self, + initial_etypes, + pac=None, + initial_kdc_options=None): + client_creds = self.get_client_creds() + client_account = client_creds.get_username() + krbtgt_creds = self.get_krbtgt_creds(require_keys=False) + krbtgt_account = krbtgt_creds.get_username() + realm = krbtgt_creds.get_realm() + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_account]) + sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=[krbtgt_account, realm]) + + expected_crealm = realm + expected_cname = cname + expected_srealm = realm + expected_sname = sname + expected_salt = client_creds.get_salt() + + if any(etype in initial_etypes + for etype in self.get_default_enctypes(client_creds)): + expected_error_mode = KDC_ERR_PREAUTH_REQUIRED + else: + expected_error_mode = KDC_ERR_ETYPE_NOSUPP + + kdc_exchange_dict = self.as_exchange_dict( + creds=client_creds, + expected_crealm=expected_crealm, + expected_cname=expected_cname, + expected_srealm=expected_srealm, + expected_sname=expected_sname, + generate_padata_fn=None, + check_error_fn=self.generic_check_kdc_error, + check_rep_fn=None, + expected_error_mode=expected_error_mode, + expected_salt=expected_salt, + kdc_options=str(initial_kdc_options), + pac_request=pac) + + self._generic_kdc_exchange(kdc_exchange_dict, + cname=cname, + realm=realm, + sname=sname, + etypes=initial_etypes) + + def _test_as_req_no_preauth_with_args(self, etype_idx, pac): + name, etypes = self.etype_test_permutation_by_idx(etype_idx) + self._test_as_req_nopreauth( + pac=pac, + initial_etypes=etypes, + initial_kdc_options=krb5_asn1.KDCOptions('forwardable')) + + def test_as_req_enc_timestamp(self): + client_creds = self.get_client_creds() + self._run_as_req_enc_timestamp(client_creds) + + def test_as_req_enc_timestamp_mac(self): + client_creds = self.get_mach_creds() + self._run_as_req_enc_timestamp(client_creds) + + def test_as_req_enc_timestamp_rc4(self): + client_creds = self.get_client_creds() + self._run_as_req_enc_timestamp( + client_creds, + etypes=(kcrypto.Enctype.RC4,)) + + def test_as_req_enc_timestamp_mac_rc4(self): + client_creds = self.get_mach_creds() + self._run_as_req_enc_timestamp( + client_creds, + etypes=(kcrypto.Enctype.RC4,)) + + def test_as_req_enc_timestamp_rc4_dummy(self): + client_creds = self.get_client_creds() + self._run_as_req_enc_timestamp( + client_creds, + etypes=(kcrypto.Enctype.RC4, + -1111)) + + def test_as_req_enc_timestamp_mac_rc4_dummy(self): + client_creds = self.get_mach_creds() + self._run_as_req_enc_timestamp( + client_creds, + etypes=(kcrypto.Enctype.RC4, + -1111)) + + def test_as_req_enc_timestamp_aes128_rc4(self): + client_creds = self.get_client_creds() + self._run_as_req_enc_timestamp( + client_creds, + etypes=(kcrypto.Enctype.AES128, + kcrypto.Enctype.RC4)) + + def test_as_req_enc_timestamp_mac_aes128_rc4(self): + client_creds = self.get_mach_creds() + self._run_as_req_enc_timestamp( + client_creds, + etypes=(kcrypto.Enctype.AES128, + kcrypto.Enctype.RC4)) + + def test_as_req_enc_timestamp_spn(self): + client_creds = self.get_mach_creds() + spn = client_creds.get_spn() + self._run_as_req_enc_timestamp( + client_creds, client_account=spn, + expected_error=KDC_ERR_C_PRINCIPAL_UNKNOWN, + expect_edata=False) + + def test_as_req_enc_timestamp_spn_realm(self): + samdb = self.get_samdb() + realm = samdb.domain_dns_name().upper() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': f'host/{{account}}.{realm}@{realm}'}) + spn = client_creds.get_spn() + self._run_as_req_enc_timestamp( + client_creds, client_account=spn, + expected_error=KDC_ERR_C_PRINCIPAL_UNKNOWN, + expect_edata=False) + + def test_as_req_enc_timestamp_spn_upn(self): + samdb = self.get_samdb() + realm = samdb.domain_dns_name().upper() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': f'host/{{account}}.{realm}@{realm}', + 'spn': f'host/{{account}}.{realm}'}) + spn = client_creds.get_spn() + self._run_as_req_enc_timestamp(client_creds, client_account=spn) + + def test_as_req_enc_timestamp_spn_enterprise(self): + client_creds = self.get_mach_creds() + spn = client_creds.get_spn() + self._run_as_req_enc_timestamp( + client_creds, client_account=spn, + name_type=NT_ENTERPRISE_PRINCIPAL, + expected_error=KDC_ERR_C_PRINCIPAL_UNKNOWN, + expect_edata=False) + + def test_as_req_enc_timestamp_spn_enterprise_realm(self): + samdb = self.get_samdb() + realm = samdb.domain_dns_name().upper() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': f'host/{{account}}.{realm}@{realm}'}) + spn = client_creds.get_spn() + self._run_as_req_enc_timestamp( + client_creds, + name_type=NT_ENTERPRISE_PRINCIPAL, + client_account=spn, + expected_error=KDC_ERR_C_PRINCIPAL_UNKNOWN, + expect_edata=False) + + def test_as_req_enc_timestamp_spn_upn_enterprise(self): + samdb = self.get_samdb() + realm = samdb.domain_dns_name().upper() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': f'host/{{account}}.{realm}@{realm}', + 'spn': f'host/{{account}}.{realm}'}) + spn = client_creds.get_spn() + self._run_as_req_enc_timestamp( + client_creds, + name_type=NT_ENTERPRISE_PRINCIPAL, + client_account=spn, + expected_error=KDC_ERR_C_PRINCIPAL_UNKNOWN, + expect_edata=False) + + def test_as_req_enterprise_canon(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': upn}) + + user_name = client_creds.get_username() + realm = client_creds.get_realm() + client_account = f'{user_name}@{realm}' + + expected_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + + self._run_as_req_enc_timestamp( + client_creds, + client_account=client_account, + expected_cname=expected_cname, + name_type=NT_ENTERPRISE_PRINCIPAL, + kdc_options=krb5_asn1.KDCOptions('canonicalize')) + + def test_as_req_enterprise_canon_case(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': upn}) + + user_name = client_creds.get_username() + realm = client_creds.get_realm().lower() + client_account = f'{user_name}@{realm}' + + expected_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + + self._run_as_req_enc_timestamp( + client_creds, + client_account=client_account, + expected_cname=expected_cname, + name_type=NT_ENTERPRISE_PRINCIPAL, + kdc_options=krb5_asn1.KDCOptions('canonicalize')) + + def test_as_req_enterprise_canon_mac(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + + user_name = client_creds.get_username() + realm = client_creds.get_realm() + client_account = f'{user_name}@{realm}' + + expected_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + + self._run_as_req_enc_timestamp( + client_creds, + client_account=client_account, + expected_cname=expected_cname, + name_type=NT_ENTERPRISE_PRINCIPAL, + kdc_options=krb5_asn1.KDCOptions('canonicalize')) + + def test_as_req_enterprise_canon_mac_case(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + + user_name = client_creds.get_username() + realm = client_creds.get_realm().lower() + client_account = f'{user_name}@{realm}' + + expected_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + + self._run_as_req_enc_timestamp( + client_creds, + client_account=client_account, + expected_cname=expected_cname, + name_type=NT_ENTERPRISE_PRINCIPAL, + kdc_options=krb5_asn1.KDCOptions('canonicalize')) + + def test_as_req_enterprise_no_canon(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': upn}) + + user_name = client_creds.get_username() + realm = client_creds.get_realm() + client_account = f'{user_name}@{realm}' + + self._run_as_req_enc_timestamp( + client_creds, + client_account=client_account, + name_type=NT_ENTERPRISE_PRINCIPAL, + kdc_options=0) + + def test_as_req_enterprise_no_canon_case(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': upn}) + + user_name = client_creds.get_username() + realm = client_creds.get_realm().lower() + client_account = f'{user_name}@{realm}' + + self._run_as_req_enc_timestamp( + client_creds, + client_account=client_account, + name_type=NT_ENTERPRISE_PRINCIPAL, + kdc_options=0) + + def test_as_req_enterprise_no_canon_mac(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + + user_name = client_creds.get_username() + realm = client_creds.get_realm() + client_account = f'{user_name}@{realm}' + + self._run_as_req_enc_timestamp( + client_creds, + client_account=client_account, + name_type=NT_ENTERPRISE_PRINCIPAL, + kdc_options=0) + + def test_as_req_enterprise_no_canon_mac_case(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + + user_name = client_creds.get_username() + realm = client_creds.get_realm().lower() + client_account = f'{user_name}@{realm}' + + self._run_as_req_enc_timestamp( + client_creds, + client_account=client_account, + name_type=NT_ENTERPRISE_PRINCIPAL, + kdc_options=0) + + # Ensure we can't use truncated well-known principals such as krb@REALM + # instead of krbtgt@REALM. + def test_krbtgt_wrong_principal(self): + client_creds = self.get_client_creds() + + krbtgt_creds = self.get_krbtgt_creds() + + krbtgt_account = krbtgt_creds.get_username() + realm = krbtgt_creds.get_realm() + + # Truncate the name of the krbtgt principal. + krbtgt_account = krbtgt_account[:3] + + wrong_krbtgt_princ = self.PrincipalName_create( + name_type=NT_SRV_INST, + names=[krbtgt_account, realm]) + + if self.strict_checking: + self._run_as_req_enc_timestamp( + client_creds, + sname=wrong_krbtgt_princ, + expected_pa_error=KDC_ERR_S_PRINCIPAL_UNKNOWN, + expect_pa_edata=False) + else: + self._run_as_req_enc_timestamp( + client_creds, + sname=wrong_krbtgt_princ, + expected_error=KDC_ERR_S_PRINCIPAL_UNKNOWN) + + def test_krbtgt_single_component_krbtgt(self): + """Test that we can make a request to the single‐component krbtgt + principal.""" + + client_creds = self.get_client_creds() + + # Create a krbtgt principal with a single component. + single_component_krbtgt_principal = self.PrincipalName_create( + name_type=NT_SRV_INST, + names=['krbtgt']) + + self._run_as_req_enc_timestamp( + client_creds, + sname=single_component_krbtgt_principal, + # Don’t ask for canonicalization. + kdc_options=0) + + # Test that we can make a request for a ticket expiring post-2038. + def test_future_till(self): + client_creds = self.get_client_creds() + + self._run_as_req_enc_timestamp( + client_creds, + till='99990913024805Z') + + def test_logon_hours(self): + """Test making an AS-REQ with a logonHours attribute that disallows + logging in.""" + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'logon_hours': bytes(21)}) + + # Expect to get a CLIENT_REVOKED error. + self._run_as_req_enc_timestamp( + client_creds, + expected_error=(KDC_ERR_CLIENT_REVOKED, KDC_ERR_PREAUTH_REQUIRED), + expect_status=ntstatus.NT_STATUS_INVALID_LOGON_HOURS, + expected_pa_error=KDC_ERR_CLIENT_REVOKED, + expect_pa_status=ntstatus.NT_STATUS_INVALID_LOGON_HOURS) + + def test_logon_hours_wrong_password(self): + """Test making an AS-REQ with a wrong password and a logonHours + attribute that disallows logging in.""" + + # Use a non-cached account so that it is not locked out for other + # tests. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'logon_hours': bytes(21)}, + use_cache=False) + + client_creds.set_password('wrong password') + + # Expect to get a CLIENT_REVOKED error. + self._run_as_req_enc_timestamp( + client_creds, + expected_error=(KDC_ERR_CLIENT_REVOKED, KDC_ERR_PREAUTH_REQUIRED), + expect_status=ntstatus.NT_STATUS_INVALID_LOGON_HOURS, + expected_pa_error=KDC_ERR_CLIENT_REVOKED, + expect_pa_status=ntstatus.NT_STATUS_INVALID_LOGON_HOURS) + + def test_as_req_unicode(self): + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'name_prefix': '🔐'}) + self._run_as_req_enc_timestamp(client_creds) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() + diff --git a/python/samba/tests/krb5/authn_policy_tests.py b/python/samba/tests/krb5/authn_policy_tests.py new file mode 100755 index 0000000..43db839 --- /dev/null +++ b/python/samba/tests/krb5/authn_policy_tests.py @@ -0,0 +1,8903 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd 2023 +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from datetime import datetime +from enum import Enum +import random +import re + +import ldb + +from samba import dsdb, ntstatus +from samba.dcerpc import netlogon, security +from samba.dcerpc import windows_event_ids as win_event +from samba.ndr import ndr_pack +from samba.netcmd.domain.models import AuthenticationPolicy, AuthenticationSilo + +import samba.tests +import samba.tests.krb5.kcrypto as kcrypto +from samba.hresult import HRES_SEC_E_INVALID_TOKEN, HRES_SEC_E_LOGON_DENIED +from samba.tests.krb5.kdc_base_test import GroupType +from samba.tests.krb5.kdc_tgs_tests import KdcTgsBaseTests +from samba.tests.auth_log_base import AuthLogTestBase, NoMessageException +from samba.tests.krb5.raw_testcase import RawKerberosTest +from samba.tests.krb5.rfc4120_constants import ( + FX_FAST_ARMOR_AP_REQUEST, + KDC_ERR_BADOPTION, + KDC_ERR_GENERIC, + KDC_ERR_NEVER_VALID, + KDC_ERR_POLICY, + NT_PRINCIPAL, + NT_SRV_INST, + PADATA_FX_FAST, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +SidType = RawKerberosTest.SidType + +global_asn1_print = False +global_hexdump = False + + +AUTHN_VERSION = {'major': 1, 'minor': 3} +AUTHZ_VERSION = {'major': 1, 'minor': 2} +KDC_AUTHZ_VERSION = {'major': 1, 'minor': 0} + + +class AuditType(Enum): + AUTHN = 'Authentication' + AUTHZ = 'Authorization' + KDC_AUTHZ = 'KDC Authorization' + + +class AuditEvent(Enum): + OK = 'OK' + KERBEROS_DEVICE_RESTRICTION = 'KERBEROS_DEVICE_RESTRICTION' + KERBEROS_SERVER_RESTRICTION = 'KERBEROS_SERVER_RESTRICTION' + NTLM_DEVICE_RESTRICTION = 'NTLM_DEVICE_RESTRICTION' + NTLM_SERVER_RESTRICTION = 'NTLM_SERVER_RESTRICTION' + OTHER_ERROR = 'OTHER_ERROR' + + +class AuditReason(Enum): + NONE = None + DESCRIPTOR_INVALID = 'DESCRIPTOR_INVALID' + DESCRIPTOR_NO_OWNER = 'DESCRIPTOR_NO_OWNER' + SECURITY_TOKEN_FAILURE = 'SECURITY_TOKEN_FAILURE' + ACCESS_DENIED = 'ACCESS_DENIED' + FAST_REQUIRED = 'FAST_REQUIRED' + + +# This decorator helps reduce boilerplate code in log-checking methods. +def policy_check_fn(fn): + def wrapper_fn(self, client_creds, *, + client_policy=None, + client_policy_status=None, + server_policy=None, + server_policy_status=None, + status=None, + event=AuditEvent.OK, + reason=AuditReason.NONE, + **kwargs): + if client_policy_status is not None: + self.assertIsNotNone(client_policy, + 'specified client policy status without ' + 'client policy') + + self.assertIsNone( + server_policy_status, + 'don’t specify both client policy status and server policy ' + 'status (at most one of which can appear in the logs)') + elif server_policy_status is not None: + self.assertIsNotNone(server_policy, + 'specified server policy status without ' + 'server policy') + elif client_policy is not None and server_policy is not None: + self.assertIsNone(status, + 'ambiguous: specify a client policy status or a ' + 'server policy status') + + overall_status = status + if overall_status is None: + overall_status = ntstatus.NT_STATUS_OK + + if client_policy_status is None: + client_policy_status = ntstatus.NT_STATUS_OK + elif status is None and client_policy.enforced: + overall_status = client_policy_status + + if server_policy_status is None: + server_policy_status = ntstatus.NT_STATUS_OK + elif status is None and server_policy.enforced: + overall_status = server_policy_status + + if client_policy_status: + client_policy_event = event + client_policy_reason = reason + else: + client_policy_event = AuditEvent.OK + client_policy_reason = AuditReason.NONE + + if server_policy_status: + server_policy_event = event + server_policy_reason = reason + else: + server_policy_event = AuditEvent.OK + server_policy_reason = AuditReason.NONE + + return fn(self, client_creds, + client_policy=client_policy, + client_policy_status=client_policy_status, + client_policy_event=client_policy_event, + client_policy_reason=client_policy_reason, + server_policy=server_policy, + server_policy_status=server_policy_status, + server_policy_event=server_policy_event, + server_policy_reason=server_policy_reason, + overall_status=overall_status, + **kwargs) + + return wrapper_fn + + +class AuthnPolicyBaseTests(AuthLogTestBase, KdcTgsBaseTests): + @classmethod + def setUpClass(cls): + super().setUpClass() + + as_req_logging_support = samba.tests.env_get_var_value( + 'AS_REQ_LOGGING_SUPPORT', + allow_missing=False) + cls.as_req_logging_support = bool(int(as_req_logging_support)) + + tgs_req_logging_support = samba.tests.env_get_var_value( + 'TGS_REQ_LOGGING_SUPPORT', + allow_missing=False) + cls.tgs_req_logging_support = bool(int(tgs_req_logging_support)) + + cls._max_ticket_life = None + cls._max_renew_life = None + + def take(self, n, iterable, *, take_all=True): + """Yield n items from an iterable.""" + i = -1 + for i in range(n): + try: + yield next(iterable) + except StopIteration: + self.fail(f'expected to find element{i}') + + if take_all: + with self.assertRaises( + StopIteration, + msg=f'got unexpected element after {i+1} elements'): + next(iterable) + + def take_pairs(self, n, iterable, *, take_all=True): + """Yield n pairs of items from an iterable.""" + i = -1 + for i in range(n): + try: + yield next(iterable), next(iterable) + except StopIteration: + self.fail(f'expected to find pair of elements {i}') + + if take_all: + with self.assertRaises( + StopIteration, + msg=f'got unexpected element after {i+1} pairs'): + next(iterable) + + def get_max_ticket_life(self): + if self._max_ticket_life is None: + self._fetch_default_lifetimes() + + return self._max_ticket_life + + def get_max_renew_life(self): + if self._max_renew_life is None: + self._fetch_default_lifetimes() + + return self._max_renew_life + + def _fetch_default_lifetimes(self): + samdb = self.get_samdb() + + domain_policy_dn = samdb.get_default_basedn() + domain_policy_dn.add_child('CN=Default Domain Policy,CN=System') + + res = samdb.search(domain_policy_dn, + scope=ldb.SCOPE_BASE, + attrs=['maxTicketAge', 'maxRenewAge']) + self.assertEqual(1, len(res)) + + max_ticket_age = res[0].get('maxTicketAge', idx=0) + max_renew_age = res[0].get('maxRenewAge', idx=0) + + if max_ticket_age is not None: + max_ticket_age = int(max_ticket_age.decode('utf-8')) + else: + max_ticket_age = 10 + + if max_renew_age is not None: + max_renew_age = int(max_renew_age.decode('utf-8')) + else: + max_renew_age = 7 + + type(self)._max_ticket_life = max_ticket_age * 60 * 60 + type(self)._max_renew_life = max_renew_age * 24 * 60 * 60 + + # Get account credentials for testing. + def _get_creds(self, + account_type=KdcTgsBaseTests.AccountType.USER, + member_of=None, + protected=False, + assigned_policy=None, + assigned_silo=None, + ntlm=False, + spn=None, + allowed_rodc=None, + additional_details=None, + cached=None): + if cached is None: + # Policies and silos are rarely reused between accounts. + cached = assigned_policy is None and assigned_silo is None + + opts = { + 'kerberos_enabled': not ntlm, + 'spn': spn, + } + + members = () + if protected: + samdb = self.get_samdb() + protected_users_group = (f'<SID={samdb.get_domain_sid()}-' + f'{security.DOMAIN_RID_PROTECTED_USERS}>') + members += (protected_users_group,) + if member_of is not None: + members += (member_of,) + if assigned_policy is not None: + opts['assigned_policy'] = str(assigned_policy.dn) + if assigned_silo is not None: + opts['assigned_silo'] = str(assigned_silo.dn) + if allowed_rodc: + opts['allowed_replication_mock'] = True + opts['revealed_to_mock_rodc'] = True + if additional_details is not None: + opts['additional_details'] = self.freeze(additional_details) + + if members: + opts['member_of'] = members + + return self.get_cached_creds(account_type=account_type, + opts=opts, + use_cache=cached) + + def _fast_as_req(self, + client_creds, + target_creds, + armor_tgt, + expected_error=0, + expect_status=None, + expected_status=None, + expected_groups=None, + expect_device_info=None, + expected_device_groups=None, + expect_device_claims=None, + expected_device_claims=None): + client_username = client_creds.get_username() + client_realm = client_creds.get_realm() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + target_name = target_creds.get_username() + target_sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[target_name]) + target_realm = target_creds.get_realm() + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + authenticator_subkey = self.RandomKey(kcrypto.Enctype.AES256) + armor_key = self.generate_armor_key(authenticator_subkey, + armor_tgt.session_key) + + preauth_key = self.PasswordKey_from_creds(client_creds, + kcrypto.Enctype.AES256) + + client_challenge_key = ( + self.generate_client_challenge_key(armor_key, preauth_key)) + fast_padata = [self.get_challenge_pa_data(client_challenge_key)] + + def _generate_fast_padata(kdc_exchange_dict, + _callback_dict, + req_body): + return list(fast_padata), req_body + + etypes = kcrypto.Enctype.AES256, kcrypto.Enctype.RC4 + + if expected_error: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + pac_options = '1' # claims support + + samdb = self.get_samdb() + domain_sid_str = samdb.get_domain_sid() + + if expected_groups is not None: + expected_groups = self.map_sids(expected_groups, None, domain_sid_str) + + if expected_device_groups is not None: + expected_device_groups = self.map_sids(expected_device_groups, None, domain_sid_str) + + kdc_exchange_dict = self.as_exchange_dict( + creds=client_creds, + expected_crealm=client_realm, + expected_cname=client_cname, + expected_srealm=target_realm, + expected_sname=target_sname, + expected_supported_etypes=target_etypes, + ticket_decryption_key=target_decryption_key, + generate_fast_fn=self.generate_simple_fast, + generate_fast_armor_fn=self.generate_ap_req, + generate_fast_padata_fn=_generate_fast_padata, + fast_armor_type=FX_FAST_ARMOR_AP_REQUEST, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error, + expected_salt=client_creds.get_salt(), + expect_status=expect_status, + expected_status=expected_status, + expected_groups=expected_groups, + expect_device_info=expect_device_info, + expected_device_domain_sid=domain_sid_str, + expected_device_groups=expected_device_groups, + expect_device_claims=expect_device_claims, + expected_device_claims=expected_device_claims, + authenticator_subkey=authenticator_subkey, + preauth_key=preauth_key, + armor_key=armor_key, + armor_tgt=armor_tgt, + armor_subkey=authenticator_subkey, + kdc_options='0', + pac_options=pac_options, + # PA-DATA types are not important for these tests. + check_patypes=False) + + rep = self._generic_kdc_exchange( + kdc_exchange_dict, + cname=client_cname, + realm=client_realm, + sname=target_sname, + etypes=etypes) + if expected_error: + self.check_error_rep(rep, expected_error) + return None + else: + self.check_as_reply(rep) + return kdc_exchange_dict['rep_ticket_creds'] + + @staticmethod + def audit_type(msg): + return AuditType(msg['type']) + + @staticmethod + def auth_type(msg): + audit_type = __class__.audit_type(msg) + key = { + AuditType.AUTHN: 'authDescription', + AuditType.AUTHZ: 'authType', + AuditType.KDC_AUTHZ: 'authType', + }[audit_type] + + return msg[audit_type.value][key] + + @staticmethod + def service_description(msg): + audit_type = __class__.audit_type(msg) + return msg[audit_type.value]['serviceDescription'] + + @staticmethod + def client_account(msg): + audit_type = __class__.audit_type(msg) + + key = { + AuditType.AUTHN: 'clientAccount', + AuditType.AUTHZ: 'account', + AuditType.KDC_AUTHZ: 'account', + }[audit_type] + + return msg[audit_type.value][key] + + def filter_msg(self, audit_type, client_name, *, + auth_type=None, + service_description=None): + def _filter_msg(msg): + if audit_type is not self.audit_type(msg): + return False + + if auth_type is not None: + if isinstance(auth_type, re.Pattern): + # Check whether the pattern matches. + if not auth_type.fullmatch(self.auth_type(msg)): + return False + else: + # Just do a standard equality check. + if auth_type != self.auth_type(msg): + return False + + if service_description is not None: + if service_description != self.service_description(msg): + return False + + return client_name == self.client_account(msg) + + return _filter_msg + + PRE_AUTH_RE = re.compile('.* Pre-authentication') + + def as_req_filter(self, client_creds): + username = client_creds.get_username() + realm = client_creds.get_realm() + client_name = f'{username}@{realm}' + + yield self.filter_msg(AuditType.AUTHN, + client_name, + auth_type=self.PRE_AUTH_RE, + service_description='Kerberos KDC') + + def tgs_req_filter(self, client_creds, target_creds): + target_name = target_creds.get_username() + if target_name[-1] == '$': + target_name = target_name[:-1] + target_realm = target_creds.get_realm() + + target_spn = f'host/{target_name}@{target_realm}' + + yield self.filter_msg(AuditType.KDC_AUTHZ, + client_creds.get_username(), + auth_type='TGS-REQ with Ticket-Granting Ticket', + service_description=target_spn) + + def samlogon_filter(self, client_creds, *, logon_type=None): + if logon_type is None: + auth_type = None + elif logon_type == netlogon.NetlogonNetworkInformation: + auth_type = 'network' + elif logon_type == netlogon.NetlogonInteractiveInformation: + auth_type = 'interactive' + else: + self.fail(f'unknown logon type ‘{logon_type}’') + + yield self.filter_msg(AuditType.AUTHN, + client_creds.get_username(), + auth_type=auth_type, + service_description='SamLogon') + + def ntlm_filter(self, client_creds): + username = client_creds.get_username() + + yield self.filter_msg(AuditType.AUTHN, + username, + auth_type='NTLMSSP', + service_description='LDAP') + + yield self.filter_msg(AuditType.AUTHZ, + username, + auth_type='NTLMSSP', + service_description='LDAP') + + def simple_bind_filter(self, client_creds): + yield self.filter_msg(AuditType.AUTHN, + str(client_creds.get_dn()), + auth_type='simple bind/TLS', + service_description='LDAP') + + yield self.filter_msg(AuditType.AUTHZ, + client_creds.get_username(), + auth_type='simple bind', + service_description='LDAP') + + def samr_pwd_change_filter(self, client_creds): + username = client_creds.get_username() + + yield self.filter_msg(AuditType.AUTHN, + username, + auth_type='NTLMSSP', + service_description='SMB2') + + yield self.filter_msg(AuditType.AUTHZ, + username, + auth_type='NTLMSSP', + service_description='SMB2') + + yield self.filter_msg(AuditType.AUTHN, + username, + auth_type='NTLMSSP', + service_description='DCE/RPC') + + yield self.filter_msg(AuditType.AUTHZ, + username, + auth_type='NTLMSSP', + service_description='DCE/RPC') + + # Password changes are attempted twice, with two different methods. + + yield self.filter_msg(AuditType.AUTHN, + username, + auth_type='samr_ChangePasswordUser2', + service_description='SAMR Password Change') + + yield self.filter_msg(AuditType.AUTHN, + username, + auth_type='samr_ChangePasswordUser3', + service_description='SAMR Password Change') + + def nextMessage(self, *args, **kwargs): + """Return the next relevant message, or throw a NoMessageException.""" + msg = super().nextMessage(*args, **kwargs) + self.assert_is_timestamp(msg.pop('timestamp')) + + msg_type = msg.pop('type') + inner = msg.pop(msg_type) + self.assertFalse(msg, 'unexpected items in outer message') + + return inner + + def assert_is_timestamp(self, ts): + try: + datetime.strptime(ts, '%Y-%m-%dT%H:%M:%S.%f%z') + except (TypeError, ValueError): + self.fail(f'‘{ts}’ is not a timestamp') + + def assert_is_guid(self, guid): + guid_re = ( + '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') + self.assertRegex(guid, guid_re) + + def assert_tgt_lifetime(self, checked_creds, policy, expected_policy): + if checked_creds is None: + self.assertNotIn('tgtLifetime', policy) + return + + account_type = checked_creds.get_type() + if account_type is self.AccountType.USER: + expected = expected_policy.user_tgt_lifetime + elif account_type is self.AccountType.COMPUTER: + expected = expected_policy.computer_tgt_lifetime + elif account_type is self.AccountType.MANAGED_SERVICE: + expected = expected_policy.service_tgt_lifetime + else: + self.fail(f'unknown account type {account_type}') + + if expected is not None: + expected /= 60 * 10_000_000 + expected = int(expected) + else: + expected = 0 + + self.assertEqual(policy.pop('tgtLifetime'), expected) + + def assert_event_id(self, audit_event, policy, expected_policy): + event_map = { + AuditEvent.KERBEROS_DEVICE_RESTRICTION: ( + # unenforced + win_event.AUTH_EVT_ID_KERBEROS_DEVICE_RESTRICTION_AUDIT, + # enforced + win_event.AUTH_EVT_ID_KERBEROS_DEVICE_RESTRICTION, + ), + AuditEvent.KERBEROS_SERVER_RESTRICTION: ( + # unenforced + win_event.AUTH_EVT_ID_KERBEROS_SERVER_RESTRICTION_AUDIT, + # enforced + win_event.AUTH_EVT_ID_KERBEROS_SERVER_RESTRICTION, + ), + AuditEvent.NTLM_DEVICE_RESTRICTION: ( + win_event.AUTH_EVT_ID_NONE, # unenforced + win_event.AUTH_EVT_ID_NTLM_DEVICE_RESTRICTION, # enforced + ), + } + + event_ids = event_map.get(audit_event) + if event_ids is not None: + expected_id = event_ids[expected_policy.enforced] + else: + expected_id = win_event.AUTH_EVT_ID_NONE + + self.assertEqual(expected_id, policy.pop('eventId')) + + def check_policy(self, checked_creds, policy, expected_policy, *, + client_creds=None, + expected_silo=None, + policy_status=ntstatus.NT_STATUS_OK, + audit_event=AuditEvent.OK, + reason=AuditReason.NONE): + if expected_policy is None: + self.assertIsNone(policy, 'got unexpected policy') + self.assertIs(ntstatus.NT_STATUS_OK, policy_status) + self.assertIs(AuditEvent.OK, audit_event) + self.assertIs(AuditReason.NONE, reason) + return + + self.assertIsNotNone(policy, 'expected to get a policy') + + policy.pop('location') # A location in the source code, for debugging. + + if checked_creds is not None: + checked_account = checked_creds.get_username() + checked_domain = checked_creds.get_domain() + checked_sid = checked_creds.get_sid() + + self.assertEqual(checked_account, policy.pop('checkedAccount')) + self.assertRegex(policy.pop('checkedAccountFlags'), '^0x[0-9a-f]{8}$') + self.assertEqual(checked_domain, policy.pop('checkedDomain')) + self.assertEqual(checked_sid, policy.pop('checkedSid')) + + logon_server = os.environ['DC_NETBIOSNAME'] + self.assertEqual(logon_server, policy.pop('checkedLogonServer')) + else: + self.assertNotIn('checkedAccount', policy) + self.assertNotIn('checkedAccountFlags', policy) + self.assertNotIn('checkedDomain', policy) + self.assertNotIn('checkedSid', policy) + self.assertNotIn('checkedLogonServer', policy) + + self.assertEqual(expected_policy.enforced, + policy.pop('policyEnforced')) + self.assertEqual(expected_policy.name, policy.pop('policyName')) + + self.assert_tgt_lifetime(client_creds, policy, expected_policy) + + silo_name = expected_silo.name if expected_silo is not None else None + self.assertEqual(silo_name, policy.pop('siloName')) + + got_status = getattr(ntstatus, policy.pop('status')) + self.assertEqual(policy_status, got_status) + + got_audit_event = policy.pop('auditEvent') + try: + got_audit_event = AuditEvent(got_audit_event) + except ValueError: + self.fail('got unrecognized audit event') + self.assertEqual(audit_event, got_audit_event) + self.assert_event_id(audit_event, policy, expected_policy) + + got_reason = policy.pop('reason') + try: + got_reason = AuditReason(got_reason) + except ValueError: + self.fail('got unrecognized audit reason') + self.assertEqual(reason, got_reason) + + self.assertFalse(policy, 'unexpected items remain in policy') + + @policy_check_fn + def check_as_log(self, client_creds, *, + client_policy, + client_policy_status, + client_policy_event, + client_policy_reason, + server_policy, + server_policy_status, + server_policy_event, + server_policy_reason, + overall_status, + armor_creds=None): + if not self.as_req_logging_support: + return + + as_req_filter = self.as_req_filter(client_creds) + for msg_filter in self.take(1, as_req_filter): + try: + msg = self.nextMessage(msg_filter) + except NoMessageException: + self.fail('expected to receive authentication message') + + self.assertEqual(AUTHN_VERSION, msg.pop('version')) + + got_status = getattr(ntstatus, msg.pop('status')) + self.assertEqual(overall_status, got_status) + + got_client_policy = msg.pop('clientPolicyAccessCheck', None) + self.check_policy(armor_creds, got_client_policy, client_policy, + client_creds=client_creds, + policy_status=client_policy_status, + audit_event=client_policy_event, + reason=client_policy_reason) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, server_policy, + policy_status=server_policy_status, + audit_event=server_policy_event, + reason=server_policy_reason) + + def check_tgs_log(self, client_creds, target_creds, *, + policy=None, + policy_status=None, + status=None, + checked_creds=None, + event=AuditEvent.OK, + reason=AuditReason.NONE): + if not self.tgs_req_logging_support: + return + + if checked_creds is None: + checked_creds = client_creds + + overall_status = status if status is not None else ntstatus.NT_STATUS_OK + + if policy_status is None: + policy_status = ntstatus.NT_STATUS_OK + + if policy is not None: + policy_status = overall_status + elif status is None and policy.enforced: + overall_status = status + + client_domain = client_creds.get_domain() + + logon_server = os.environ['DC_NETBIOSNAME'] + + # An example of a typical KDC Authorization log message: + + # { + # "KDC Authorization": { + # "account": "alice", + # "authTime": "2023-06-15T23:45:13.183564+0000", + # "authType": "TGS-REQ with Ticket-Granting Ticket", + # "domain": "ADDOMAIN", + # "localAddress": null, + # "logonServer": "ADDC", + # "remoteAddress": "ipv4:10.53.57.11:28004", + # "serverPolicyAccessCheck": { + # "auditEvent": "KERBEROS_SERVER_RESTRICTION", + # "checkedAccount": "alice", + # "checkedAccountFlags": "0x00000010", + # "checkedDomain": "ADDOMAIN", + # "checkedLogonServer": "ADDC", + # "checkedSid": "S-1-5-21-3907522332-2561495341-3138977981-1159", + # "eventId": 106, + # "location": "../../source4/kdc/authn_policy_util.c:1181", + # "policyEnforced": true, + # "policyName": "Example Policy", + # "reason": "ACCESS_DENIED", + # "siloName": null, + # "status": "NT_STATUS_AUTHENTICATION_FIREWALL_FAILED" + # }, + # "serviceDescription": "host/target@ADDOM.SAMBA.EXAMPLE.COM", + # "sid": "S-1-5-21-3907522332-2561495341-3138977981-1159", + # "status": "NT_STATUS_AUTHENTICATION_FIREWALL_FAILED", + # "version": { + # "major": 1, + # "minor": 0 + # } + # }, + # "timestamp": "2023-06-15T23:45:13.202312+0000", + # "type": "KDC Authorization" + # } + + tgs_req_filter = self.tgs_req_filter(client_creds, target_creds) + for msg_filter in self.take(1, tgs_req_filter): + try: + msg = self.nextMessage(msg_filter) + except NoMessageException: + self.fail('expected to receive KDC authorization message') + + # These parameters have already been checked. + msg.pop('account') + msg.pop('authType') + msg.pop('remoteAddress') + msg.pop('serviceDescription') + + self.assertEqual(KDC_AUTHZ_VERSION, msg.pop('version')) + + self.assert_is_timestamp(msg.pop('authTime')) + self.assertEqual(client_domain, msg.pop('domain')) + self.assertIsNone(msg.pop('localAddress')) + self.assertEqual(logon_server, msg.pop('logonServer')) + self.assertEqual(client_creds.get_sid(), msg.pop('sid')) + + got_status = getattr(ntstatus, msg.pop('status')) + self.assertEqual(overall_status, got_status) + + server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(checked_creds, server_policy, policy, + policy_status=policy_status, + audit_event=event, + reason=reason) + + self.assertFalse(msg, 'unexpected items remain in message') + + @policy_check_fn + def check_samlogon_log(self, client_creds, *, + client_policy, + client_policy_status, + client_policy_event, + client_policy_reason, + server_policy, + server_policy_status, + server_policy_event, + server_policy_reason, + overall_status, + logon_type=None): + samlogon_filter = self.samlogon_filter(client_creds, + logon_type=logon_type) + for msg_filter in self.take(1, samlogon_filter): + try: + msg = self.nextMessage(msg_filter) + except NoMessageException: + self.fail('expected to receive authentication message') + + self.assertEqual(AUTHN_VERSION, msg.pop('version')) + + got_status = getattr(ntstatus, msg.pop('status')) + self.assertEqual(overall_status, got_status) + + got_client_policy = msg.pop('clientPolicyAccessCheck', None) + self.check_policy(None, got_client_policy, client_policy, + policy_status=client_policy_status, + audit_event=client_policy_event, + reason=client_policy_reason) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, server_policy, + policy_status=server_policy_status, + audit_event=server_policy_event, + reason=server_policy_reason) + + def check_samlogon_network_log(self, client_creds, **kwargs): + return self.check_samlogon_log( + client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + **kwargs) + + def check_samlogon_interactive_log(self, client_creds, **kwargs): + return self.check_samlogon_log( + client_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + **kwargs) + + @policy_check_fn + def check_ntlm_log(self, client_creds, *, + client_policy, + client_policy_status, + client_policy_event, + client_policy_reason, + server_policy, + server_policy_status, + server_policy_event, + server_policy_reason, + overall_status): + ntlm_filter = self.ntlm_filter(client_creds) + + for authn_filter, authz_filter in self.take_pairs(1, ntlm_filter): + try: + msg = self.nextMessage(authn_filter) + except NoMessageException: + self.fail('expected to receive authentication message') + + self.assertEqual(AUTHN_VERSION, msg.pop('version')) + + got_status = getattr(ntstatus, msg.pop('status')) + self.assertEqual(overall_status, got_status) + + got_client_policy = msg.pop('clientPolicyAccessCheck', None) + self.check_policy(None, got_client_policy, client_policy, + policy_status=client_policy_status, + audit_event=client_policy_event, + reason=client_policy_reason) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, server_policy, + policy_status=server_policy_status, + audit_event=server_policy_event, + reason=server_policy_reason) + + if overall_status: + # Authentication can proceed no further. + return + + try: + msg = self.nextMessage(authz_filter) + except NoMessageException: + self.fail('expected to receive authorization message') + + self.assertEqual(AUTHZ_VERSION, msg.pop('version')) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, server_policy) + + @policy_check_fn + def check_simple_bind_log(self, client_creds, *, + client_policy, + client_policy_status, + client_policy_event, + client_policy_reason, + server_policy, + server_policy_status, + server_policy_event, + server_policy_reason, + overall_status): + simple_bind_filter = self.simple_bind_filter(client_creds) + + for authn_filter, authz_filter in self.take_pairs(1, + simple_bind_filter): + try: + msg = self.nextMessage(authn_filter) + except NoMessageException: + self.fail('expected to receive authentication message') + + self.assertEqual(AUTHN_VERSION, msg.pop('version')) + + got_status = getattr(ntstatus, msg.pop('status')) + self.assertEqual(overall_status, got_status) + + got_client_policy = msg.pop('clientPolicyAccessCheck', None) + self.check_policy(None, got_client_policy, client_policy, + policy_status=client_policy_status, + audit_event=client_policy_event, + reason=client_policy_reason) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, server_policy, + policy_status=server_policy_status, + audit_event=server_policy_event, + reason=server_policy_reason) + + if overall_status: + # Authentication can proceed no further. + return + + try: + msg = self.nextMessage(authz_filter) + except NoMessageException: + self.fail('expected to receive authorization message') + + self.assertEqual(AUTHZ_VERSION, msg.pop('version')) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, server_policy, + policy_status=server_policy_status, + audit_event=server_policy_event, + reason=server_policy_reason) + + @policy_check_fn + def check_samr_pwd_change_log(self, client_creds, *, + client_policy, + client_policy_status, + client_policy_event, + client_policy_reason, + server_policy, + server_policy_status, + server_policy_event, + server_policy_reason, + overall_status): + pwd_change_filter = self.samr_pwd_change_filter(client_creds) + + # There will be two authorization attempts. + for authn_filter, authz_filter in self.take_pairs(2, + pwd_change_filter, + take_all=False): + try: + msg = self.nextMessage(authn_filter) + except NoMessageException: + self.fail('expected to receive authentication message') + + self.assertEqual(AUTHN_VERSION, msg.pop('version')) + + got_status = getattr(ntstatus, msg.pop('status')) + self.assertEqual(overall_status, got_status) + + got_client_policy = msg.pop('clientPolicyAccessCheck', None) + self.check_policy(None, got_client_policy, client_policy, + policy_status=client_policy_status, + audit_event=client_policy_event, + reason=client_policy_reason) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, server_policy, + policy_status=server_policy_status, + audit_event=server_policy_event, + reason=server_policy_reason) + + if overall_status: + # Authentication can proceed no further. + return + + try: + msg = self.nextMessage(authz_filter) + except NoMessageException: + self.fail('expected to receive authorization message') + + self.assertEqual(AUTHZ_VERSION, msg.pop('version')) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, server_policy, + policy_status=server_policy_status, + audit_event=server_policy_event, + reason=server_policy_reason) + + # There will be two SAMR password change attempts. + for msg_filter in self.take(2, pwd_change_filter): + try: + msg = self.nextMessage(msg_filter) + except NoMessageException: + self.fail('expected to receive SAMR password change message') + + self.assertEqual(AUTHN_VERSION, msg.pop('version')) + + got_status = getattr(ntstatus, msg.pop('status')) + self.assertEqual(ntstatus.NT_STATUS_OK, got_status) + + got_client_policy = msg.pop('clientPolicyAccessCheck', None) + self.check_policy(None, got_client_policy, None, + policy_status=client_policy_status, + audit_event=client_policy_event, + reason=client_policy_reason) + + got_server_policy = msg.pop('serverPolicyAccessCheck', None) + self.check_policy(client_creds, got_server_policy, None, + policy_status=server_policy_status, + audit_event=server_policy_event, + reason=server_policy_reason) + + def check_ticket_times(self, + ticket_creds, + expected_life=None, + expected_renew_life=None): + ticket = ticket_creds.ticket_private + + authtime = ticket['authtime'] + starttime = ticket.get('starttime', authtime) + endtime = ticket['endtime'] + renew_till = ticket.get('renew-till', None) + + starttime = self.get_EpochFromKerberosTime(starttime) + + if expected_life is not None: + actual_end = self.get_EpochFromKerberosTime( + endtime.decode('ascii')) + actual_lifetime = actual_end - starttime + + self.assertEqual(expected_life, actual_lifetime) + + if renew_till is None: + self.assertIsNone(expected_renew_life) + else: + if expected_renew_life is not None: + actual_renew_till = self.get_EpochFromKerberosTime( + renew_till.decode('ascii')) + actual_renew_life = actual_renew_till - starttime + + self.assertEqual(expected_renew_life, actual_renew_life) + + def _get_tgt(self, creds, *, + armor_tgt=None, + till=None, + kdc_options=None, + expected_flags=None, + unexpected_flags=None, + expected_error=0, + expect_status=None, + expected_status=None): + user_name = creds.get_username() + realm = creds.get_realm() + salt = creds.get_salt() + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=user_name.split('/')) + sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=['krbtgt', realm]) + expected_sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=['krbtgt', realm.upper()]) + + expected_cname = cname + + if till is None: + till = self.get_KerberosTime(offset=36000) + + renew_time = till + + krbtgt_creds = self.get_krbtgt_creds() + ticket_decryption_key = ( + self.TicketDecryptionKey_from_creds(krbtgt_creds)) + + expected_etypes = krbtgt_creds.tgs_supported_enctypes + + if kdc_options is None: + kdc_options = str(krb5_asn1.KDCOptions('renewable')) + # Contrary to Microsoft’s documentation, the returned ticket is + # renewable. + expected_flags = krb5_asn1.TicketFlags('renewable') + + preauth_key = self.PasswordKey_from_creds(creds, + kcrypto.Enctype.AES256) + + expected_realm = realm.upper() + + etypes = kcrypto.Enctype.AES256, kcrypto.Enctype.RC4 + + if armor_tgt is not None: + authenticator_subkey = self.RandomKey(kcrypto.Enctype.AES256) + armor_key = self.generate_armor_key(authenticator_subkey, + armor_tgt.session_key) + armor_subkey = authenticator_subkey + + client_challenge_key = self.generate_client_challenge_key( + armor_key, preauth_key) + enc_challenge_padata = self.get_challenge_pa_data( + client_challenge_key) + + def generate_fast_padata_fn(kdc_exchange_dict, + _callback_dict, + req_body): + return [enc_challenge_padata], req_body + + generate_fast_fn = self.generate_simple_fast + generate_fast_armor_fn = self.generate_ap_req + generate_padata_fn = None + + fast_armor_type = FX_FAST_ARMOR_AP_REQUEST + else: + ts_enc_padata = self.get_enc_timestamp_pa_data_from_key( + preauth_key) + + def generate_padata_fn(kdc_exchange_dict, + _callback_dict, + req_body): + return [ts_enc_padata], req_body + + generate_fast_fn = None + generate_fast_padata_fn = None + generate_fast_armor_fn = None + + armor_key = None + armor_subkey = None + + fast_armor_type = None + + if not expected_error: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + else: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + + kdc_exchange_dict = self.as_exchange_dict( + creds=creds, + expected_error_mode=expected_error, + expect_status=expect_status, + expected_status=expected_status, + expected_crealm=expected_realm, + expected_cname=expected_cname, + expected_srealm=expected_realm, + expected_sname=expected_sname, + expected_salt=salt, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + expected_supported_etypes=expected_etypes, + generate_padata_fn=generate_padata_fn, + generate_fast_padata_fn=generate_fast_padata_fn, + generate_fast_fn=generate_fast_fn, + generate_fast_armor_fn=generate_fast_armor_fn, + fast_armor_type=fast_armor_type, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + armor_key=armor_key, + armor_tgt=armor_tgt, + armor_subkey=armor_subkey, + kdc_options=kdc_options, + preauth_key=preauth_key, + ticket_decryption_key=ticket_decryption_key, + # PA-DATA types are not important for these tests. + check_patypes=False) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=cname, + realm=realm, + sname=sname, + till_time=till, + renew_time=renew_time, + etypes=etypes) + if expected_error: + self.check_error_rep(rep, expected_error) + + return None + + self.check_as_reply(rep) + + ticket_creds = kdc_exchange_dict['rep_ticket_creds'] + return ticket_creds + + +class AuthnPolicyTests(AuthnPolicyBaseTests): + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def test_authn_policy_tgt_lifetime_user(self): + # Create an authentication policy with certain TGT lifetimes set. + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=user_life, + expected_renew_life=user_life) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_computer(self): + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + # Create a computer account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the computer lifetime set in the + # policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=computer_life, + expected_renew_life=computer_life) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_service(self): + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the service lifetime set in the + # policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=service_life, + expected_renew_life=service_life) + + self.check_as_log(client_creds) + + def test_authn_silo_tgt_lifetime_user(self): + # Create an authentication policy with certain TGT lifetimes set. + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + # Create a second policy with different lifetimes, so we can verify the + # correct policy is enforced. + wrong_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=444, + computer_tgt_lifetime=555, + service_tgt_lifetime=666) + + # Create an authentication silo with our existing policies. + silo = self.create_authn_silo(user_policy=policy, + computer_policy=wrong_policy, + service_policy=wrong_policy, + enforced=True) + + # Create a user account assigned to the silo. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the + # appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=user_life, + expected_renew_life=user_life) + + self.check_as_log(client_creds) + + def test_authn_silo_tgt_lifetime_computer(self): + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + wrong_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=444, + computer_tgt_lifetime=555, + service_tgt_lifetime=666) + + # Create an authentication silo with our existing policies. + silo = self.create_authn_silo(user_policy=wrong_policy, + computer_policy=policy, + service_policy=wrong_policy, + enforced=True) + + # Create a computer account assigned to the silo. + client_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_silo=silo) + client_dn_str = str(client_creds.get_dn()) + + # Add the computer to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the computer lifetime set in the + # appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=computer_life, + expected_renew_life=computer_life) + + self.check_as_log(client_creds) + + def test_authn_silo_tgt_lifetime_service(self): + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + wrong_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=444, + computer_tgt_lifetime=555, + service_tgt_lifetime=666) + + # Create an authentication silo with our existing policies. + silo = self.create_authn_silo(user_policy=wrong_policy, + computer_policy=wrong_policy, + service_policy=policy, + enforced=True) + + # Create a managed service account assigned to the silo. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_silo=silo) + client_dn_str = str(client_creds.get_dn()) + + # Add the managed service account to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the service lifetime set in the + # appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=service_life, + expected_renew_life=service_life) + + self.check_as_log(client_creds) + + # Test that an authentication silo takes priority over a policy assigned + # directly. + def test_authn_silo_and_policy_tgt_lifetime_user(self): + # Create an authentication policy with certain TGT lifetimes set. + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + # Create a second policy with different lifetimes, so we can verify the + # correct policy is enforced. + wrong_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=444, + computer_tgt_lifetime=555, + service_tgt_lifetime=666) + + # Create an authentication silo with our existing policies. + silo = self.create_authn_silo(user_policy=policy, + computer_policy=wrong_policy, + service_policy=wrong_policy, + enforced=True) + + # Create a user account assigned to the silo, and also to a policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo, + assigned_policy=wrong_policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the + # appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=user_life, + expected_renew_life=user_life) + + self.check_as_log(client_creds) + + def test_authn_silo_and_policy_tgt_lifetime_computer(self): + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + wrong_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=444, + computer_tgt_lifetime=555, + service_tgt_lifetime=666) + + # Create an authentication silo with our existing policies. + silo = self.create_authn_silo(user_policy=wrong_policy, + computer_policy=policy, + service_policy=wrong_policy, + enforced=True) + + # Create a computer account assigned to the silo, and also to a policy. + client_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_silo=silo, + assigned_policy=wrong_policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the computer to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the computer lifetime set in the + # appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=computer_life, + expected_renew_life=computer_life) + + self.check_as_log(client_creds) + + def test_authn_silo_and_policy_tgt_lifetime_service(self): + user_life = 111 + computer_life = 222 + service_life = 333 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life, + computer_tgt_lifetime=computer_life, + service_tgt_lifetime=service_life) + + wrong_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=444, + computer_tgt_lifetime=555, + service_tgt_lifetime=666) + + # Create an authentication silo with our existing policies. + silo = self.create_authn_silo(user_policy=wrong_policy, + computer_policy=wrong_policy, + service_policy=policy, + enforced=True) + + # Create a managed service account assigned to the silo, and also to a + # policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_silo=silo, + assigned_policy=wrong_policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the managed service account to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the service lifetime set in the + # appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=service_life, + expected_renew_life=service_life) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_max(self): + # Create an authentication policy with the maximum allowable TGT + # lifetime set. + INT64_MAX = 0x7fff_ffff_ffff_ffff + max_lifetime = INT64_MAX // 10_000_000 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=max_lifetime) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future, and assert that the actual lifetime is the maximum + # allowed by the Default Domain policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_lifetime) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_min(self): + # Create an authentication policy with the minimum allowable TGT + # lifetime set. + INT64_MIN = -0x8000_0000_0000_0000 + min_lifetime = round(INT64_MIN / 10_000_000) + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=min_lifetime) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of two hours. The request + # should fail with a NEVER_VALID error. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + self._get_tgt(client_creds, till=till, + expected_error=KDC_ERR_NEVER_VALID, + expect_status=True, + expected_status=ntstatus.NT_STATUS_TIME_DIFFERENCE_AT_DC) + + self.check_as_log( + client_creds, + status=ntstatus.NT_STATUS_TIME_DIFFERENCE_AT_DC) + + def test_authn_policy_tgt_lifetime_zero(self): + # Create an authentication policy with the TGT lifetime set to zero. + lifetime = 0 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum + # allowed by the Default Domain Policy + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_one_second(self): + # Create an authentication policy with the TGT lifetime set to one + # second. + lifetime = 1 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the + # appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_kpasswd_lifetime(self): + # Create an authentication policy with the TGT lifetime set to two + # minutes (the lifetime of a kpasswd ticket). + lifetime = 2 * 60 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the + # appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_short_protected(self): + # Create an authentication policy with a short TGT lifetime set. + lifetime = 111 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy, belonging to the + # Protected Users group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + protected=True, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_long_protected(self): + # Create an authentication policy with a long TGT lifetime set. This + # exceeds the lifetime of four hours enforced by Protected Users. + lifetime = 6 * 60 * 60 # 6 hours + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy, belonging to the + # Protected Users group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + protected=True, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of eight hours, and assert + # that the actual lifetime matches the user lifetime set in the policy, + # taking precedence over the lifetime enforced by Protected Users. + till = self.get_KerberosTime(offset=8 * 60 * 60) # 8 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + # This variant of the test is adapted to the behaviour of Windows and MIT + # Kerberos. It asserts that tickets issued to Protected Users are neither + # forwardable nor proxiable. + def test_authn_policy_protected_flags_without_policy_error(self): + # Create an authentication policy with a TGT lifetime set. + lifetime = 6 * 60 * 60 # 6 hours + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy, belonging to the + # Protected Users group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + protected=True, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of eight hours, and request + # that it be renewable, forwardable and proxiable. Show that the + # returned ticket for the protected user is only renewable. + till = self.get_KerberosTime(offset=8 * 60 * 60) # 8 hours + tgt = self._get_tgt( + client_creds, + till=till, + kdc_options=str(krb5_asn1.KDCOptions( + 'renewable,forwardable,proxiable')), + expected_flags=krb5_asn1.TicketFlags('renewable'), + unexpected_flags=krb5_asn1.TicketFlags('forwardable,proxiable')) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + # This variant of the test is adapted to the behaviour of Heimdal + # Kerberos. It asserts that we get a policy error when requesting a + # proxiable ticket. + def test_authn_policy_protected_flags_with_policy_error(self): + # Create an authentication policy with a TGT lifetime set. + lifetime = 6 * 60 * 60 # 6 hours + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy, belonging to the + # Protected Users group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + protected=True, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of eight hours, and request + # that it be renewable and forwardable. Show that the returned ticket + # for the protected user is only renewable. + till = self.get_KerberosTime(offset=8 * 60 * 60) # 8 hours + tgt = self._get_tgt( + client_creds, + till=till, + kdc_options=str(krb5_asn1.KDCOptions('renewable,forwardable')), + expected_flags=krb5_asn1.TicketFlags('renewable'), + unexpected_flags=krb5_asn1.TicketFlags('forwardable')) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + # Request that the Kerberos ticket be proxiable. Show that we get a + # policy error. + self._get_tgt(client_creds, + till=till, + kdc_options=str(krb5_asn1.KDCOptions('proxiable')), + expected_error=KDC_ERR_POLICY) + + self.check_as_log(client_creds, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_tgt_lifetime_zero_protected(self): + # Create an authentication policy with the TGT lifetime set to zero. + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=0) + + # Create a user account with the assigned policy, belonging to the + # Protected Users group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + protected=True, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of six hours, and assert + # that the actual lifetime is the four hours enforced by Protected + # Users. + till = self.get_KerberosTime(offset=6 * 60 * 60) # 6 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=4 * 60 * 60, + expected_renew_life=4 * 60 * 60) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_none_protected(self): + # Create an authentication policy with no TGT lifetime set. + policy = self.create_authn_policy(enforced=True) + + # Create a user account with the assigned policy, belonging to the + # Protected Users group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + protected=True, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of six hours, and assert + # that the actual lifetime is the four hours enforced by Protected + # Users. + till = self.get_KerberosTime(offset=6 * 60 * 60) # 6 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=4 * 60 * 60, + expected_renew_life=4 * 60 * 60) + + self.check_as_log(client_creds) + + def test_authn_policy_tgt_lifetime_unenforced_protected(self): + # Create an unenforced authentication policy with a TGT lifetime set. + lifetime = 123 + policy = self.create_authn_policy(enforced=False, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy, belonging to the + # Protected Users group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + protected=True, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of six hours, and assert + # that the actual lifetime is the four hours enforced by Protected + # Users. + till = self.get_KerberosTime(offset=6 * 60 * 60) # 6 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=4 * 60 * 60, + expected_renew_life=4 * 60 * 60) + + self.check_as_log(client_creds) + + def test_authn_policy_not_enforced(self): + # Create an authentication policy with the TGT lifetime set. The policy + # is not enforced. + lifetime = 123 + policy = self.create_authn_policy(user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum allowed by + # the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_policy_unenforced(self): + # Create an authentication policy with the TGT lifetime set. The policy + # is set to be unenforced. + lifetime = 123 + policy = self.create_authn_policy(enforced=False, + user_tgt_lifetime=lifetime) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum allowed by + # the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_silo_not_enforced(self): + # Create an authentication policy with the TGT lifetime set. + lifetime = 123 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create an authentication silo with our existing policy. The silo is + # not enforced. + silo = self.create_authn_silo(user_policy=policy) + + # Create a user account assigned to the silo. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum allowed by + # the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_silo_unenforced(self): + # Create an authentication policy with the TGT lifetime set. + lifetime = 123 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create an authentication silo with our existing policy. The silo is + # set to be unenforced. + silo = self.create_authn_silo(user_policy=policy, + enforced=False) + + # Create a user account assigned to the silo. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum allowed by + # the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_silo_not_enforced_policy(self): + # Create an authentication policy with the TGT lifetime set. The policy + # is not enforced. + lifetime = 123 + policy = self.create_authn_policy(user_tgt_lifetime=lifetime) + + # Create an authentication silo with our existing policy. + silo = self.create_authn_silo(user_policy=policy, + enforced=True) + + # Create a user account assigned to the silo. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours. Despite the + # fact that the policy is unenforced, the actual lifetime matches the + # user lifetime set in the appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + def test_authn_silo_unenforced_policy(self): + # Create an authentication policy with the TGT lifetime set. The policy + # is set to be unenforced. + lifetime = 123 + policy = self.create_authn_policy(enforced=False, + user_tgt_lifetime=lifetime) + + # Create an authentication silo with our existing policy. + silo = self.create_authn_silo(user_policy=policy, + enforced=True) + + # Create a user account assigned to the silo. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours. Despite the + # fact that the policy is unenforced, the actual lifetime matches the + # user lifetime set in the appropriate policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + def test_authn_silo_not_enforced_and_assigned_policy(self): + # Create an authentication policy with the TGT lifetime set. + silo_lifetime = 123 + silo_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=silo_lifetime) + + # Create an authentication silo with our existing policy. The silo is + # not enforced. + silo = self.create_authn_silo(user_policy=silo_policy) + + # Create a second policy with a different lifetime, so we can verify + # the correct policy is enforced. + lifetime = 456 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account assigned to the silo, and also to the policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo, + assigned_policy=policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum + # allowed by the Default Domain Policy. The directly-assigned + # policy is not enforced. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_silo_unenforced_and_assigned_policy(self): + # Create an authentication policy with the TGT lifetime set. + silo_lifetime = 123 + silo_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=silo_lifetime) + + # Create an authentication silo with our existing policy. The silo is + # set to be unenforced. + silo = self.create_authn_silo(user_policy=silo_policy, + enforced=False) + + # Create a second policy with a different lifetime, so we can verify + # the correct policy is enforced. + lifetime = 456 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account assigned to the silo, and also to the policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo, + assigned_policy=policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum + # allowed by the Default Domain Policy. The directly-assigned + # policy is not enforced. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_silo_not_enforced_policy_and_assigned_policy(self): + # Create an authentication policy with the TGT lifetime set. The policy + # is not enforced. + silo_lifetime = 123 + silo_policy = self.create_authn_policy(user_tgt_lifetime=silo_lifetime) + + # Create an authentication silo with our existing policy. + silo = self.create_authn_silo(user_policy=silo_policy, + enforced=True) + + # Create a second policy with a different lifetime, so we can verify + # the correct policy is enforced. + lifetime = 456 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account assigned to the silo, and also to the policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo, + assigned_policy=policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours. Despite the + # fact that the policy is unenforced, the actual lifetime matches the + # user lifetime set in the appropriate policy. The directly-assigned + # policy is not enforced. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=silo_lifetime, + expected_renew_life=silo_lifetime) + + self.check_as_log(client_creds) + + def test_authn_silo_unenforced_policy_and_assigned_policy(self): + # Create an authentication policy with the TGT lifetime set. The policy + # is set to be unenforced. + silo_lifetime = 123 + silo_policy = self.create_authn_policy(enforced=False, + user_tgt_lifetime=silo_lifetime) + + # Create an authentication silo with our existing policy. + silo = self.create_authn_silo(user_policy=silo_policy, + enforced=True) + + # Create a second policy with a different lifetime, so we can verify + # the correct policy is enforced. + lifetime = 456 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account assigned to the silo, and also to the policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo, + assigned_policy=policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours. Despite the + # fact that the policy is unenforced, the actual lifetime matches the + # user lifetime set in the appropriate policy. The directly-assigned + # policy is not enforced. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=silo_lifetime, + expected_renew_life=silo_lifetime) + + self.check_as_log(client_creds) + + def test_authn_silo_not_a_member(self): + # Create an authentication policy with the TGT lifetime set. + lifetime = 123 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create an authentication silo with our existing policy. + silo = self.create_authn_silo(user_policy=policy, + enforced=True) + + # Create a user account assigned to the silo. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo) + + # Do not add the user to the silo as a member. + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum allowed by + # the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_silo_not_a_member_and_assigned_policy(self): + # Create an authentication policy with the TGT lifetime set. + silo_lifetime = 123 + silo_policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=silo_lifetime) + + # Create an authentication silo with our existing policy. + silo = self.create_authn_silo(user_policy=silo_policy, + enforced=True) + + # Create a second policy with a different lifetime, so we can verify + # the correct policy is enforced. + lifetime = 456 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account assigned to the silo, and also to the policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo, + assigned_policy=policy) + + # Do not add the user to the silo as a member. + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the + # directly-assigned policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + def test_authn_silo_not_assigned(self): + # Create an authentication policy with the TGT lifetime set. + lifetime = 123 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create an authentication silo with our existing policies. + silo = self.create_authn_silo(user_policy=policy, + enforced=True) + + # Create a user account, but don’t assign it to the silo. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future. Assert that the actual lifetime is the maximum allowed by + # the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_silo_not_assigned_and_assigned_policy(self): + # Create an authentication policy with the TGT lifetime set. + lifetime = 123 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create an authentication silo with our existing policies. + silo = self.create_authn_silo(user_policy=policy, + enforced=True) + + # Create a second policy with a different lifetime, so we can verify + # the correct policy is enforced. + lifetime = 456 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=lifetime) + + # Create a user account assigned to the policy, but not to the silo. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the + # directly-assigned policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=lifetime, + expected_renew_life=lifetime) + + self.check_as_log(client_creds) + + def test_authn_silo_no_applicable_policy(self): + # Create an authentication policy with the TGT lifetime set. + user_life = 111 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life) + + # Create an authentication silo containing no policies. + silo = self.create_authn_silo(enforced=True) + + # Create a user account assigned to the silo, and also to a policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo, + assigned_policy=policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future, and assert that the actual lifetime is the maximum + # allowed by the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_authn_silo_no_tgt_lifetime(self): + # Create an authentication policy with no TGT lifetime set. + silo_policy = self.create_authn_policy(enforced=True) + + # Create a second policy with a lifetime set, so we can verify the + # correct policy is enforced. + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=456) + + # Create an authentication silo with our existing policy. + silo = self.create_authn_silo(user_policy=silo_policy, + enforced=True) + + # Create a user account assigned to the silo, and also to a policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_silo=silo, + assigned_policy=policy) + client_dn_str = str(client_creds.get_dn()) + + # Add the user to the silo as a member. + self.add_to_group(client_dn_str, silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future, and assert that the actual lifetime is the maximum + # allowed by the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_not_a_policy(self): + samdb = self.get_samdb() + + not_a_policy = AuthenticationPolicy() + not_a_policy.dn = samdb.get_default_basedn() + + # Create a user account with the assigned policy set to something that + # isn’t a policy. + client_creds = self._get_creds( + account_type=self.AccountType.USER, + assigned_policy=not_a_policy) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future, and assert that the actual lifetime is the maximum + # allowed by the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_not_a_silo(self): + samdb = self.get_samdb() + + not_a_silo = AuthenticationSilo() + not_a_silo.dn = samdb.get_default_basedn() + + # Create a user account assigned to a silo that isn’t a silo. + client_creds = self._get_creds( + account_type=self.AccountType.USER, + assigned_silo=not_a_silo) + + # Request a Kerberos ticket with a ‘till’ time far in the + # future, and assert that the actual lifetime is the maximum + # allowed by the Default Domain Policy. + till = '99991231235959Z' + expected_lifetime = self.get_max_ticket_life() + expected_renew_life = self.get_max_renew_life() + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=expected_lifetime, + expected_renew_life=expected_renew_life) + + self.check_as_log(client_creds) + + def test_not_a_silo_and_policy(self): + samdb = self.get_samdb() + + not_a_silo = AuthenticationSilo() + not_a_silo.dn = samdb.get_default_basedn() + + # Create an authentication policy with the TGT lifetime set. + user_life = 123 + policy = self.create_authn_policy(enforced=True, + user_tgt_lifetime=user_life) + + # Create a user account assigned to a silo that isn’t a silo, and also + # to a policy. + client_creds = self._get_creds( + account_type=self.AccountType.USER, + assigned_silo=not_a_silo, + assigned_policy=policy) + + # Request a Kerberos ticket with a lifetime of two hours, and assert + # that the actual lifetime matches the user lifetime set in the + # directly-assigned policy. + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._get_tgt(client_creds, till=till) + self.check_ticket_times(tgt, expected_life=user_life, + expected_renew_life=user_life) + + self.check_as_log(client_creds) + + def test_authn_policy_allowed_from_empty(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy with no DACL in the security + # descriptor. + allowed_from = 'O:SY' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed_from) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly allows the machine + # account for a user. Include some different TGT lifetimes for testing + # what gets logged. + allowed = f'O:SYD:(A;;CR;;;{mach_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed, + user_tgt_lifetime=120, + computer_tgt_lifetime=240, + service_allowed_from=denied, + service_tgt_lifetime=360) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_deny(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly denies the machine + # account for a user. Include some different TGT lifetimes for testing + # what gets logged. + allowed = 'O:SYD:(A;;CR;;;WD)' + denied = f'O:SYD:(D;;CR;;;{mach_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied, + user_tgt_lifetime=120, + computer_tgt_lifetime=240, + service_allowed_from=allowed, + service_tgt_lifetime=360) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error when trying to authenticate. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_bad_pwd_allowed_from_user_deny(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly denies the machine + # account for a user. + allowed = 'O:SYD:(A;;CR;;;WD)' + denied = f'O:SYD:(D;;CR;;;{mach_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. Use a non-cached + # account so that it is not locked out for other tests. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + cached=False) + + # Set a wrong password. + client_creds.set_password('wrong password') + + # Show that we get a policy error when trying to authenticate. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_service_allow(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly allows the machine + # account for a service. + allowed = f'O:SYD:(A;;CR;;;{mach_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_service_deny(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly denies the machine + # account for a service. + allowed = 'O:SYD:(A;;CR;;;WD)' + denied = f'O:SYD:(D;;CR;;;{mach_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed, + service_allowed_from=denied) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that we get a policy error when trying to authenticate. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_no_owner(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly allows the machine + # account for a user. Omit the owner (O:SY) from the SDDL. Enforce a + # TGT lifetime for testing what gets logged. + allowed = 'D:(A;;CR;;;WD)' + INT64_MAX = 0x7fff_ffff_ffff_ffff + max_lifetime = INT64_MAX // 10_000_000 + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed, + user_tgt_lifetime=max_lifetime) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a generic error if the security descriptor lacks an + # owner. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_GENERIC) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_INVALID_PARAMETER, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.DESCRIPTOR_NO_OWNER, + status=ntstatus.NT_STATUS_UNSUCCESSFUL) + + def test_authn_policy_allowed_from_no_owner_unenforced(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an unenforced authentication policy that explicitly allows the + # machine account for a user. Omit the owner (O:SY) from the SDDL. + allowed = 'D:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=False, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we don’t get an error if the policy is unenforced. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_INVALID_PARAMETER, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.DESCRIPTOR_NO_OWNER) + + def test_authn_policy_allowed_from_owner_self(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly allows the machine + # account for a user. Set the owner to the machine account. + allowed = f'O:{mach_creds.get_sid()}D:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_owner_anon(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly allows the machine + # account for a user. Set the owner to be anonymous. + allowed = 'O:AND:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_no_fast(self): + # Create an authentication policy that restricts authentication. + # Include some different TGT lifetimes for testing what gets logged. + allowed_from = 'O:SY' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed_from, + user_tgt_lifetime=115, + computer_tgt_lifetime=235, + service_tgt_lifetime=355) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we cannot authenticate without using an armor ticket. + self._get_tgt(client_creds, expected_error=KDC_ERR_POLICY, + expect_status=True, + expected_status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + self.check_as_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_INVALID_WORKSTATION, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.FAST_REQUIRED) + + def test_authn_policy_allowed_from_no_fast_negative_lifetime(self): + # Create an authentication policy that restricts + # authentication. Include some negative TGT lifetimes for testing what + # gets logged. + allowed_from = 'O:SY' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed_from, + user_tgt_lifetime=-115, + computer_tgt_lifetime=-235, + service_tgt_lifetime=-355) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we cannot authenticate without using an armor ticket. + self._get_tgt(client_creds, expected_error=KDC_ERR_POLICY, + expect_status=True, + expected_status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + self.check_as_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_INVALID_WORKSTATION, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.FAST_REQUIRED) + + def test_authn_policy_allowed_from_no_fast_unenforced(self): + # Create an unenforced authentication policy that restricts + # authentication. + allowed_from = 'O:SY' + policy = self.create_authn_policy(enforced=False, + user_allowed_from=allowed_from) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we don’t get an error when the policy is unenforced. + self._get_tgt(client_creds) + + self.check_as_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_INVALID_WORKSTATION, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.FAST_REQUIRED) + + def test_authn_policy_allowed_from_user_allow_group_not_a_member(self): + samdb = self.get_samdb() + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a machine account with which to perform FAST and which does + # not belong to the group. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error, as the machine account does not + # belong to the group. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_user_allow_group_member(self): + samdb = self.get_samdb() + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a machine account with which to perform FAST that belongs to + # the group. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'member_of': (group_dn,)}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket, since the + # machine account belongs to the group. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_domain_local_group(self): + samdb = self.get_samdb() + + # Create a new domain-local group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name, + gtype=GroupType.DOMAIN_LOCAL.value) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a machine account with which to perform FAST that belongs to + # the group. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'member_of': (group_dn,)}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that the groups in the armor ticket are expanded to include the + # domain-local group. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_asserted_identity(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that allows accounts with the + # Authentication Authority Asserted Identity SID. + allowed = ( + f'O:SYD:(A;;CR;;;' + f'{security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY})' + ) + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is allowed. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_claims_valid(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that allows accounts with the + # Claims Valid SID. + allowed = f'O:SYD:(A;;CR;;;{security.SID_CLAIMS_VALID})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is allowed. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_compounded_auth(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that allows accounts with the + # Compounded Authentication SID. + allowed = f'O:SYD:(A;;CR;;;{security.SID_COMPOUNDED_AUTHENTICATION})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is denied. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_user_allow_authenticated_users(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that allows accounts with the + # Authenticated Users SID. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_AUTHENTICATED_USERS})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is allowed. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_ntlm_authn(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that allows accounts with the NTLM + # Authentication SID. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_NTLM_AUTHENTICATION})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is denied. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_user_allow_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that explicitly allows the machine + # account for a user. + allowed = f'O:SYD:(A;;CR;;;{mach_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed, + service_allowed_from=denied) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_deny_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that explicitly denies the machine + # account for a user. + allowed = 'O:SYD:(A;;CR;;;WD)' + denied = f'O:SYD:(D;;CR;;;{mach_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error when trying to authenticate. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_service_allow_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that explicitly allows the machine + # account for a service. + allowed = f'O:SYD:(A;;CR;;;{mach_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_service_deny_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that explicitly denies the machine + # account for a service. + allowed = 'O:SYD:(A;;CR;;;WD)' + denied = f'O:SYD:(D;;CR;;;{mach_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed, + service_allowed_from=denied) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that we get a policy error when trying to authenticate. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_user_allow_group_not_a_member_from_rodc(self): + samdb = self.get_samdb() + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a machine account with which to perform FAST and which does + # not belong to the group. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error, as the machine account does not + # belong to the group. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_user_allow_group_member_from_rodc(self): + samdb = self.get_samdb() + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a machine account with which to perform FAST that belongs to + # the group. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'member_of': (group_dn,), + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True}) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we can authenticate using an armor ticket, since the + # machine account belongs to the group. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_domain_local_group_from_rodc(self): + samdb = self.get_samdb() + + # Create a new domain-local group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name, + gtype=GroupType.DOMAIN_LOCAL.value) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a machine account with which to perform FAST that belongs to + # the group. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'member_of': (group_dn,), + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True}) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that the groups in the armor ticket are expanded to include the + # domain-local group. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_asserted_identity_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that allows accounts with the + # Authentication Authority Asserted Identity SID. + allowed = ( + f'O:SYD:(A;;CR;;;' + f'{security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY})' + ) + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is allowed. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_claims_valid_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that allows accounts with the + # Claims Valid SID. + allowed = f'O:SYD:(A;;CR;;;{security.SID_CLAIMS_VALID})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is allowed. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_compounded_authn_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that allows accounts with the + # Compounded Authentication SID. + allowed = f'O:SYD:(A;;CR;;;{security.SID_COMPOUNDED_AUTHENTICATION})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is denied. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_user_allow_authenticated_users_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that allows accounts with the + # Authenticated Users SID. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_AUTHENTICATED_USERS})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is allowed. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_from_user_allow_ntlm_authn_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + mach_tgt = self.issued_by_rodc(self.get_tgt(mach_creds)) + + # Create an authentication policy that allows accounts with the NTLM + # Authentication SID. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_NTLM_AUTHENTICATION})' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication is denied. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_from_user_deny_user(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + mach_sid = mach_creds.get_sid() + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + client_dn = client_creds.get_dn() + client_sid = client_creds.get_sid() + + # Create an authentication policy that explicitly allows the machine + # account for a user, while denying the user account itself. + allowed = f'O:SYD:(A;;CR;;;{mach_sid})(D;;CR;;;{client_sid})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed, + service_allowed_from=denied) + + # Assign the policy to the user account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that authentication is allowed. + self._get_tgt(client_creds, armor_tgt=mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_authn_policy_allowed_to_empty(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy with no DACL in the security + # descriptor. + allowed_to = 'O:SY' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed_to) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, + policy=policy) + + def test_authn_policy_allowed_to_computer_allow(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, + policy=policy) + + def test_authn_policy_allowed_to_computer_deny(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_computer_allow_but_deny_mach(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + mach_sid = mach_creds.get_sid() + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket, while + # explicitly denying the machine account. + allowed = f'O:SYD:(A;;CR;;;{client_sid})(D;;CR;;;{mach_sid})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Despite the documentation’s claims that the machine account is also + # access-checked, obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_mach(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the machine account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{mach_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_no_fast(self): + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed without an armor TGT. + self._tgs_req(tgt, 0, client_creds, target_creds) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_denied_no_fast(self): + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly disallows the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is not allowed. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + expect_edata=self.expect_padata_outer, + expect_status=True, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_computer_allow_asserted_identity(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows accounts with the + # Authentication Authority Asserted Identity SID to obtain a service + # ticket. + allowed = ( + f'O:SYD:(A;;CR;;;' + f'{security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY})' + ) + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_claims_valid(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows accounts with the Claims + # Valid SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_CLAIMS_VALID})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_compounded_auth(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows accounts with the + # Compounded Authentication SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_COMPOUNDED_AUTHENTICATION})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_computer_allow_authenticated_users(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows accounts with the + # Authenticated Users SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_AUTHENTICATED_USERS})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_ntlm_authn(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows accounts with the NTLM + # Authentication SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_NTLM_AUTHENTICATION})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_no_owner(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. Omit + # the owner (O:SY) from the SDDL. + allowed = f'D:(A;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req(tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an + # NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_INVALID_PARAMETER, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_INVALID_PARAMETER, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.DESCRIPTOR_NO_OWNER) + + def test_authn_policy_allowed_to_no_owner_unenforced(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an unenforced authentication policy that applies to a computer + # and explicitly allows the user account to obtain a service + # ticket. Omit the owner (O:SY) from the SDDL. + allowed = f'D:(A;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=False, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, + target_creds, + policy=policy, + policy_status=ntstatus.NT_STATUS_INVALID_PARAMETER, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.DESCRIPTOR_NO_OWNER) + + def test_authn_policy_allowed_to_owner_self(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. Set + # the owner to the user account. + allowed = f'O:{client_sid}D:(A;;CR;;;{client_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_owner_anon(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. Set + # the owner to be anonymous. + allowed = f'O:AND:(A;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_user_allow(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a user and explicitly + # allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=denied) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_user_deny(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a user and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_service_allow(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a managed service and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_service_deny(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a managed service and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_user_allow_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that applies to a user and explicitly + # allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=denied) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_user_deny_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that applies to a user and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_computer_allow_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_deny_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED,) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_service_allow_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that applies to a managed service and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_service_deny_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that applies to a managed service and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_user_allow_group_not_a_member(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account which does not belong to the group. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that we get a policy error, as the user account does not belong + # to the group. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_user_allow_group_member(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account that belongs to the group. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'member_of': (group_dn,)}) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that we can get a service ticket, since the user account belongs + # to the group. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_user_allow_domain_local_group(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a new domain-local group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name, + gtype=GroupType.DOMAIN_LOCAL.value) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account that belongs to the group. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'member_of': (group_dn,)}) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that the groups in the TGT are expanded to include the + # domain-local group. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_asserted_identity_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that allows accounts with the + # Authentication Authority Asserted Identity SID to obtain a service + # ticket. + allowed = ( + f'O:SYD:(A;;CR;;;' + f'{security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY})' + ) + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_claims_valid_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that allows accounts with the Claims + # Valid SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_CLAIMS_VALID})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_compounded_authn_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that allows accounts with the + # Compounded Authentication SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_COMPOUNDED_AUTHENTICATION})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_computer_allow_authenticated_users_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that allows accounts with the + # Authenticated Users SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_AUTHENTICATED_USERS})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_ntlm_authn_from_rodc(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that allows accounts with the NTLM + # Authentication SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_NTLM_AUTHENTICATION})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_user_allow_group_not_a_member_from_rodc(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account which does not belong to the group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + allowed_rodc=True) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that we get a policy error, as the user account does not belong + # to the group. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_user_allow_group_member_from_rodc(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account that belongs to the group. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'member_of': (group_dn,), + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True}) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that we can get a service ticket, since the user account belongs + # to the group. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_user_allow_domain_local_group_from_rodc(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a new domain-local group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name, + gtype=GroupType.DOMAIN_LOCAL.value) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account that belongs to the group. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'member_of': (group_dn,), + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True}) + # Modify the TGT to be issued by an RODC. + tgt = self.issued_by_rodc(self.get_tgt(client_creds)) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that the groups in the TGT are expanded to include the + # domain-local group. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_to_self(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a computer account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + client_dn = client_creds.get_dn() + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Assign the policy to the account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that obtaining a service ticket to ourselves is allowed. + self._tgs_req(tgt, 0, client_creds, client_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, client_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_deny_to_self(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a computer account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + client_dn = client_creds.get_dn() + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Assign the policy to the account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that obtaining a service ticket to ourselves is allowed, despite + # the policy disallowing it. + self._tgs_req(tgt, 0, client_creds, client_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, client_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_to_self_with_self(self): + samdb = self.get_samdb() + + # Create a computer account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + client_dn = client_creds.get_dn() + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Assign the policy to the account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that obtaining a service ticket to ourselves armored with our + # own TGT is allowed. + self._tgs_req(tgt, 0, client_creds, client_creds, + armor_tgt=tgt) + + self.check_tgs_log(client_creds, client_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_deny_to_self_with_self(self): + samdb = self.get_samdb() + + # Create a computer account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + client_dn = client_creds.get_dn() + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly denies the account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Assign the policy to the account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that obtaining a service ticket to ourselves armored with our + # own TGT is allowed, despite the policy’s disallowing it. + self._tgs_req(tgt, 0, client_creds, client_creds, + armor_tgt=tgt) + + self.check_tgs_log(client_creds, client_creds, policy=policy) + + def test_authn_policy_allowed_to_user_allow_s4u2self(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[client_creds.get_username()]) + client_realm = client_creds.get_realm() + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + target_tgt = self.get_tgt(target_creds) + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + padata = self.PA_S4U2Self_create( + name=client_cname, + realm=client_realm, + tgt_session_key=target_tgt.session_key, + ctype=None) + + return [padata], req_body + + # Show that obtaining a service ticket with S4U2Self is allowed. + self._tgs_req(target_tgt, 0, target_creds, target_creds, + expected_cname=client_cname, + generate_fast_padata_fn=generate_s4u2self_padata, + armor_tgt=mach_tgt) + + # The policy does not apply for S4U2Self, and thus does not appear in + # the logs. + self.check_tgs_log(client_creds, target_creds, policy=None) + + def test_authn_policy_allowed_to_user_deny_s4u2self(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[client_creds.get_username()]) + client_realm = client_creds.get_realm() + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + target_tgt = self.get_tgt(target_creds) + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + padata = self.PA_S4U2Self_create( + name=client_cname, + realm=client_realm, + tgt_session_key=target_tgt.session_key, + ctype=None) + + return [padata], req_body + + # Show that obtaining a service ticket with S4U2Self is allowed, + # despite the policy. + self._tgs_req(target_tgt, 0, target_creds, target_creds, + expected_cname=client_cname, + generate_fast_padata_fn=generate_s4u2self_padata, + armor_tgt=mach_tgt) + + # The policy does not apply for S4U2Self, and thus does not appear in + # the logs. + self.check_tgs_log(client_creds, target_creds, policy=None) + + # Obtain a service ticket with S4U2Self and use it to perform constrained + # delegation while a policy is in place. + def test_authn_policy_allowed_to_user_deny_s4u2self_constrained_delegation(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[client_username]) + client_realm = client_creds.get_realm() + client_sid = client_creds.get_sid() + + # Create a target account. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + target_spn = target_creds.get_spn() + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + service_policy = self.create_authn_policy(enforced=True, + computer_allowed_to=denied) + + # Create a computer account with the assigned policy. + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'assigned_policy': str(service_policy.dn), + # Allow delegation to the target service. + 'delegation_to_spn': target_spn, + 'trusted_to_auth_for_delegation': True, + }) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the service account to obtain a service ticket, + # while denying the user. + allowed = f'O:SYD:(A;;CR;;;{service_sid})(D;;CR;;;{client_sid})' + target_policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Assign the policy to the target account. + self.add_attribute(samdb, str(target_creds.get_dn()), + 'msDS-AssignedAuthNPolicy', str(target_policy.dn)) + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + padata = self.PA_S4U2Self_create( + name=client_cname, + realm=client_realm, + tgt_session_key=service_tgt.session_key, + ctype=None) + + return [padata], req_body + + # Make sure the ticket is forwardable, so it can be used with + # constrained delegation. + forwardable_flag = 'forwardable' + client_tkt_options = str(krb5_asn1.KDCOptions(forwardable_flag)) + expected_flags = krb5_asn1.TicketFlags(forwardable_flag) + + # Show that obtaining a service ticket with S4U2Self is allowed, + # despite the policy. + client_service_tkt = self._tgs_req( + service_tgt, 0, service_creds, service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags, + expected_cname=client_cname, + generate_fast_padata_fn=generate_s4u2self_padata, + armor_tgt=mach_tgt) + + # The policy does not apply for S4U2Self, and thus does not appear in + # the logs. + self.check_tgs_log(client_creds, service_creds, policy=None) + + # Now perform constrained delegation with this service ticket. + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Show that obtaining a service ticket with constrained delegation is + # allowed. + self._tgs_req(service_tgt, 0, service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=target_etypes, + expected_proxy_target=target_spn, + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, target_creds, + policy=target_policy, + checked_creds=service_creds) + + def test_authn_policy_s4u2self_not_allowed_from(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that applies to a user and explicitly + # denies authentication with any device. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + client_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[client_creds.get_username()]) + client_realm = client_creds.get_realm() + + # Create a computer account. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + target_tgt = self.get_tgt(target_creds) + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + padata = self.PA_S4U2Self_create( + name=client_cname, + realm=client_realm, + tgt_session_key=target_tgt.session_key, + ctype=None) + + return [padata], req_body + + # Show that obtaining a service ticket with S4U2Self is allowed, + # despite the client’s policy. + self._tgs_req(target_tgt, 0, target_creds, target_creds, + expected_cname=client_cname, + generate_fast_padata_fn=generate_s4u2self_padata, + armor_tgt=mach_tgt) + + # The client’s policy does not apply for S4U2Self, and thus does not + # appear in the logs. + self.check_tgs_log(client_creds, target_creds, policy=None) + + def test_authn_policy_allowed_to_user_allow_s4u2self_inner_fast(self): + """Test that the correct Asserted Identity SID is placed into the PAC + when an S4U2Self requests contains inner FX‐FAST padata.""" + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[client_creds.get_username()]) + client_realm = client_creds.get_realm() + + # Create a target account. + target_creds = self.get_service_creds() + target_tgt = self.get_tgt(target_creds) + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + s4u2self_padata = self.PA_S4U2Self_create( + name=client_cname, + realm=client_realm, + tgt_session_key=target_tgt.session_key, + ctype=None) + + # Add empty FX‐FAST padata to the inner request. + fx_fast_padata = self.PA_DATA_create(PADATA_FX_FAST, b'') + + padata = [s4u2self_padata, fx_fast_padata] + + return padata, req_body + + # Check that the PAC contains the correct groups. + self._tgs_req( + target_tgt, 0, target_creds, target_creds, + expected_cname=client_cname, + generate_fast_padata_fn=generate_s4u2self_padata, + armor_tgt=mach_tgt, + expected_groups={ + ( + # Expect to get the Service Asserted Identity SID. + security.SID_SERVICE_ASSERTED_IDENTITY, + SidType.EXTRA_SID, + security.SE_GROUP_DEFAULT_FLAGS, + ), + ..., + }, + unexpected_groups={ + # Expect not to get the Authentication Authority Asserted + # Identity SID. + security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, + }) + + def test_authn_policy_allowed_to_user_allow_constrained_delegation(self): + samdb = self.get_samdb() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a target account. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + target_spn = target_creds.get_spn() + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'delegation_to_spn': target_spn, + }) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the service account to obtain a service ticket, + # while denying the user. + allowed = f'O:SYD:(A;;CR;;;{service_sid})(D;;CR;;;{client_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Assign the policy to the target account. + self.add_attribute(samdb, str(target_creds.get_dn()), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following constrained delegation request to the service. + self.discardMessages() + + # Show that obtaining a service ticket with constrained delegation is + # allowed. + self._tgs_req(service_tgt, 0, service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=target_etypes, + expected_proxy_target=target_spn, + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, target_creds, + policy=policy, + checked_creds=service_creds) + + def test_authn_policy_allowed_to_user_deny_constrained_delegation(self): + samdb = self.get_samdb() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a target account. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + target_spn = target_creds.get_spn() + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'delegation_to_spn': target_spn, + }) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Create an authentication policy that applies to a computer and + # explicitly denies the service account to obtain a service ticket, + # while allowing the user. + denied = f'O:SYD:(D;;CR;;;{service_sid})(A;;CR;;;{client_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=denied) + + # Assign the policy to the target account. + self.add_attribute(samdb, str(target_creds.get_dn()), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following constrained delegation request to the service. + self.discardMessages() + + # Show that obtaining a service ticket with constrained delegation is + # not allowed. + self._tgs_req( + service_tgt, KDC_ERR_POLICY, service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + service_creds, target_creds, + policy=policy, + checked_creds=service_creds, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_constrained_delegation_not_allowed_from(self): + samdb = self.get_samdb() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create an authentication policy that applies to a user and explicitly + # denies authentication with any device. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied) + + # Assign the policy to the client account. + self.add_attribute(samdb, str(client_creds.get_dn()), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a target account. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + target_spn = target_creds.get_spn() + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'delegation_to_spn': target_spn, + }) + service_tgt = self.get_tgt(service_creds) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following constrained delegation request to the service. + self.discardMessages() + + # Show that obtaining a service ticket with constrained delegation is + # allowed, despite the client’s policy. + self._tgs_req(service_tgt, 0, service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=target_etypes, + expected_proxy_target=target_spn, + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, target_creds, + policy=None, + checked_creds=service_creds) + + def test_authn_policy_rbcd_not_allowed_from(self): + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create an authentication policy that applies to a user and explicitly + # denies authentication with any device. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied) + + # Assign the policy to the client account. + self.add_attribute(samdb, str(client_creds.get_dn()), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 1}) + service_tgt = self.get_tgt(service_creds) + + # Create a target account. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'delegation_from_dn': str(service_creds.get_dn()), + }) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following RBCD request to the service. + self.discardMessages() + + # Show that obtaining a service ticket with RBCD is allowed, despite + # the client’s policy. + self._tgs_req(service_tgt, 0, service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options='1001', # supports claims, RBCD + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=target_etypes, + expected_proxy_target=target_creds.get_spn(), + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, target_creds, + policy=None, + checked_creds=service_creds) + + def test_authn_policy_allowed_to_user_allow_constrained_delegation_wrong_sname(self): + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a target account. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 1}) + target_spn = target_creds.get_spn() + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'delegation_to_spn': target_spn}) + service_tgt = self.get_tgt(service_creds) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags, + fresh=True) + # Change the ‘sname’ of the ticket to an incorrect value. + client_service_tkt.set_sname(self.get_krbtgt_sname()) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following constrained delegation request to the service. + self.discardMessages() + + # Show that obtaining a service ticket with constrained delegation + # fails if the sname doesn’t match. + self._tgs_req(service_tgt, KDC_ERR_BADOPTION, + service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expect_edata=self.expect_padata_outer, + check_patypes=False) + + self.check_tgs_log( + service_creds, target_creds, + checked_creds=service_creds, + status=ntstatus.NT_STATUS_UNSUCCESSFUL) + + def test_authn_policy_allowed_to_user_allow_rbcd(self): + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 1}) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the service account to obtain a service ticket, + # while denying the user. + allowed = f'O:SYD:(A;;CR;;;{service_sid})(D;;CR;;;{client_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a target account with the assigned policy. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'assigned_policy': str(policy.dn), + 'delegation_from_dn': str(service_creds.get_dn()), + }) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following RBCD request to the service. + self.discardMessages() + + # Show that obtaining a service ticket with RBCD is allowed. + self._tgs_req(service_tgt, 0, service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options='1001', # supports claims, RBCD + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=target_etypes, + expected_proxy_target=target_creds.get_spn(), + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, target_creds, + policy=policy, + checked_creds=service_creds) + + def test_authn_policy_allowed_to_user_deny_rbcd(self): + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 1}) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Create an authentication policy that applies to a computer and + # explicitly denies the service account to obtain a service ticket, + # while allowing the user. + denied = f'O:SYD:(D;;CR;;;{service_sid})(A;;CR;;;{client_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=denied) + + # Create a target account with the assigned policy. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'assigned_policy': str(policy.dn), + 'delegation_from_dn': str(service_creds.get_dn()), + }) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following RBCD request to the service. + self.discardMessages() + + # Show that obtaining a service ticket with RBCD is not allowed. + self._tgs_req( + service_tgt, KDC_ERR_POLICY, service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options='1001', # supports claims, RBCD + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expect_edata=self.expect_padata_outer, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + service_creds, target_creds, + policy=policy, + checked_creds=service_creds, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_user_allow_rbcd_wrong_sname(self): + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 1}) + service_tgt = self.get_tgt(service_creds) + + # Create a target account with the assigned policy. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'delegation_from_dn': str(service_creds.get_dn()), + }) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags, + fresh=True) + # Change the ‘sname’ of the ticket to an incorrect value. + client_service_tkt.set_sname(self.get_krbtgt_sname()) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following RBCD request to the service. + self.discardMessages() + + # Show that obtaining a service ticket with RBCD fails if the sname + # doesn’t match. + self._tgs_req(service_tgt, KDC_ERR_BADOPTION, + service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options='1001', # supports claims, RBCD + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expect_edata=self.expect_padata_outer, + check_patypes=False) + + self.check_tgs_log(service_creds, target_creds, + checked_creds=service_creds, + status=ntstatus.NT_STATUS_UNSUCCESSFUL) + + def test_authn_policy_allowed_to_user_allow_constrained_delegation_to_self(self): + samdb = self.get_samdb() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a service account. + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + service_dn_str = str(service_creds.get_dn()) + service_spn = service_creds.get_spn() + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Allow delegation to ourselves. + self.add_attribute(samdb, service_dn_str, + 'msDS-AllowedToDelegateTo', service_spn) + + # Create an authentication policy that applies to a computer and + # explicitly allows the client account to obtain a service ticket, + # while denying the service. + allowed = f'O:SYD:(A;;CR;;;{client_sid})(D;;CR;;;{service_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Assign the policy to the service account. + self.add_attribute(samdb, service_dn_str, + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + target_etypes = service_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following constrained delegation request to the service. + self.discardMessages() + + # Show that obtaining a service ticket to ourselves with constrained + # delegation is allowed. + self._tgs_req(service_tgt, 0, service_creds, service_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=target_etypes, + expected_proxy_target=service_spn, + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, service_creds, + policy=policy, + checked_creds=service_creds) + + def test_authn_policy_allowed_to_user_deny_constrained_delegation_to_self(self): + samdb = self.get_samdb() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a service account. + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + service_dn_str = str(service_creds.get_dn()) + service_spn = service_creds.get_spn() + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Allow delegation to ourselves. + self.add_attribute(samdb, service_dn_str, + 'msDS-AllowedToDelegateTo', service_spn) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create an authentication policy that applies to a computer and + # explicitly denies the client account to obtain a service ticket, + # while allowing the service. + allowed = f'O:SYD:(D;;CR;;;{client_sid})(A;;CR;;;{service_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Assign the policy to the service account. + self.add_attribute(samdb, service_dn_str, + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + target_etypes = service_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following constrained delegation request to the service. + self.discardMessages() + + # Show that obtaining a service ticket to ourselves with constrained + # delegation is allowed, despite the policy’s disallowing it. + self._tgs_req(service_tgt, 0, service_creds, service_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=target_etypes, + expected_proxy_target=service_spn, + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, service_creds, + policy=policy, + checked_creds=service_creds) + + def test_authn_policy_allowed_to_user_not_allowed_constrained_delegation_to_self(self): + samdb = self.get_samdb() + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a service account. + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + service_dn_str = str(service_creds.get_dn()) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Don’t set msDS-AllowedToDelegateTo. + + # Create an authentication policy that applies to a computer and + # explicitly allows the client account to obtain a service ticket, + # while denying the service. + allowed = f'O:SYD:(A;;CR;;;{client_sid})(D;;CR;;;{service_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Assign the policy to the service account. + self.add_attribute(samdb, service_dn_str, + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following constrained delegation request to the service. + self.discardMessages() + + # Show that obtaining a service ticket to ourselves with constrained + # delegation is not allowed without msDS-AllowedToDelegateTo. + self._tgs_req(service_tgt, KDC_ERR_BADOPTION, + service_creds, service_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expect_edata=self.expect_padata_outer, + check_patypes=False) + + self.check_tgs_log( + service_creds, service_creds, + # The failure is not due to a policy error, so no policy appears in + # the logs. + policy=None, + checked_creds=service_creds, + status=ntstatus.NT_STATUS_UNSUCCESSFUL) + + def test_authn_policy_allowed_to_user_allow_rbcd_to_self(self): + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a service account allowed to delegate to itself. We can’t use + # a more specific ACE containing the account’s SID (obtained + # post-creation) as Samba (unlike Windows) won’t let us modify + # msDS-AllowedToActOnBehalfOfOtherIdentity without being System. + domain_sid = security.dom_sid(samdb.get_domain_sid()) + security_descriptor = security.descriptor.from_sddl( + 'O:BAD:(A;;CR;;;WD)', domain_sid) + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'delegation_from_dn': ndr_pack(security_descriptor)}, + use_cache=False) + service_dn_str = str(service_creds.get_dn()) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the client account to obtain a service ticket, + # while denying the service. + allowed = f'O:SYD:(A;;CR;;;{client_sid})(D;;CR;;;{service_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Assign the policy to the service account. + self.add_attribute(samdb, service_dn_str, + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + service_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + service_etypes = service_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following RBCD request to the service. + self.discardMessages() + + # Show that obtaining a service ticket to ourselves with RBCD is + # allowed. + self._tgs_req(service_tgt, 0, service_creds, service_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options='1001', # supports claims, RBCD + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=service_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=service_etypes, + expected_proxy_target=service_creds.get_spn(), + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, service_creds, + policy=policy, + checked_creds=service_creds) + + def test_authn_policy_allowed_to_user_deny_rbcd_to_self(self): + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a service account allowed to delegate to itself. We can’t use + # a more specific ACE containing the account’s SID (obtained + # post-creation) as Samba (unlike Windows) won’t let us modify + # msDS-AllowedToActOnBehalfOfOtherIdentity without being System. + domain_sid = security.dom_sid(samdb.get_domain_sid()) + security_descriptor = security.descriptor.from_sddl( + 'O:BAD:(A;;CR;;;WD)', domain_sid) + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'delegation_from_dn': ndr_pack(security_descriptor)}, + use_cache=False) + service_dn_str = str(service_creds.get_dn()) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create an authentication policy that applies to a computer and + # explicitly denies the client account to obtain a service ticket, + # while allowing the service. + allowed = f'O:SYD:(D;;CR;;;{client_sid})(A;;CR;;;{service_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Assign the policy to the service account. + self.add_attribute(samdb, service_dn_str, + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + service_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + service_etypes = service_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following RBCD request to the service. + self.discardMessages() + + # Show that obtaining a service ticket to ourselves with RBCD is + # allowed, despite the policy’s disallowing it. + self._tgs_req(service_tgt, 0, service_creds, service_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options='1001', # supports claims, RBCD + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=service_decryption_key, + expected_sid=client_sid, + expected_supported_etypes=service_etypes, + expected_proxy_target=service_creds.get_spn(), + expected_transited_services=expected_transited_services) + + self.check_tgs_log(client_creds, service_creds, + policy=policy, + checked_creds=service_creds) + + def test_authn_policy_allowed_to_user_not_allowed_rbcd_to_self(self): + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + client_sid = client_creds.get_sid() + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a service account. + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + use_cache=False) + service_dn_str = str(service_creds.get_dn()) + service_sid = service_creds.get_sid() + service_tgt = self.get_tgt(service_creds) + + # Don’t set msDS-AllowedToActOnBehalfOfOtherIdentity. + + # Create an authentication policy that applies to a computer and + # explicitly allows the client account to obtain a service ticket, + # while denying the service. + allowed = f'O:SYD:(A;;CR;;;{client_sid})(D;;CR;;;{service_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Assign the policy to the service account. + self.add_attribute(samdb, service_dn_str, + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + service_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + + # Don’t confuse the client’s TGS-REQ to the service, above, with the + # following RBCD request to the service. + self.discardMessages() + + # Show that obtaining a service ticket to ourselves with RBCD + # is not allowed without msDS-AllowedToActOnBehalfOfOtherIdentity. + self._tgs_req(service_tgt, KDC_ERR_BADOPTION, + service_creds, service_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options='1001', # supports claims, RBCD + additional_ticket=client_service_tkt, + decryption_key=service_decryption_key, + expect_edata=self.expect_padata_outer, + check_patypes=False) + + self.check_tgs_log(service_creds, service_creds, + # The failure is not due to a policy error, so no + # policy appears in the logs. + policy=None, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + checked_creds=service_creds) + + def test_authn_policy_allowed_to_computer_allow_user2user(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + client_creds = self.get_mach_creds() + client_tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + target_tgt = self._get_tgt(target_creds) + + kdc_options = str(krb5_asn1.KDCOptions('enc-tkt-in-skey')) + + # Show that obtaining a service ticket with user-to-user is allowed. + self._tgs_req(client_tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + additional_ticket=target_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_deny_user2user(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + client_creds = self.get_mach_creds() + client_tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + target_tgt = self._get_tgt(target_creds) + + kdc_options = str(krb5_asn1.KDCOptions('enc-tkt-in-skey')) + + # Show that obtaining a service ticket with user-to-user is not + # allowed. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + additional_ticket=target_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + self.check_tgs_log( + client_creds, target_creds, + policy=policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_authn_policy_allowed_to_user_derived_class_allow(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a user and explicitly + # allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=denied) + + # Create a schema class derived from ‘user’. + class_id = random.randint(0, 100000000) + user_class_cn = f'my-User-Class-{class_id}' + user_class = user_class_cn.replace('-', '') + class_dn = samdb.get_schema_basedn() + class_dn.add_child(f'CN={user_class_cn}') + governs_id = f'1.3.6.1.4.1.7165.4.6.2.9.{class_id}' + + samdb.add({ + 'dn': class_dn, + 'objectClass': 'classSchema', + 'subClassOf': 'user', + 'governsId': governs_id, + 'lDAPDisplayName': user_class, + }) + + # Create an account derived from ‘user’ with the assigned policy. + target_name = self.get_new_username() + target_creds, target_dn = self.create_account( + samdb, target_name, + account_type=self.AccountType.USER, + spn='host/{account}', + additional_details={ + 'msDS-AssignedAuthNPolicy': str(policy.dn), + 'objectClass': user_class, + }) + + keys = self.get_keys(target_creds) + self.creds_set_keys(target_creds, keys) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_derived_class_allow(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a schema class derived from ‘computer’. + class_id = random.randint(0, 100000000) + computer_class_cn = f'my-Computer-Class-{class_id}' + computer_class = computer_class_cn.replace('-', '') + class_dn = samdb.get_schema_basedn() + class_dn.add_child(f'CN={computer_class_cn}') + governs_id = f'1.3.6.1.4.1.7165.4.6.2.9.{class_id}' + + samdb.add({ + 'dn': class_dn, + 'objectClass': 'classSchema', + 'subClassOf': 'computer', + 'governsId': governs_id, + 'lDAPDisplayName': computer_class, + }) + + # Create an account derived from ‘computer’ with the assigned policy. + target_name = self.get_new_username() + target_creds, target_dn = self.create_account( + samdb, target_name, + account_type=self.AccountType.COMPUTER, + spn=f'host/{target_name}', + additional_details={ + 'msDS-AssignedAuthNPolicy': str(policy.dn), + 'objectClass': computer_class, + }) + + keys = self.get_keys(target_creds) + self.creds_set_keys(target_creds, keys) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_service_derived_class_allow(self): + samdb = self.get_samdb() + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that applies to a managed service and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a schema class derived from ‘msDS-ManagedServiceAccount’. + class_id = random.randint(0, 100000000) + service_class_cn = f'my-Managed-Service-Class-{class_id}' + service_class = service_class_cn.replace('-', '') + class_dn = samdb.get_schema_basedn() + class_dn.add_child(f'CN={service_class_cn}') + governs_id = f'1.3.6.1.4.1.7165.4.6.2.9.{class_id}' + + samdb.add({ + 'dn': class_dn, + 'objectClass': 'classSchema', + 'subClassOf': 'msDS-ManagedServiceAccount', + 'governsId': governs_id, + 'lDAPDisplayName': service_class, + }) + + # Create an account derived from ‘msDS-ManagedServiceAccount’ with the + # assigned policy. + target_name = self.get_new_username() + target_creds, target_dn = self.create_account( + samdb, target_name, + account_type=self.AccountType.MANAGED_SERVICE, + spn=f'host/{target_name}', + additional_details={ + 'msDS-AssignedAuthNPolicy': str(policy.dn), + 'objectClass': service_class, + }) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_authn_policy_allowed_to_computer_allow_as_req(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket with an AS-REQ is allowed. + self._fast_as_req(client_creds, target_creds, mach_tgt) + + self.check_as_log(client_creds, + server_policy=policy) + + def test_authn_policy_allowed_to_computer_deny_as_req(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket with an AS-REQ is denied. + self._fast_as_req( + client_creds, target_creds, mach_tgt, + expected_error=KDC_ERR_POLICY, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_as_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_to_user_allow_as_req(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + + # Create an authentication policy that applies to a user and explicitly + # allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=denied) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that obtaining a service ticket with an AS-REQ is allowed. + self._fast_as_req(client_creds, target_creds, mach_tgt) + + self.check_as_log(client_creds, + server_policy=policy) + + def test_authn_policy_allowed_to_user_deny_as_req(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + + # Create an authentication policy that applies to a user and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=allowed) + + # Create a user account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + spn='host/{account}') + + # Show that obtaining a service ticket with an AS-REQ is denied. + self._fast_as_req( + client_creds, target_creds, mach_tgt, + expected_error=KDC_ERR_POLICY, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_as_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_to_service_allow_as_req(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + + # Create an authentication policy that applies to a managed service and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that obtaining a service ticket with an AS-REQ is allowed. + self._fast_as_req(client_creds, target_creds, mach_tgt) + + self.check_as_log(client_creds, + server_policy=policy) + + def test_authn_policy_allowed_to_service_deny_as_req(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + + # Create an authentication policy that applies to a managed service and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that obtaining a service ticket with an AS-REQ is denied. + self._fast_as_req( + client_creds, target_creds, mach_tgt, + expected_error=KDC_ERR_POLICY, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_as_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_allowed_to_computer_allow_as_req_no_fast(self): + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket with an AS-REQ is allowed. + self._as_req(client_creds, 0, target_creds, + etype=(kcrypto.Enctype.AES256,)) + + self.check_as_log(client_creds, + server_policy=policy) + + def test_authn_policy_allowed_to_computer_deny_as_req_no_fast(self): + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that obtaining a service ticket with an AS-REQ is denied. + self._as_req(client_creds, KDC_ERR_POLICY, target_creds, + etype=(kcrypto.Enctype.AES256,)) + + self.check_as_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_authn_policy_ntlm_allow_user(self): + # Create an authentication policy allowing NTLM authentication for + # users. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=True, + user_allowed_from=allowed, + service_allowed_ntlm=False, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that NTLM authentication succeeds. + self._connect(client_creds, simple_bind=False) + + self.check_ntlm_log(client_creds, + client_policy=policy) + + def test_authn_policy_ntlm_deny_user(self): + # Create an authentication policy denying NTLM authentication for + # users. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + user_allowed_from=allowed, + service_allowed_ntlm=True, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that NTLM authentication fails. + self._connect(client_creds, simple_bind=False, + expect_error=f'{HRES_SEC_E_LOGON_DENIED:08X}') + + self.check_ntlm_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_ntlm_computer(self): + # Create an authentication policy denying NTLM authentication. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + user_allowed_from=denied, + service_allowed_ntlm=False, + service_allowed_from=denied) + + # Create a computer account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy, + ntlm=True) + + # Show that NTLM authentication succeeds. + self._connect(client_creds, simple_bind=False) + + self.check_ntlm_log( + client_creds, + client_policy=None) # Client policies don’t apply to computers. + + def test_authn_policy_ntlm_allow_service(self): + # Create an authentication policy allowing NTLM authentication for + # services. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + user_allowed_from=allowed, + service_allowed_ntlm=True, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True) + + # Show that NTLM authentication succeeds. + self._connect(client_creds, simple_bind=False) + + self.check_ntlm_log(client_creds, + client_policy=policy) + + def test_authn_policy_ntlm_deny_service(self): + # Create an authentication policy denying NTLM authentication for + # services. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=True, + user_allowed_from=allowed, + service_allowed_ntlm=False, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True) + + # Show that NTLM authentication fails. + self._connect(client_creds, simple_bind=False, + expect_error=f'{HRES_SEC_E_LOGON_DENIED:08X}') + + self.check_ntlm_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_ntlm_deny_no_device_restrictions(self): + # Create an authentication policy denying NTLM authentication for + # users. + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + service_allowed_ntlm=True) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that without AllowedToAuthenticateFrom set in the policy, NTLM + # authentication succeeds. + self._connect(client_creds, simple_bind=False) + + self.check_ntlm_log(client_creds, + client_policy=policy) + + def test_authn_policy_simple_bind_allow_user(self): + # Create an authentication policy allowing NTLM authentication for + # users. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=True, + user_allowed_from=allowed, + service_allowed_ntlm=False, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that a simple bind succeeds. + self._connect(client_creds, simple_bind=True) + + self.check_simple_bind_log(client_creds, + client_policy=policy) + + def test_authn_policy_simple_bind_deny_user(self): + # Create an authentication policy denying NTLM authentication for + # users. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + user_allowed_from=allowed, + service_allowed_ntlm=True, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that a simple bind fails. + self._connect(client_creds, simple_bind=True, + expect_error=f'{HRES_SEC_E_INVALID_TOKEN:08X}') + + self.check_simple_bind_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_simple_bind_deny_no_device_restrictions(self): + # Create an authentication policy denying NTLM authentication for + # users. + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + service_allowed_ntlm=True) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that without AllowedToAuthenticateFrom set in the policy, a + # simple bind succeeds. + self._connect(client_creds, simple_bind=True) + + self.check_simple_bind_log(client_creds, + client_policy=policy) + + def test_authn_policy_samr_pwd_change_allow_service_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # managed service accounts. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True) + + # Show that a SAMR password change is allowed. + self._test_samr_change_password(client_creds, expect_error=None) + + self.check_samr_pwd_change_log(client_creds, + client_policy=policy) + + def test_authn_policy_samr_pwd_change_allow_service_not_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # managed service accounts. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True, + service_allowed_from=denied) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True) + + # Show that a SAMR password change is allowed. + self._test_samr_change_password(client_creds, expect_error=None) + + self.check_samr_pwd_change_log(client_creds, + client_policy=policy) + + def test_authn_policy_samr_pwd_change_allow_service_no_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # managed service accounts. + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True) + + # Show that a SAMR password change is allowed. + self._test_samr_change_password(client_creds, expect_error=None) + + self.check_samr_pwd_change_log(client_creds, + client_policy=policy) + + def test_authn_policy_samr_pwd_change_deny_service_allowed_from(self): + # Create an authentication policy denying NTLM authentication for + # managed service accounts. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True) + + # Show that the SAMR connection fails. + self._test_samr_change_password( + client_creds, expect_error=None, + connect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samr_pwd_change_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samr_pwd_change_deny_service_not_allowed_from(self): + # Create an authentication policy denying NTLM authentication for + # managed service accounts. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False, + service_allowed_from=denied) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True) + + # Show that the SAMR connection fails. + self._test_samr_change_password( + client_creds, expect_error=None, + connect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samr_pwd_change_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samr_pwd_change_deny_service_no_allowed_from(self): + # Create an authentication policy denying NTLM authentication for + # managed service accounts. + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True) + + # Show that a SAMR password change is allowed. + self._test_samr_change_password(client_creds, expect_error=None) + + self.check_samr_pwd_change_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_allow_user(self): + # Create an authentication policy allowing NTLM authentication for + # users. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=True, + user_allowed_from=allowed, + service_allowed_ntlm=False, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy) + + # Show that an interactive SamLogon succeeds. Although MS-APDS doesn’t + # state it, AllowedNTLMNetworkAuthentication applies to interactive + # logons too. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_deny_user(self): + # Create an authentication policy denying NTLM authentication for + # users. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + user_allowed_from=allowed, + service_allowed_ntlm=True, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_interactive_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samlogon_network_computer(self): + # Create an authentication policy denying NTLM authentication. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + user_allowed_from=denied, + service_allowed_ntlm=False, + service_allowed_from=denied) + + # Create a computer account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy, + ntlm=True) + + # Show that a network SamLogon succeeds. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log( + client_creds, + client_policy=None) # Client policies don’t apply to computers. + + def test_authn_policy_samlogon_interactive_allow_user_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # users. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that an interactive SamLogon succeeds. Although MS-APDS doesn’t + # state it, AllowedNTLMNetworkAuthentication applies to interactive + # logons too. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_interactive_allow_user_not_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # users. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=True, + user_allowed_from=denied) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that an interactive SamLogon succeeds. Although MS-APDS doesn’t + # state it, AllowedNTLMNetworkAuthentication applies to interactive + # logons too. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_interactive_allow_user_no_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # users. + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=True) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that an interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_interactive_deny_user_allowed_from(self): + # Create an authentication policy disallowing NTLM authentication for + # users. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_interactive_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samlogon_interactive_deny_user_not_allowed_from(self): + # Create an authentication policy disallowing NTLM authentication for + # users. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + user_allowed_from=denied) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_interactive_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samlogon_interactive_deny_user_no_allowed_from(self): + # Create an authentication policy disallowing NTLM authentication for + # users. + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that an interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_interactive_user_allowed_from(self): + # Create an authentication policy not specifying whether NTLM + # authentication is allowed or not. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_interactive_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samlogon_network_user_allowed_from(self): + # Create an authentication policy not specifying whether NTLM + # authentication is allowed or not. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samlogon_network_allow_service_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # services. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_network_allow_service_not_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # services. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True, + service_allowed_from=denied) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_network_allow_service_no_allowed_from(self): + # Create an authentication policy allowing NTLM authentication for + # services. + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_network_deny_service_allowed_from(self): + # Create an authentication policy disallowing NTLM authentication for + # services. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samlogon_network_deny_service_not_allowed_from(self): + # Create an authentication policy disallowing NTLM authentication for + # services. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False, + service_allowed_from=denied) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samlogon_network_deny_service_no_allowed_from(self): + # Create an authentication policy disallowing NTLM authentication for + # services. + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_network_allow_service_allowed_from_to_self(self): + # Create an authentication policy allowing NTLM authentication for + # services. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon to ourselves succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy, + server_policy=policy) + + def test_authn_policy_samlogon_network_allow_service_not_allowed_from_to_self(self): + # Create an authentication policy allowing NTLM authentication for + # services. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True, + service_allowed_from=denied) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon to ourselves succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy, + server_policy=policy) + + def test_authn_policy_samlogon_network_allow_service_no_allowed_from_to_self(self): + # Create an authentication policy allowing NTLM authentication for + # services. + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=True) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon to ourselves succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy, + server_policy=policy) + + def test_authn_policy_samlogon_network_deny_service_allowed_from_to_self(self): + # Create an authentication policy disallowing NTLM authentication for + # services. + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False, + service_allowed_from=allowed) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon to ourselves fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + server_policy=None, # Only the client policy appears in the logs. + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_authn_policy_samlogon_network_deny_service_not_allowed_from_to_self(self): + # Create an authentication policy disallowing NTLM authentication for + # services. + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False, + service_allowed_from=denied) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon to ourselves fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION, + server_policy=None) # Only the client policy appears in the logs. + + def test_authn_policy_samlogon_network_deny_service_no_allowed_from_to_self(self): + # Create an authentication policy disallowing NTLM authentication for + # services. + policy = self.create_authn_policy(enforced=True, + service_allowed_ntlm=False) + + # Create a managed service account with the assigned policy. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Show that a network SamLogon to ourselves succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy, + server_policy=policy) + + def test_authn_policy_samlogon_interactive_deny_no_device_restrictions(self): + # Create an authentication policy denying NTLM authentication for + # users. + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + service_allowed_ntlm=True) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that without AllowedToAuthenticateFrom set in the policy, an + # interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + client_policy=policy) + + def test_authn_policy_samlogon_network_deny_no_device_restrictions(self): + # Create an authentication policy denying NTLM authentication for + # users. + policy = self.create_authn_policy(enforced=True, + user_allowed_ntlm=False, + service_allowed_ntlm=True) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True) + + # Show that without AllowedToAuthenticateFrom set in the policy, a + # network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy) + + def test_samlogon_allowed_to_computer_allow(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + server_policy=policy) + + # Show that an interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + server_policy=policy) + + def test_samlogon_allowed_to_computer_deny(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_computer_deny_protected(self): + # Create a protected user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + protected=True, + ntlm=True) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + # The account’s protection takes precedence, and no policy appears + # in the log. + server_policy=None, + status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_interactive_log( + client_creds, + # The account’s protection takes precedence, and no policy appears + # in the log. + server_policy=None, + status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + def test_samlogon_allowed_to_computer_allow_asserted_identity(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that allows accounts with the + # Authentication Authority Asserted Identity SID to obtain a service + # ticket. + allowed = ( + f'O:SYD:(A;;CR;;;' + f'{security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY})' + ) + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_computer_allow_claims_valid(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that allows accounts with the Claims + # Valid SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_CLAIMS_VALID})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_computer_allow_compounded_auth(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that allows accounts with the + # Compounded Authentication SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_COMPOUNDED_AUTHENTICATION})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_computer_allow_authenticated_users(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that allows accounts with the + # Authenticated Users SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_AUTHENTICATED_USERS})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + server_policy=policy) + + # Show that an interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + server_policy=policy) + + def test_samlogon_allowed_to_computer_allow_ntlm_authn(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that allows accounts with the NTLM + # Authentication SID to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{security.SID_NT_NTLM_AUTHENTICATION})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_no_owner(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. Omit + # the owner (O:SY) from the SDDL. + allowed = f'D:(A;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_INVALID_PARAMETER) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_INVALID_PARAMETER, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.DESCRIPTOR_NO_OWNER) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_INVALID_PARAMETER) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_INVALID_PARAMETER, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.DESCRIPTOR_NO_OWNER) + + def test_samlogon_allowed_to_no_owner_unenforced(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an unenforced authentication policy that applies to a computer + # and explicitly allows the user account to obtain a service + # ticket. Omit the owner (O:SY) from the SDDL. + allowed = f'D:(A;;CR;;;{client_creds.get_sid()})' + policy = self.create_authn_policy(enforced=False, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_INVALID_PARAMETER, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.DESCRIPTOR_NO_OWNER) + + # Show that an interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_INVALID_PARAMETER, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.DESCRIPTOR_NO_OWNER) + + def test_samlogon_allowed_to_service_allow(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that applies to a managed service and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + server_policy=policy) + + # Show that an interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + server_policy=policy) + + def test_samlogon_allowed_to_service_deny(self): + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that applies to a managed service and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_computer_allow_group_not_a_member(self): + samdb = self.get_samdb() + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account which does not belong to the group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon fails, as the user account does not + # belong to the group. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Show that an interactive SamLogon fails, as the user account does not + # belong to the group. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + client_creds, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_computer_allow_group_member(self): + samdb = self.get_samdb() + + # Create a new group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account that belongs to the group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + member_of=group_dn, + ntlm=True) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon succeeds, since the user account belongs + # to the group. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + server_policy=policy) + + # Show that an interactive SamLogon succeeds, since the user account + # belongs to the group. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + server_policy=policy) + + def test_samlogon_allowed_to_computer_allow_domain_local_group(self): + samdb = self.get_samdb() + + # Create a new domain-local group. + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name, + gtype=GroupType.DOMAIN_LOCAL.value) + group_sid = self.get_objectSid(samdb, group_dn) + + # Create a user account that belongs to the group. + client_creds = self._get_creds(account_type=self.AccountType.USER, + member_of=group_dn, + ntlm=True) + + # Create an authentication policy that allows accounts belonging to the + # group. + allowed = f'O:SYD:(A;;CR;;;{group_sid})' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Show that a network SamLogon succeeds, since the user account belongs + # to the group. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + server_policy=policy) + + # Show that an interactive SamLogon succeeds, since the user account + # belongs to the group. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + server_policy=policy) + + def test_samlogon_allowed_to_computer_allow_to_self(self): + samdb = self.get_samdb() + + # Create a computer account. + client_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + ntlm=True, + cached=False) + client_dn = client_creds.get_dn() + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Assign the policy to the account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that a network SamLogon to ourselves succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log( + client_creds, + client_policy=None, # Client policies don’t apply to computers. + server_policy=policy) + + def test_samlogon_allowed_to_computer_deny_to_self(self): + samdb = self.get_samdb() + + # Create a computer account. + client_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + ntlm=True, + cached=False) + client_dn = client_creds.get_dn() + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Assign the policy to the account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that a network SamLogon to ourselves fails, despite + # authentication being allowed in the Kerberos case. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + client_policy=None, # Client policies don’t apply to computers. + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_service_allow_to_self(self): + samdb = self.get_samdb() + + # Create a managed service account. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + ntlm=True, + cached=False) + client_dn = client_creds.get_dn() + + # Create an authentication policy that applies to a managed service and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Assign the policy to the account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that a network SamLogon to ourselves succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + client_policy=policy, + server_policy=policy) + + def test_samlogon_allowed_to_service_deny_to_self(self): + samdb = self.get_samdb() + + # Create a managed service account. + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + ntlm=True, + cached=False) + client_dn = client_creds.get_dn() + + # Create an authentication policy that applies to a managed service and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Assign the policy to the account. + self.add_attribute(samdb, str(client_dn), + 'msDS-AssignedAuthNPolicy', str(policy.dn)) + + # Show that a network SamLogon to ourselves fails, despite + # authentication being allowed in the Kerberos case. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_computer_derived_class_allow(self): + samdb = self.get_samdb() + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=allowed, + service_allowed_to=denied) + + # Create a schema class derived from ‘computer’. + class_id = random.randint(0, 100000000) + computer_class_cn = f'my-Computer-Class-{class_id}' + computer_class = computer_class_cn.replace('-', '') + class_dn = samdb.get_schema_basedn() + class_dn.add_child(f'CN={computer_class_cn}') + governs_id = f'1.3.6.1.4.1.7165.4.6.2.9.{class_id}' + + samdb.add({ + 'dn': class_dn, + 'objectClass': 'classSchema', + 'subClassOf': 'computer', + 'governsId': governs_id, + 'lDAPDisplayName': computer_class, + }) + + # Create an account derived from ‘computer’ with the assigned policy. + target_name = self.get_new_username() + target_creds, target_dn = self.create_account( + samdb, target_name, + account_type=self.AccountType.COMPUTER, + spn=f'host/{target_name}', + additional_details={ + 'msDS-AssignedAuthNPolicy': str(policy.dn), + 'objectClass': computer_class, + }) + + keys = self.get_keys(target_creds) + self.creds_set_keys(target_creds, keys) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + server_policy=policy) + + # Show that an interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + server_policy=policy) + + def test_samlogon_allowed_to_service_derived_class_allow(self): + samdb = self.get_samdb() + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True) + + # Create an authentication policy that applies to a managed service and + # explicitly allows the user account to obtain a service ticket. + allowed = f'O:SYD:(A;;CR;;;{client_creds.get_sid()})' + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=denied, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a schema class derived from ‘msDS-ManagedServiceAccount’. + class_id = random.randint(0, 100000000) + service_class_cn = f'my-Managed-Service-Class-{class_id}' + service_class = service_class_cn.replace('-', '') + class_dn = samdb.get_schema_basedn() + class_dn.add_child(f'CN={service_class_cn}') + governs_id = f'1.3.6.1.4.1.7165.4.6.2.9.{class_id}' + + samdb.add({ + 'dn': class_dn, + 'objectClass': 'classSchema', + 'subClassOf': 'msDS-ManagedServiceAccount', + 'governsId': governs_id, + 'lDAPDisplayName': service_class, + }) + + # Create an account derived from ‘msDS-ManagedServiceAccount’ with the + # assigned policy. + target_name = self.get_new_username() + target_creds, target_dn = self.create_account( + samdb, target_name, + account_type=self.AccountType.MANAGED_SERVICE, + spn=f'host/{target_name}', + additional_details={ + 'msDS-AssignedAuthNPolicy': str(policy.dn), + 'objectClass': service_class, + }) + + # Show that a network SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(client_creds, + server_policy=policy) + + # Show that an interactive SamLogon succeeds. + self._test_samlogon(creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(client_creds, + server_policy=policy) + + def test_samlogon_bad_pwd_client_policy(self): + # Create an authentication policy with device restrictions for users. + allowed = 'O:SY' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. Use a non-cached + # account so that it is not locked out for other tests. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Set a wrong password. + client_creds.set_password('wrong password') + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_interactive_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + def test_samlogon_bad_pwd_server_policy(self): + # Create a user account. Use a non-cached account so that it is not + # locked out for other tests. + client_creds = self._get_creds(account_type=self.AccountType.USER, + ntlm=True, + cached=False) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Set a wrong password. + client_creds.set_password('wrong password') + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_WRONG_PASSWORD) + + self.check_samlogon_network_log( + client_creds, + # The bad password failure takes precedence, and no policy appears + # in the log. + server_policy=None, + status=ntstatus.NT_STATUS_WRONG_PASSWORD) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_WRONG_PASSWORD) + + self.check_samlogon_interactive_log( + client_creds, + # The bad password failure takes precedence, and no policy appears + # in the log. + server_policy=None, + status=ntstatus.NT_STATUS_WRONG_PASSWORD) + + def test_samlogon_bad_pwd_client_and_server_policy(self): + # Create an authentication policy with device restrictions for users. + allowed = 'O:SY' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed) + + # Create a user account with the assigned policy. Use a non-cached + # account so that it is not locked out for other tests. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy, + ntlm=True, + cached=False) + + # Create an authentication policy that applies to a computer and + # explicitly denies the user account to obtain a service ticket. + denied = f'O:SYD:(D;;CR;;;{client_creds.get_sid()})' + allowed = 'O:SYD:(A;;CR;;;WD)' + server_policy = self.create_authn_policy(enforced=True, + user_allowed_to=allowed, + computer_allowed_to=denied, + service_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=server_policy) + + # Set a wrong password. + client_creds.set_password('wrong password') + + # Show that a network SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_network_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + # Show that an interactive SamLogon fails. + self._test_samlogon( + creds=client_creds, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + self.check_samlogon_interactive_log( + client_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + event=AuditEvent.NTLM_DEVICE_RESTRICTION) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/claims_in_pac.py b/python/samba/tests/krb5/claims_in_pac.py new file mode 100755 index 0000000..a5db7ba --- /dev/null +++ b/python/samba/tests/krb5/claims_in_pac.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Andrew Bartlett 2023 +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from samba.dcerpc import krb5pac, claims +from samba.ndr import ndr_pack, ndr_unpack +from samba.tests import TestCase + +class PacClaimsTests(TestCase): + + pac_data_uncompressed = bytes.fromhex( + '08000000000000000100000000020000880000000000000006000000100000008802' + '000000000000070000001000000098020000000000000a00000020000000a8020000' + '000000000c000000b8000000c8020000000000000d00000090010000800300000000' + '000011000000080000001005000000000000120000001c0000001805000000000000' + '01100800ccccccccf001000000000000000002000000000000000000ffffffffffff' + 'ff7fffffffffffffff7f6ebd4f913c60d9016e7db9bb0561d9016e3da9863d81d901' + '16001600040002000000000008000200000000000c00020000000000100002000000' + '000014000200000000001800020000000000ae04000001020000010000001c000200' + '20000000000000000000000000000000000000001e002000200002000a000c002400' + '02002800020000000000000000001000000000000000000000000000000000000000' + '000000000000000000000000020000002c0002000000000000000000000000000b00' + '0000000000000b000000370032003000660064003300630033005f00310030000000' + '00000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000100000001020000' + '0700000010000000000000000f000000410042004100520054004c00450054002d00' + '440043002d00570049004e000000060000000000000005000000570049004e003200' + '32000000040000000104000000000005150000003f1ba8749a54499be10ea4590200' + '00003000020007000000340002000700000005000000010500000000000515000000' + '000000000000000000000000f1010000010000000101000000000012010000000000' + '000010000000d89573aeb6f036c4ca5f5412100000008ada43082e7dfccb7587a478' + '8097ee903c60d9011600370032003000660064003300630033005f00310030003a00' + '18002200580003000000160080001c00980000000000370032003000660064003300' + '630033005f00310030004000770069006e00320032002e006500780061006d007000' + '6c0065002e0063006f006d00000000000000570049004e00320032002e0045005800' + '41004d0050004c0045002e0043004f004d0000000000000037003200300066006400' + '3300630033005f003100300000000105000000000005150000003f1ba8749a54499b' + 'e10ea459ae0400000000000001100800cccccccc8001000000000000000002005801' + '00000400020000000000580100000000000000000000000000005801000001100800' + 'cccccccc480100000000000000000200010000000400020000000000000000000000' + '000001000000010000000300000008000200030000000c0002000600060001000000' + '10000200140002000300030003000000180002002800020002000200040000002c00' + '02000b000000000000000b000000370032003000660064003300630033005f003900' + '00000000010000000000000001000000000000000b000000000000000b0000003700' + '32003000660064003300630033005f00370000000000030000001c00020020000200' + '2400020004000000000000000400000066006f006f00000004000000000000000400' + '00006200610072000000040000000000000004000000620061007a0000000b000000' + '000000000b000000370032003000660064003300630033005f003800000000000400' + '000009000a0000000000070001000000000006000100000000000000010000000000' + '0000000002000000010000000105000000000005150000003f1ba8749a54499be10e' + 'a459ae04000000000000' + ) + + pac_data_compressed = bytes.fromhex( + '080000000000000001000000f8010000880000000000000006000000100000008002' + '000000000000070000001000000090020000000000000a0000001e000000a0020000' + '000000000c000000b0000000c0020000000000000d00000060020000700300000000' + '00001100000008000000d005000000000000120000001c000000d805000000000000' + '01100800cccccccce801000000000000000002000000000000000000ffffffffffff' + 'ff7fffffffffffffff7f50b330913c60d90150739abb0561d90150338a863d81d901' + '14001400040002000000000008000200000000000c00020000000000100002000000' + '000014000200000000001800020000000000ad04000001020000010000001c000200' + '20000000000000000000000000000000000000001e002000200002000a000c002400' + '02002800020000000000000000001000000000000000000000000000000000000000' + '000000000000000000000000020000002c0002000000000000000000000000000a00' + '0000000000000a000000370032003000660064003300630033005f00360000000000' + '00000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000010000000102000007000000' + '10000000000000000f000000410042004100520054004c00450054002d0044004300' + '2d00570049004e000000060000000000000005000000570049004e00320032000000' + '040000000104000000000005150000003f1ba8749a54499be10ea459020000003000' + '02000700000034000200070000000500000001050000000000051500000000000000' + '0000000000000000f10100000100000001010000000000120100000010000000ace7' + 'b599ff30aa486b52983210000000b50e9bea014545c97eca0b978097ee903c60d901' + '1400370032003000660064003300630033005f003600000038001800220050000300' + '0000140078001c00900000000000370032003000660064003300630033005f003600' + '4000770069006e00320032002e006500780061006d0070006c0065002e0063006f00' + '6d00570049004e00320032002e004500580041004d0050004c0045002e0043004f00' + '4d00000000000000370032003000660064003300630033005f003600000000000105' + '000000000005150000003f1ba8749a54499be10ea459ad0400000000000001100800' + 'cccccccc500200000000000000000200290200000400020004000000282000000000' + '00000000000000000000290200007377878887880888070008000780080006000700' + '07000708877707800800880088700700080008080000800000000080707877877700' + '76770867868788000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000800000000000000000000000000000000000000' + '00000000000077000800800000008700000000000000850700000000000074768000' + '00000000750587000800000066078000000080706677880080008060878708000000' + '00800080000000000080000000000000000000000000000000000000000000000000' + '6080080000000070000000000000000000000000000000000000000000000000fd74' + 'eaf001add6213aecf4346587eec48c323e3e1a5a32042eecf243669a581e383d2940' + 'e80e383c294463b8c0b49024f1def20df819586b086cd2ab98700923386674845663' + 'ef57e91718110c1ad4c0ac88912126d2180545e98670ea2aa002052aa54189cc318d' + '26c46b667f18b6876262a9a4985ecdf76e5161033fd457ba020075360c837aaa3aa8' + '2749ee8152420999b553c60195be5e5c35c4330557538772972a7d527aeca1fc6b29' + '51ca254ac83960272a930f3194892d4729eff48e48ccfb929329ff501c356c0e8ed1' + '8471ec70986c31da86a8090b4022c1db257514fdba4347532146648d4f99f9065e0d' + '9a0d90d80f38389c39cb9ebe6d4e5e681e5a8a5418f591f1dbb7594a3f2aa3220ced' + '1cd18cb49cffcc2ff18eef6caf443663640c56640000120000000200000001000000' + '0105000000000005150000003f1ba8749a54499be10ea459ad04000000000000' + ) + + pac_data_int64_claim = bytes.fromhex( + '080000000000000001000000f0010000880000000000000006000000100000007802' + '000000000000070000001000000088020000000000000a0000001a00000098020000' + '000000000c00000088000000b8020000000000000d000000d0000000400300000000' + '000011000000080000001004000000000000120000001c0000001804000000000000' + '01100800cccccccce001000000000000000002000000000000000000ffffffffffff' + 'ff7fffffffffffffff7f52a2a6d607cfd90152621001d1cfd901522200cc08f0d901' + '10001000040002000000000008000200000000000c00020000000000100002000000' + '0000140002000000000018000200000000004362000001020000010000001c000200' + '200000000000000000000000000000000000000014001600200002000e0010002400' + '02002800020000000000000000001000000000000000000000000000000000000000' + '000000000000000000000000020000002c0002000000000000000000000000000800' + '0000000000000800000075007300650072006e0061006d0065000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000' + '0000000000000000000000000000000000000100000001020000070000000b000000' + '000000000a000000570049004e0032004b00310039002d0044004300080000000000' + '0000070000006500780061006d0070006c0065000000040000000104000000000005' + '15000000bcfb8bf5af39e9b21f9b5fcd020000003000020007000000340002000700' + '000005000000010500000000000515000000000000000000000000000000f1010000' + '010000000101000000000012010000000000000010000000147a8762afe3366b316c' + '936410000000e05a433ae9271bcc603d933480353ad607cfd9011000750073006500' + '72006e0061006d006500000000000000280018001600400003000000100058001c00' + '68000000000075007300650072006e0061006d00650040006500780061006d007000' + '6c0065002e0063006f006d004500580041004d0050004c0045002e0043004f004d00' + '000075007300650072006e0061006d006500010500000000000515000000bcfb8bf5' + 'af39e9b21f9b5fcd436200000000000001100800ccccccccc0000000000000000000' + '02009800000004000200000000009800000000000000000000000000000098000000' + '01100800cccccccc8800000000000000000002000100000004000200000000000000' + '00000000000001000000010000000100000008000200010000000c00020001000100' + '05000000100002000800000000000000080000006100200063006c00610069006d00' + '0000050000000000000003000000000000002a0000000000000019fcffffffffffff' + 'e803000000000000204e000000000000000000000200000001000000010500000000' + '000515000000bcfb8bf5af39e9b21f9b5fcd4362000000000000' + ) + + def test_unpack_raw(self): + pac_unpacked_raw = ndr_unpack(krb5pac.PAC_DATA_RAW, self.pac_data_uncompressed) + self.assertEqual(pac_unpacked_raw.num_buffers, 8) + self.assertEqual(pac_unpacked_raw.version, 0) + + def confirm_uncompressed_claims(self, claim_metadata): + self.assertEqual(claim_metadata.uncompressed_claims_set_size, + 344) + claims_set = claim_metadata.claims_set.claims.claims + self.assertEqual(claims_set.claims_array_count, + 1) + claim_arrays = claims_set.claims_arrays + self.assertEqual(claim_arrays[0].claims_source_type, + claims.CLAIMS_SOURCE_TYPE_AD) + self.assertEqual(claim_arrays[0].claims_count, + 3) + claim_entries = claim_arrays[0].claim_entries + self.assertEqual(claim_entries[0].id, + '720fd3c3_9') + self.assertEqual(claim_entries[0].type, + claims.CLAIM_TYPE_BOOLEAN) + self.assertEqual(claim_entries[0].values.value_count, + 1) + self.assertEqual(claim_entries[0].values.values[0], + 1) + + self.assertEqual(claim_entries[1].id, + '720fd3c3_7') + self.assertEqual(claim_entries[1].type, + claims.CLAIM_TYPE_STRING) + self.assertEqual(claim_entries[1].values.value_count, + 3) + self.assertEqual(claim_entries[1].values.values[0], + "foo") + self.assertEqual(claim_entries[1].values.values[1], + "bar") + self.assertEqual(claim_entries[1].values.values[2], + "baz") + + self.assertEqual(claim_entries[2].id, + '720fd3c3_8') + self.assertEqual(claim_entries[2].type, + claims.CLAIM_TYPE_UINT64) + self.assertEqual(claim_entries[2].values.value_count, + 4) + self.assertEqual(claim_entries[2].values.values[0], + 655369) + self.assertEqual(claim_entries[2].values.values[1], + 65543) + self.assertEqual(claim_entries[2].values.values[2], + 65542) + self.assertEqual(claim_entries[2].values.values[3], + 65536) + + def test_unpack_claims_pac_uncompressed(self): + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_uncompressed) + + self.assertEqual(pac.num_buffers, 8) + self.assertEqual(pac.version, 0) + self.assertEqual(pac.buffers[0].type, krb5pac.PAC_TYPE_LOGON_INFO) + self.assertEqual(pac.buffers[0].info.info.info3.base.account_name.string, "720fd3c3_10") + + self.assertEqual(pac.buffers[5].type, krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO) + self.assertIsNotNone(pac.buffers[5].info.remaining) + + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, pac.buffers[5].info.remaining) + claim_metadata = client_claims.claims.metadata + + self.assertEqual(pac.buffers[6].type, krb5pac.PAC_TYPE_ATTRIBUTES_INFO) + self.assertEqual(pac.buffers[7].type, krb5pac.PAC_TYPE_REQUESTER_SID) + + self.assertEqual(claim_metadata.compression_format, + claims.CLAIMS_COMPRESSION_FORMAT_NONE) + self.confirm_uncompressed_claims(claim_metadata) + + def confirm_compressed_claims(self, claim_metadata): + self.assertEqual(claim_metadata.uncompressed_claims_set_size, + 8232) + claims_set = claim_metadata.claims_set.claims.claims + self.assertEqual(claims_set.claims_array_count, + 1) + claim_arrays = claims_set.claims_arrays + self.assertEqual(claim_arrays[0].claims_source_type, + claims.CLAIMS_SOURCE_TYPE_AD) + self.assertEqual(claim_arrays[0].claims_count, + 5) + claim_entries = claim_arrays[0].claim_entries + self.assertEqual(claim_entries[0].id, + '720fd3c3_4') + self.assertEqual(claim_entries[0].type, + claims.CLAIM_TYPE_BOOLEAN) + self.assertEqual(claim_entries[0].values.value_count, + 1) + self.assertEqual(claim_entries[0].values.values[0], + 1) + + self.assertEqual(claim_entries[1].id, + '720fd3c3_0') + self.assertEqual(claim_entries[1].type, + claims.CLAIM_TYPE_STRING) + self.assertEqual(claim_entries[1].values.value_count, + 4) + self.assertEqual(claim_entries[1].values.values[0], + "A first value.") + self.assertEqual(claim_entries[1].values.values[1], + "A second value.") + self.assertEqual(claim_entries[1].values.values[2], + "A third value.") + + self.assertEqual(claim_entries[2].id, + '720fd3c3_1') + self.assertEqual(claim_entries[2].type, + claims.CLAIM_TYPE_STRING) + self.assertEqual(claim_entries[2].values.value_count, + 3) + self.assertEqual(claim_entries[2].values.values[0], + "DC=win22,DC=example,DC=com") + self.assertEqual(claim_entries[2].values.values[1], + "CN=Users,DC=win22,DC=example,DC=com") + self.assertEqual(claim_entries[2].values.values[2], + "CN=Computers,DC=win22,DC=example,DC=com") + + self.assertEqual(claim_entries[3].id, + '720fd3c3_2') + self.assertEqual(claim_entries[3].type, + claims.CLAIM_TYPE_UINT64) + self.assertEqual(claim_entries[3].values.value_count, + 4) + self.assertEqual(claim_entries[3].values.values[0], + 655369) + self.assertEqual(claim_entries[3].values.values[1], + 65543) + self.assertEqual(claim_entries[3].values.values[2], + 65542) + self.assertEqual(claim_entries[3].values.values[3], + 65536) + + def test_unpack_claims_pac_compressed(self): + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_compressed) + + self.assertEqual(pac.num_buffers, 8) + self.assertEqual(pac.version, 0) + self.assertEqual(pac.buffers[0].type, krb5pac.PAC_TYPE_LOGON_INFO) + self.assertEqual(pac.buffers[0].info.info.info3.base.account_name.string, "720fd3c3_6") + + self.assertEqual(pac.buffers[5].type, krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO) + self.assertIsNotNone(pac.buffers[5].info.remaining) + + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, pac.buffers[5].info.remaining) + claim_metadata = client_claims.claims.metadata + + self.assertEqual(pac.buffers[6].type, krb5pac.PAC_TYPE_ATTRIBUTES_INFO) + self.assertEqual(pac.buffers[7].type, krb5pac.PAC_TYPE_REQUESTER_SID) + + self.assertEqual(claim_metadata.compression_format, + claims.CLAIMS_COMPRESSION_FORMAT_XPRESS_HUFF) + self.assertEqual(claim_metadata.claims_set_size, + 553) + self.confirm_compressed_claims(claim_metadata) + + def test_repack_claims_pac_uncompressed(self): + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_uncompressed) + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, pac.buffers[5].info.remaining) + client_claims_bytes1 = ndr_pack(client_claims) + client_claims2 = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, client_claims_bytes1) + client_claims_bytes2 = ndr_pack(client_claims2) + self.assertEqual(client_claims_bytes1, client_claims_bytes2) + + claim_metadata = client_claims2.claims.metadata + self.assertEqual(claim_metadata.compression_format, + claims.CLAIMS_COMPRESSION_FORMAT_NONE) + self.confirm_uncompressed_claims(claim_metadata) + + def test_repack_claims_pac_compressed(self): + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_compressed) + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, pac.buffers[5].info.remaining) + client_claims_bytes1 = ndr_pack(client_claims) + client_claims2 = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, client_claims_bytes1) + client_claims_bytes2 = ndr_pack(client_claims2) + self.assertEqual(client_claims_bytes1, client_claims_bytes2) + + # This confirms that after compression and decompression, we + # still get the values we expect + claim_metadata = client_claims2.claims.metadata + self.assertEqual(claim_metadata.compression_format, + claims.CLAIMS_COMPRESSION_FORMAT_XPRESS_HUFF) + self.assertEqual(claim_metadata.claims_set_size, + 585) + self.confirm_compressed_claims(claim_metadata) + + def test_repack_claims_pac_uncompressed_set_compressed(self): + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_uncompressed) + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, pac.buffers[5].info.remaining) + client_claims.claims.metadata.compression_format = claims.CLAIMS_COMPRESSION_FORMAT_XPRESS_HUFF + client_claims_bytes1 = ndr_pack(client_claims) + client_claims2 = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, client_claims_bytes1) + + # Confirm that despite setting FORMAT_XPRESS_HUFF compression is never attempted + self.assertEqual(client_claims2.claims.metadata.uncompressed_claims_set_size, + 344) + self.assertEqual(client_claims2.claims.metadata.claims_set_size, + 344) + self.assertEqual(client_claims2.claims.metadata.compression_format, + claims.CLAIMS_COMPRESSION_FORMAT_NONE) + + # Confirm we match the originally uncompressed sample + claim_metadata = client_claims2.claims.metadata + self.confirm_uncompressed_claims(claim_metadata) + + # Finally confirm a re-pack gets identical bytes + client_claims_bytes2 = ndr_pack(client_claims2) + self.assertEqual(client_claims_bytes1, client_claims_bytes2) + + + def test_repack_claims_pac_compressed_set_uncompressed(self): + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_compressed) + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, pac.buffers[5].info.remaining) + client_claims.claims.metadata.compression_format = claims.CLAIMS_COMPRESSION_FORMAT_NONE + client_claims_bytes1 = ndr_pack(client_claims) + client_claims2 = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, client_claims_bytes1) + + # Confirm that by setting FORMAT_NONE compression is never attempted + self.assertEqual(client_claims2.claims.metadata.uncompressed_claims_set_size, + 8232) + self.assertEqual(client_claims2.claims.metadata.claims_set_size, + 8232) + self.assertEqual(client_claims2.claims.metadata.compression_format, + claims.CLAIMS_COMPRESSION_FORMAT_NONE) + + # This confirms that after pack and unpack, despite being + # larger than the compression minimum we get add the data and + # the values we expect for the originally-compressed data + claim_metadata = client_claims2.claims.metadata + self.confirm_compressed_claims(claim_metadata) + + # Finally confirm a re-pack gets identical bytes + client_claims_bytes2 = ndr_pack(client_claims2) + self.assertEqual(client_claims_bytes1, client_claims_bytes2) + + def test_repack_claims_pac_uncompressed_uninit_lengths(self): + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_uncompressed) + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, pac.buffers[5].info.remaining) + # This matches what we expect the KDC to do, which is to ask for compression always + client_claims.claims.metadata.compression_format = claims.CLAIMS_COMPRESSION_FORMAT_XPRESS_HUFF + client_claims.claims.metadata.uncompressed_claims_set_size = 0 + client_claims.claims.metadata.claims_set_size = 0 + + client_claims_bytes1 = ndr_pack(client_claims) + client_claims2 = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, client_claims_bytes1) + + # Confirm that the NDR code did not compress and sent FORMAT_NONE on the wire + self.assertEqual(client_claims2.claims.metadata.uncompressed_claims_set_size, + 344) + self.assertEqual(client_claims2.claims.metadata.claims_set_size, + 344) + self.assertEqual(client_claims2.claims.metadata.compression_format, + claims.CLAIMS_COMPRESSION_FORMAT_NONE) + + claim_metadata = client_claims2.claims.metadata + self.confirm_uncompressed_claims(claim_metadata) + + # Finally confirm a re-pack gets identical bytes + client_claims_bytes2 = ndr_pack(client_claims2) + self.assertEqual(client_claims_bytes1, client_claims_bytes2) + + def test_repack_claims_pac_compressed_uninit_lengths(self): + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_compressed) + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, pac.buffers[5].info.remaining) + client_claims.claims.metadata.compression_format = claims.CLAIMS_COMPRESSION_FORMAT_XPRESS_HUFF + client_claims.claims.metadata.uncompressed_claims_set_size = 0 + client_claims.claims.metadata.claims_set_size = 0 + + client_claims_bytes1 = ndr_pack(client_claims) + client_claims2 = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, client_claims_bytes1) + + # Confirm that despite no lengths being set, the data is compressed correctly + self.assertEqual(client_claims2.claims.metadata.uncompressed_claims_set_size, + 8232) + self.assertEqual(client_claims2.claims.metadata.claims_set_size, + 585) + self.assertEqual(client_claims2.claims.metadata.compression_format, + claims.CLAIMS_COMPRESSION_FORMAT_XPRESS_HUFF) + + claim_metadata = client_claims2.claims.metadata + self.confirm_compressed_claims(claim_metadata) + + # Finally confirm a re-pack gets identical bytes + client_claims_bytes2 = ndr_pack(client_claims2) + self.assertEqual(client_claims_bytes1, client_claims_bytes2) + + def test_pac_int64_claims(self): + """Test that we can parse a PAC containing INT64 claims.""" + + # Decode the PAC. + pac = ndr_unpack(krb5pac.PAC_DATA, self.pac_data_int64_claim) + + # Get the PAC buffer which contains the client claims. + self.assertEqual(8, pac.num_buffers) + client_claims_buf = pac.buffers[5] + self.assertEqual(krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO, + client_claims_buf.type) + + # Ensure that we can decode the client claims. + client_claims_data = client_claims_buf.info.remaining + client_claims = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, + client_claims_data) + + claims_set = client_claims.claims.metadata.claims_set.claims.claims + + # We should find a single claims array, … + self.assertEqual(1, claims_set.claims_array_count) + claims_array = claims_set.claims_arrays[0] + self.assertEqual(claims.CLAIMS_SOURCE_TYPE_AD, + claims_array.claims_source_type) + + # …containing our INT64 claim. + self.assertEqual(1, claims_array.claims_count) + claim_entry = claims_array.claim_entries[0] + self.assertEqual('a claim', claim_entry.id) + self.assertEqual(claims.CLAIM_TYPE_INT64, claim_entry.type) + + # Ensure that the values have been decoded correctly. + self.assertEqual([3, 42, -999, 1000, 20000], claim_entry.values.values) + + # Re-encode the claims buffer and ensure that the result is identical + # to the original encoded claims produced by Windows. + client_claims_packed = ndr_pack(client_claims) + self.assertEqual(client_claims_data, client_claims_packed) + + +if __name__ == '__main__': + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/claims_tests.py b/python/samba/tests/krb5/claims_tests.py new file mode 100755 index 0000000..074147e --- /dev/null +++ b/python/samba/tests/krb5/claims_tests.py @@ -0,0 +1,2032 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd 2022 +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +import re +import ldb + +from samba.dcerpc import claims, krb5pac, security +from samba.ndr import ndr_pack + +from samba.tests import DynamicTestCase, env_get_var_value +from samba.tests.krb5 import kcrypto +from samba.tests.krb5.kcrypto import Enctype +from samba.tests.krb5.kdc_base_test import GroupType, KDCBaseTest, Principal +from samba.tests.krb5.raw_testcase import Krb5EncryptionKey, RawKerberosTest +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KRB_TGS_REP, + NT_PRINCIPAL, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +SidType = RawKerberosTest.SidType + +global_asn1_print = False +global_hexdump = False + + +class UnorderedList(tuple): + def __eq__(self, other): + if not isinstance(other, UnorderedList): + raise AssertionError('unexpected comparison attempt') + return sorted(self) == sorted(other) + + def __hash__(self): + return hash(tuple(sorted(self))) + + +@DynamicTestCase +class ClaimsTests(KDCBaseTest): + # Placeholder objects that represent accounts undergoing testing. + user = object() + mach = object() + + # Constants for group SID attributes. + default_attrs = security.SE_GROUP_DEFAULT_FLAGS + resource_attrs = default_attrs | security.SE_GROUP_RESOURCE + + asserted_identity = security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY + compounded_auth = security.SID_COMPOUNDED_AUTHENTICATION + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls._search_iterator = None + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def get_sample_dn(self): + if self._search_iterator is None: + samdb = self.get_samdb() + type(self)._search_iterator = samdb.search_iterator() + + return str(next(self._search_iterator).dn) + + def get_binary_dn(self): + return 'B:8:01010101:' + self.get_sample_dn() + + def setup_claims(self, all_claims): + expected_claims = {} + unexpected_claims = set() + + details = {} + mod_msg = ldb.Message() + security_desc = None + + for claim in all_claims: + # Make a copy to avoid modifying the original. + claim = dict(claim) + + claim_id = self.get_new_username() + + expected = claim.pop('expected', False) + expected_values = claim.pop('expected_values', None) + if not expected: + self.assertIsNone(expected_values, + 'claim not expected, ' + 'but expected values provided') + + values = claim.pop('values', None) + if values is not None: + def get_placeholder(val): + if val is self.sample_dn: + return self.get_sample_dn() + elif val is self.binary_dn: + return self.get_binary_dn() + else: + return val + + def ldb_transform(val): + if val is True: + return 'TRUE' + elif val is False: + return 'FALSE' + elif isinstance(val, int): + return str(val) + else: + return val + + values_type = type(values) + values = values_type(map(get_placeholder, values)) + transformed_values = values_type(map(ldb_transform, values)) + + attribute = claim['attribute'] + if attribute in details: + self.assertEqual(details[attribute], transformed_values, + 'conflicting values set for attribute') + details[attribute] = transformed_values + + readable = claim.pop('readable', True) + if not readable: + if security_desc is None: + security_desc = security.descriptor() + + # Deny all read property access to the attribute. + ace = security.ace() + ace.type = security.SEC_ACE_TYPE_ACCESS_DENIED_OBJECT + ace.access_mask = security.SEC_ADS_READ_PROP + ace.trustee = security.dom_sid(security.SID_WORLD) + ace.object.flags |= security.SEC_ACE_OBJECT_TYPE_PRESENT + ace.object.type = self.get_schema_id_guid_from_attribute( + attribute) + + security_desc.dacl_add(ace) + + if expected_values is None: + expected_values = values + + mod_values = claim.pop('mod_values', None) + if mod_values is not None: + flag = (ldb.FLAG_MOD_REPLACE + if values is not None else ldb.FLAG_MOD_ADD) + mod_msg[attribute] = ldb.MessageElement(mod_values, + flag, + attribute) + + if expected: + self.assertIsNotNone(expected_values, + 'expected claim, but no value(s) set') + value_type = claim['value_type'] + + expected_claims[claim_id] = { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': value_type, + 'values': expected_values, + } + else: + unexpected_claims.add(claim_id) + + self.create_claim(claim_id, **claim) + + if security_desc is not None: + self.assertNotIn('nTSecurityDescriptor', details) + details['nTSecurityDescriptor'] = ndr_pack(security_desc) + + return details, mod_msg, expected_claims, unexpected_claims + + def modify_pac_remove_client_claims(self, pac): + pac_buffers = pac.buffers + for pac_buffer in pac_buffers: + if pac_buffer.type == krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO: + pac.num_buffers -= 1 + pac_buffers.remove(pac_buffer) + + break + else: + self.fail('expected client claims in PAC') + + pac.buffers = pac_buffers + + return pac + + def remove_client_claims(self, ticket): + return self.modified_ticket( + ticket, + modify_pac_fn=self.modify_pac_remove_client_claims, + checksum_keys=self.get_krbtgt_checksum_key()) + + def remove_client_claims_tgt_from_rodc(self, ticket): + rodc_krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + rodc_krbtgt_key = self.TicketDecryptionKey_from_creds( + rodc_krbtgt_creds) + + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: rodc_krbtgt_key + } + + return self.modified_ticket( + ticket, + new_ticket_key=rodc_krbtgt_key, + modify_pac_fn=self.modify_pac_remove_client_claims, + checksum_keys=checksum_keys) + + def test_tgs_claims(self): + self.run_tgs_test(remove_claims=False, to_krbtgt=False) + + def test_tgs_claims_remove_claims(self): + self.run_tgs_test(remove_claims=True, to_krbtgt=False) + + def test_tgs_claims_to_krbtgt(self): + self.run_tgs_test(remove_claims=False, to_krbtgt=True) + + def test_tgs_claims_remove_claims_to_krbtgt(self): + self.run_tgs_test(remove_claims=True, to_krbtgt=True) + + def test_delegation_claims(self): + self.run_delegation_test(remove_claims=False) + + def test_delegation_claims_remove_claims(self): + self.run_delegation_test(remove_claims=True) + + def test_rodc_issued_claims_modify(self): + self.run_rodc_tgs_test(remove_claims=False, delete_claim=False) + + def test_rodc_issued_claims_delete(self): + self.run_rodc_tgs_test(remove_claims=False, delete_claim=True) + + def test_rodc_issued_claims_remove_claims_modify(self): + self.run_rodc_tgs_test(remove_claims=True, delete_claim=False) + + def test_rodc_issued_claims_remove_claims_delete(self): + self.run_rodc_tgs_test(remove_claims=True, delete_claim=True) + + def test_rodc_issued_device_claims_modify(self): + self.run_device_rodc_tgs_test(remove_claims=False, delete_claim=False) + + def test_rodc_issued_device_claims_delete(self): + self.run_device_rodc_tgs_test(remove_claims=False, delete_claim=True) + + def test_rodc_issued_device_claims_remove_claims_modify(self): + self.run_device_rodc_tgs_test(remove_claims=True, delete_claim=False) + + def test_rodc_issued_device_claims_remove_claims_delete(self): + self.run_device_rodc_tgs_test(remove_claims=True, delete_claim=True) + + # Create a user account with an applicable claim for the 'middleName' + # attribute. After obtaining a TGT, from which we optionally remove the + # claims, change the middleName attribute values for the account in the + # database to a different value. By which we may observe, when examining + # the reply to our following Kerberos TGS request, whether the claims + # contained therein are taken directly from the ticket, or obtained fresh + # from the database. + def run_tgs_test(self, remove_claims, to_krbtgt): + samdb = self.get_samdb() + user_creds, user_dn = self.create_account(samdb, + self.get_new_username(), + additional_details={ + 'middleName': 'foo', + }) + + claim_id = self.get_new_username() + self.create_claim(claim_id, + enabled=True, + attribute='middleName', + single_valued=True, + source_type='AD', + for_classes=['user'], + value_type=claims.CLAIM_TYPE_STRING) + + expected_claims = { + claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + } + + # Get a TGT for the user. + tgt = self.get_tgt(user_creds, expect_pac=True, + expect_client_claims=True, + expected_client_claims=expected_claims) + + if remove_claims: + tgt = self.remove_client_claims(tgt) + + # Change the value of the attribute used for the claim. + msg = ldb.Message(ldb.Dn(samdb, user_dn)) + msg['middleName'] = ldb.MessageElement('bar', + ldb.FLAG_MOD_REPLACE, + 'middleName') + samdb.modify(msg) + + if to_krbtgt: + target_creds = self.get_krbtgt_creds() + sname = self.get_krbtgt_sname() + else: + target_creds = self.get_service_creds() + sname = None + + # Get a service ticket for the user. The claim value should not have + # changed, indicating that the client claims are propagated straight + # through. + self.get_service_ticket( + tgt, target_creds, + sname=sname, + expect_pac=True, + expect_client_claims=not remove_claims, + expected_client_claims=(expected_claims + if not remove_claims else None)) + + # Perform a test similar to that preceding. This time, create both a user + # and a computer account, each having an applicable claim. After obtaining + # tickets, from which the claims are optionally removed, change the claim + # attribute of each account to a different value. Then perform constrained + # delegation with the user's service ticket, verifying that the user's + # claims are carried into the resulting ticket. + def run_delegation_test(self, remove_claims): + service_creds = self.get_service_creds() + service_spn = service_creds.get_spn() + + user_name = self.get_new_username() + mach_name = self.get_new_username() + + samdb = self.get_samdb() + user_creds, user_dn = self.create_account( + samdb, + user_name, + self.AccountType.USER, + additional_details={ + 'middleName': 'user_old', + }) + mach_creds, mach_dn = self.create_account( + samdb, + mach_name, + self.AccountType.COMPUTER, + spn=f'host/{mach_name}', + additional_details={ + 'middleName': 'mach_old', + 'msDS-AllowedToDelegateTo': service_spn, + }) + + claim_id = self.get_new_username() + self.create_claim(claim_id, + enabled=True, + attribute='middleName', + single_valued=True, + source_type='AD', + for_classes=['user', 'computer'], + value_type=claims.CLAIM_TYPE_STRING) + + options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(options) + + expected_claims_user = { + claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('user_old',), + }, + } + expected_claims_mach = { + claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('mach_old',), + }, + } + + user_tgt = self.get_tgt(user_creds, + kdc_options=options, + expect_pac=True, + expected_flags=expected_flags, + expect_client_claims=True, + expected_client_claims=expected_claims_user) + user_ticket = self.get_service_ticket( + user_tgt, + mach_creds, + kdc_options=options, + expect_pac=True, + expected_flags=expected_flags, + expect_client_claims=True, + expected_client_claims=expected_claims_user) + + mach_tgt = self.get_tgt(mach_creds, + expect_pac=True, + expect_client_claims=True, + expected_client_claims=expected_claims_mach) + + if remove_claims: + user_ticket = self.remove_client_claims(user_ticket) + mach_tgt = self.remove_client_claims(mach_tgt) + + # Change the value of the attribute used for the user claim. + msg = ldb.Message(ldb.Dn(samdb, user_dn)) + msg['middleName'] = ldb.MessageElement('user_new', + ldb.FLAG_MOD_REPLACE, + 'middleName') + samdb.modify(msg) + + # Change the value of the attribute used for the machine claim. + msg = ldb.Message(ldb.Dn(samdb, mach_dn)) + msg['middleName'] = ldb.MessageElement('mach_new', + ldb.FLAG_MOD_REPLACE, + 'middleName') + samdb.modify(msg) + + additional_tickets = [user_ticket.ticket] + options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + user_realm = user_creds.get_realm() + user_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[user_name]) + + user_sid = user_creds.get_sid() + + mach_realm = mach_creds.get_realm() + + service_name = service_creds.get_username()[:-1] + service_realm = service_creds.get_realm() + service_sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', service_name]) + service_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + service_etypes = service_creds.tgs_supported_enctypes + + expected_proxy_target = service_creds.get_spn() + expected_transited_services = [f'host/{mach_name}@{mach_realm}'] + + authenticator_subkey = self.RandomKey(Enctype.AES256) + + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + # The user's claims are propagated into the new ticket, while the + # machine's claims are dispensed with. + expected_claims = expected_claims_user if not remove_claims else None + + # Perform constrained delegation. + kdc_exchange_dict = self.tgs_exchange_dict( + creds=user_creds, + expected_crealm=user_realm, + expected_cname=user_cname, + expected_srealm=service_realm, + expected_sname=service_sname, + expected_account_name=user_name, + expected_sid=user_sid, + expected_supported_etypes=service_etypes, + ticket_decryption_key=service_decryption_key, + check_rep_fn=self.generic_check_kdc_rep, + check_kdc_private_fn=self.generic_check_kdc_private, + tgt=mach_tgt, + authenticator_subkey=authenticator_subkey, + kdc_options=options, + expected_proxy_target=expected_proxy_target, + expected_transited_services=expected_transited_services, + expect_client_claims=not remove_claims, + expected_client_claims=expected_claims, + expect_device_claims=False, + expect_pac=True) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=service_realm, + sname=service_sname, + etypes=etypes, + additional_tickets=additional_tickets) + self.check_reply(rep, KRB_TGS_REP) + + def run_rodc_tgs_test(self, remove_claims, delete_claim): + samdb = self.get_samdb() + # Create a user account permitted to replicate to the RODC. + user_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + # Set the value of the claim attribute. + 'additional_details': (('middleName', 'foo'),), + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + }, + use_cache=False) + user_dn = user_creds.get_dn() + + # Create a claim that applies to the user. + claim_id = self.get_new_username() + self.create_claim(claim_id, + enabled=True, + attribute='middleName', + single_valued=True, + source_type='AD', + for_classes=['user'], + value_type=claims.CLAIM_TYPE_STRING) + + expected_claims = { + claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + } + + # Get a TGT for the user. + tgt = self.get_tgt(user_creds, expect_pac=True, + expect_client_claims=True, + expected_client_claims=expected_claims) + + # Modify the TGT to be issued by an RODC. Optionally remove the client + # claims. + if remove_claims: + tgt = self.remove_client_claims_tgt_from_rodc(tgt) + else: + tgt = self.issued_by_rodc(tgt) + + # Modify or delete the value of the attribute used for the claim. Modify + # our test expectations accordingly. + msg = ldb.Message(user_dn) + if delete_claim: + msg['middleName'] = ldb.MessageElement([], + ldb.FLAG_MOD_DELETE, + 'middleName') + expected_claims = None + unexpected_claims = {claim_id} + else: + msg['middleName'] = ldb.MessageElement('bar', + ldb.FLAG_MOD_REPLACE, + 'middleName') + expected_claims = { + claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('bar',), + }, + } + unexpected_claims = None + samdb.modify(msg) + + target_creds = self.get_service_creds() + + # Get a service ticket for the user. The claim value should have + # changed, indicating that the client claims have been regenerated or + # removed, depending on whether the corresponding attribute is still + # present on the account. + self.get_service_ticket( + tgt, target_creds, + expect_pac=True, + # Expect the CLIENT_CLAIMS_INFO PAC buffer. It may be empty. + expect_client_claims=True, + expected_client_claims=expected_claims, + unexpected_client_claims=unexpected_claims) + + def run_device_rodc_tgs_test(self, remove_claims, delete_claim): + samdb = self.get_samdb() + + # Create the user account. + user_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + user_name = user_creds.get_username() + + # Create a machine account permitted to replicate to the RODC. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + # Set the value of the claim attribute. + 'additional_details': (('middleName', 'foo'),), + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + }, + use_cache=False) + mach_dn = mach_creds.get_dn() + + # Create a claim that applies to the computer. + claim_id = self.get_new_username() + self.create_claim(claim_id, + enabled=True, + attribute='middleName', + single_valued=True, + source_type='AD', + for_classes=['computer'], + value_type=claims.CLAIM_TYPE_STRING) + + expected_claims = { + claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + } + + # Get a TGT for the user. + user_tgt = self.get_tgt(user_creds) + + # Get a TGT for the computer. + mach_tgt = self.get_tgt(mach_creds, expect_pac=True, + expect_client_claims=True, + expected_client_claims=expected_claims) + + # Modify the computer's TGT to be issued by an RODC. Optionally remove + # the client claims. + if remove_claims: + mach_tgt = self.remove_client_claims_tgt_from_rodc(mach_tgt) + else: + mach_tgt = self.issued_by_rodc(mach_tgt) + + # Modify or delete the value of the attribute used for the claim. Modify + # our test expectations accordingly. + msg = ldb.Message(mach_dn) + if delete_claim: + msg['middleName'] = ldb.MessageElement([], + ldb.FLAG_MOD_DELETE, + 'middleName') + expected_claims = None + unexpected_claims = {claim_id} + else: + msg['middleName'] = ldb.MessageElement('bar', + ldb.FLAG_MOD_REPLACE, + 'middleName') + expected_claims = { + claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('bar',), + }, + } + unexpected_claims = None + samdb.modify(msg) + + subkey = self.RandomKey(user_tgt.session_key.etype) + + armor_subkey = self.RandomKey(subkey.etype) + explicit_armor_key = self.generate_armor_key(armor_subkey, + mach_tgt.session_key) + armor_key = kcrypto.cf2(explicit_armor_key.key, + subkey.key, + b'explicitarmor', + b'tgsarmor') + armor_key = Krb5EncryptionKey(armor_key, None) + + target_creds = self.get_service_creds() + target_name = target_creds.get_username() + if target_name[-1] == '$': + target_name = target_name[:-1] + + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', target_name]) + srealm = target_creds.get_realm() + + decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + target_supported_etypes = target_creds.tgs_supported_enctypes + + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + kdc_options = '0' + pac_options = '1' # claims support + + # Perform a TGS-REQ for the user. The device claim value should have + # changed, indicating that the computer's client claims have been + # regenerated or removed, depending on whether the corresponding + # attribute is still present on the account. + + kdc_exchange_dict = self.tgs_exchange_dict( + creds=user_creds, + expected_crealm=user_tgt.crealm, + expected_cname=user_tgt.cname, + expected_srealm=srealm, + expected_sname=sname, + expected_account_name=user_name, + ticket_decryption_key=decryption_key, + generate_fast_fn=self.generate_simple_fast, + generate_fast_armor_fn=self.generate_ap_req, + check_rep_fn=self.generic_check_kdc_rep, + check_kdc_private_fn=self.generic_check_kdc_private, + tgt=user_tgt, + armor_key=armor_key, + armor_tgt=mach_tgt, + armor_subkey=armor_subkey, + pac_options=pac_options, + authenticator_subkey=subkey, + kdc_options=kdc_options, + expect_pac=True, + expected_supported_etypes=target_supported_etypes, + # Expect the DEVICE_CLAIMS_INFO PAC buffer. It may be empty. + expect_device_claims=True, + expected_device_claims=expected_claims, + unexpected_device_claims=unexpected_claims) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=srealm, + sname=sname, + etypes=etypes) + self.check_reply(rep, KRB_TGS_REP) + + @classmethod + def setUpDynamicTestCases(cls): + FILTER = env_get_var_value('FILTER', allow_missing=True) + for case in cls.cases: + name = case.pop('name') + name = re.sub(r'\W+', '_', name) + if FILTER and not re.search(FILTER, name): + continue + + # Run tests making requests both to the krbtgt and to our own + # account. + cls.generate_dynamic_test('test_claims', name, + dict(case), False) + cls.generate_dynamic_test('test_claims', name + '_to_self', + dict(case), True) + + for case in cls.device_claims_cases: + name = case.pop('test') + name = re.sub(r'\W+', '_', name) + if FILTER and not re.search(FILTER, name): + continue + + cls.generate_dynamic_test('test_device_claims', name, + dict(case)) + + def _test_claims_with_args(self, case, to_self): + account_class = case.pop('class') + if account_class == 'user': + account_type = self.AccountType.USER + elif account_class == 'computer': + account_type = self.AccountType.COMPUTER + else: + self.fail(f'Unknown class "{account_class}"') + + all_claims = case.pop('claims') + (details, mod_msg, + expected_claims, + unexpected_claims) = self.setup_claims(all_claims) + self.assertFalse(mod_msg, + 'mid-test modifications not supported in this test') + creds = self.get_cached_creds( + account_type=account_type, + opts={ + 'additional_details': self.freeze(details), + }) + + # Whether to specify claims support in PA-PAC-OPTIONS. + pac_options_claims = case.pop('pac-options:claims-support', None) + + self.assertFalse(case, 'unexpected parameters in testcase') + + if pac_options_claims is None: + pac_options_claims = True + + if to_self: + service_creds = self.get_service_creds() + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[service_creds.get_username()]) + ticket_etype = Enctype.RC4 + else: + service_creds = None + sname = None + ticket_etype = None + + if pac_options_claims: + pac_options = '1' # claims support + else: + pac_options = '0' # no claims support + + self.get_tgt(creds, + sname=sname, + target_creds=service_creds, + ticket_etype=ticket_etype, + pac_options=pac_options, + expect_pac=True, + expect_client_claims=True, + expected_client_claims=expected_claims or None, + unexpected_client_claims=unexpected_claims or None) + + sample_dn = object() + binary_dn = object() + security_descriptor = (b'\x01\x00\x04\x95\x14\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00$\x00\x00\x00\x01\x02\x00\x00\x00' + b'\x00\x00\x05 \x00\x00\x00 \x02\x00\x00\x04\x00' + b'\x1c\x00\x01\x00\x00\x00\x00\x1f\x14\x00\xff\x01' + b'\x0f\xf0\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00') + + cases = [ + { + 'name': 'no claims', + 'claims': [], + 'class': 'user', + }, + { + 'name': 'simple AD-sourced claim', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'no claims support in pac options', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + # We still get claims in the PAC even if we don't specify + # claims support in PA-PAC-OPTIONS. + 'expected': True, + }, + ], + 'class': 'user', + 'pac-options:claims-support': False, + }, + { + 'name': 'deny RP', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + # Deny read access to the attribute. It still shows up in + # the claim. + 'readable': False, + 'expected': True, + }, + ], + 'class': 'user', + }, + { + # Note: The order of these DNs may differ on Windows. + 'name': 'dn string syntax', + 'claims': [ + { + # 2.5.5.1 + 'enabled': True, + 'attribute': 'msDS-AuthenticatedAtDC', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': UnorderedList([sample_dn, sample_dn, sample_dn]), + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'dn string syntax, wrong value type', + 'claims': [ + { + # 2.5.5.1 + 'enabled': True, + 'attribute': 'msDS-AuthenticatedAtDC', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_BOOLEAN, + 'values': UnorderedList([sample_dn, sample_dn, sample_dn]), + }, + ], + 'class': 'user', + }, + { + 'name': 'oid syntax', + 'claims': [ + { + # 2.5.5.2 + 'enabled': True, + 'attribute': 'objectClass', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_UINT64, + 'expected_values': [655369, 65543, 65542, 65536], + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'oid syntax 2', + 'claims': [ + { + # 2.5.5.2 + 'enabled': True, + 'attribute': 'objectClass', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['computer'], + 'value_type': claims.CLAIM_TYPE_UINT64, + 'expected_values': [196638, 655369, 65543, 65542, 65536], + 'expected': True, + }, + ], + 'class': 'computer', + }, + { + 'name': 'oid syntax, wrong value type', + 'claims': [ + { + # 2.5.5.2 + 'enabled': True, + 'attribute': 'objectClass', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_INT64, + }, + ], + 'class': 'user', + }, + { + 'name': 'boolean syntax, true', + 'claims': [ + { + # 2.5.5.8 + 'enabled': True, + 'attribute': 'msTSAllowLogon', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_BOOLEAN, + 'values': (True,), + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'boolean syntax, false', + 'claims': [ + { + # 2.5.5.8 + 'enabled': True, + 'attribute': 'msTSAllowLogon', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_BOOLEAN, + 'values': (False,), + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'boolean syntax, wrong value type', + 'claims': [ + { + # 2.5.5.8 + 'enabled': True, + 'attribute': 'msTSAllowLogon', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': (True,), + }, + ], + 'class': 'user', + }, + { + 'name': 'integer syntax', + 'claims': [ + { + # 2.5.5.9 + 'enabled': True, + 'attribute': 'localeID', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_INT64, + 'values': (3, 42, -999, 1000, 20000), + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'integer syntax, duplicate claim', + 'claims': [ + { + # 2.5.5.9 + 'enabled': True, + 'attribute': 'localeID', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_INT64, + 'values': (3, 42, -999, 1000, 20000), + 'expected': True, + }, + ] * 2, # Create two integer syntax claims. + 'class': 'user', + }, + { + 'name': 'integer syntax, wrong value type', + 'claims': [ + { + # 2.5.5.9 + 'enabled': True, + 'attribute': 'localeID', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_UINT64, + 'values': (3, 42, -999, 1000), + }, + ], + 'class': 'user', + }, + { + 'name': 'security descriptor syntax', + 'claims': [ + { + # 2.5.5.15 + 'enabled': True, + 'attribute': 'msDS-AllowedToActOnBehalfOfOtherIdentity', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['computer'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': (security_descriptor,), + 'expected_values': ( + 'O:BAD:PARAI(A;OICINPIOID;CCDCLCSWRPWPDTLOCRSDRCWDWOGAGXGWGR;;;S-1-0-0)', + ), + 'expected': True, + }, + ], + 'class': 'computer', + }, + { + 'name': 'security descriptor syntax, wrong value type', + 'claims': [ + { + # 2.5.5.15 + 'enabled': True, + 'attribute': 'msDS-AllowedToActOnBehalfOfOtherIdentity', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['computer'], + 'value_type': claims.CLAIM_TYPE_UINT64, + 'values': (security_descriptor,), + }, + ], + 'class': 'computer', + }, + { + 'name': 'case insensitive string syntax (invalid)', + 'claims': [ + { + # 2.5.5.4 + 'enabled': True, + 'attribute': 'networkAddress', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo', 'bar'), + }, + ], + 'class': 'user', + }, + { + 'name': 'printable string syntax (invalid)', + 'claims': [ + { + # 2.5.5.5 + 'enabled': True, + 'attribute': 'displayNamePrintable', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'numeric string syntax (invalid)', + 'claims': [ + { + # 2.5.5.6 + 'enabled': True, + 'attribute': 'internationalISDNNumber', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo', 'bar'), + }, + ], + 'class': 'user', + }, + { + 'name': 'dn binary syntax (invalid)', + 'claims': [ + { + # 2.5.5.7 + 'enabled': True, + 'attribute': 'msDS-RevealedUsers', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': (binary_dn, binary_dn, binary_dn), + }, + ], + 'class': 'computer', + }, + { + 'name': 'octet string syntax (invalid)', + 'claims': [ + { + # 2.5.5.10 + 'enabled': True, + 'attribute': 'jpegPhoto', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo', 'bar'), + }, + ], + 'class': 'user', + }, + { + 'name': 'utc time syntax (invalid)', + 'claims': [ + { + # 2.5.5.11 + 'enabled': True, + 'attribute': 'msTSExpireDate2', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('19700101000000.0Z',), + }, + ], + 'class': 'user', + }, + { + 'name': 'access point syntax (invalid)', + 'claims': [ + { + # 2.5.5.17 + 'enabled': True, + 'attribute': 'mS-DS-CreatorSID', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + }, + ], + 'class': 'user', + }, + { + 'name': 'no value set', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + }, + ], + 'class': 'user', + }, + { + 'name': 'multi-valued claim', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo', 'bar', 'baz'), + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'missing attribute', + 'claims': [ + { + 'enabled': True, + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + }, + ], + 'class': 'user', + }, + { + 'name': 'invalid attribute', + 'claims': [ + { + # 2.5.5.10 + 'enabled': True, + 'attribute': 'unicodePwd', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + }, + ], + 'class': 'user', + }, + { + 'name': 'incorrect value type', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_INT64, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'invalid value type', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': 0, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'missing value type', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'string syntax, duplicate claim', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + 'expected': True, + }, + ] * 2, # Create two string syntax claims. + 'class': 'user', + }, + { + 'name': 'multiple claims', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo', 'bar', 'baz'), + 'expected': True, + }, + { + # 2.5.5.8 + 'enabled': True, + 'attribute': 'msTSAllowLogon', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_BOOLEAN, + 'values': (True,), + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'case difference for source type', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'ad', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + 'expected': True, + }, + ], + 'class': 'user', + }, + { + 'name': 'unhandled source type', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': '<unknown>', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'disabled claim', + 'claims': [ + { + # 2.5.5.12 + 'enabled': False, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'not enabled claim', + 'claims': [ + { + # 2.5.5.12 + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'not applicable to any class', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'not applicable to class', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + ], + 'class': 'computer', + }, + { + 'name': 'applicable to class', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user', 'computer'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + 'expected': True, + }, + ], + 'class': 'computer', + }, + { + 'name': 'applicable to base class', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['top'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'applicable to base class 2', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['organizationalPerson'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + ], + 'class': 'user', + }, + { + 'name': 'large compressed claim', + 'claims': [ + { + # 2.5.5.12 + 'enabled': True, + 'attribute': 'carLicense', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['user'], + 'value_type': claims.CLAIM_TYPE_STRING, + # a large value that should cause the claim to be + # compressed. + 'values': ('a' * 10000,), + 'expected': True, + }, + ], + 'class': 'user', + }, + ] + + def _test_device_claims_with_args(self, case): + # The group arrangement for the test. + group_setup = case.pop('groups') + + # Groups that should be the primary group for the user and machine + # respectively. + primary_group = case.pop('primary_group', None) + mach_primary_group = case.pop('mach:primary_group', None) + + # Whether the TGS-REQ should be directed to the krbtgt. + tgs_to_krbtgt = case.pop('tgs:to_krbtgt', None) + + # Whether the target server of the TGS-REQ should support compound + # identity or resource SID compression. + tgs_compound_id = case.pop('tgs:compound_id', None) + tgs_compression = case.pop('tgs:compression', None) + + # Optional SIDs to replace those in the machine account PAC prior to a + # TGS-REQ. + tgs_mach_sids = case.pop('tgs:mach:sids', None) + + # Optional machine SID to replace that in the PAC prior to a TGS-REQ. + tgs_mach_sid = case.pop('tgs:mach_sid', None) + + # User flags that may be set or reset in the PAC prior to a TGS-REQ. + tgs_mach_set_user_flags = case.pop('tgs:mach:set_user_flags', None) + tgs_mach_reset_user_flags = case.pop('tgs:mach:reset_user_flags', None) + + # The SIDs we expect to see in the PAC after a AS-REQ or a TGS-REQ. + as_expected = case.pop('as:expected', None) + as_mach_expected = case.pop('as:mach:expected', None) + tgs_expected = case.pop('tgs:expected', None) + tgs_device_expected = case.pop('tgs:device:expected', None) + + # Whether to specify claims support in PA-PAC-OPTIONS. + pac_options_claims = case.pop('pac-options:claims-support', None) + + all_claims = case.pop('claims') + + # There should be no parameters remaining in the testcase. + self.assertFalse(case, 'unexpected parameters in testcase') + + if as_expected is None: + self.assertIsNotNone(tgs_expected, + 'no set of expected SIDs is provided') + + if as_mach_expected is None: + self.assertIsNotNone(tgs_expected, + 'no set of expected machine SIDs is provided') + + if tgs_to_krbtgt is None: + tgs_to_krbtgt = False + + if tgs_compound_id is None and not tgs_to_krbtgt: + # Assume the service supports compound identity by default. + tgs_compound_id = True + + if tgs_to_krbtgt: + self.assertIsNone(tgs_device_expected, + 'device SIDs are not added for a krbtgt request') + + self.assertIsNotNone(tgs_expected, + 'no set of expected TGS SIDs is provided') + + if tgs_mach_sid is not None: + self.assertIsNotNone(tgs_mach_sids, + 'specified TGS-REQ mach SID, but no ' + 'accompanying machine SIDs provided') + + if tgs_mach_set_user_flags is None: + tgs_mach_set_user_flags = 0 + else: + self.assertIsNotNone(tgs_mach_sids, + 'specified TGS-REQ set user flags, but no ' + 'accompanying machine SIDs provided') + + if tgs_mach_reset_user_flags is None: + tgs_mach_reset_user_flags = 0 + else: + self.assertIsNotNone(tgs_mach_sids, + 'specified TGS-REQ reset user flags, but no ' + 'accompanying machine SIDs provided') + + if pac_options_claims is None: + pac_options_claims = True + + (details, mod_msg, + expected_claims, + unexpected_claims) = self.setup_claims(all_claims) + + samdb = self.get_samdb() + + domain_sid = samdb.get_domain_sid() + + user_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + user_dn = user_creds.get_dn() + user_sid = user_creds.get_sid() + + mach_name = self.get_new_username() + mach_creds, mach_dn_str = self.create_account( + samdb, + mach_name, + account_type=self.AccountType.COMPUTER, + additional_details=details) + mach_dn = ldb.Dn(samdb, mach_dn_str) + mach_sid = mach_creds.get_sid() + + user_principal = Principal(user_dn, user_sid) + mach_principal = Principal(mach_dn, mach_sid) + preexisting_groups = { + self.user: user_principal, + self.mach: mach_principal, + } + primary_groups = {} + if primary_group is not None: + primary_groups[user_principal] = primary_group + if mach_primary_group is not None: + primary_groups[mach_principal] = mach_primary_group + groups = self.setup_groups(samdb, + preexisting_groups, + group_setup, + primary_groups) + del group_setup + + tgs_user_sid = user_sid + tgs_user_domain_sid, tgs_user_rid = tgs_user_sid.rsplit('-', 1) + + if tgs_mach_sid is None: + tgs_mach_sid = mach_sid + elif tgs_mach_sid in groups: + tgs_mach_sid = groups[tgs_mach_sid].sid + + tgs_mach_domain_sid, tgs_mach_rid = tgs_mach_sid.rsplit('-', 1) + + expected_groups = self.map_sids(as_expected, groups, + domain_sid) + mach_expected_groups = self.map_sids(as_mach_expected, groups, + domain_sid) + tgs_mach_sids_mapped = self.map_sids(tgs_mach_sids, groups, + tgs_mach_domain_sid) + tgs_expected_mapped = self.map_sids(tgs_expected, groups, + tgs_user_domain_sid) + tgs_device_expected_mapped = self.map_sids(tgs_device_expected, groups, + tgs_mach_domain_sid) + + user_tgt = self.get_tgt(user_creds, expected_groups=expected_groups) + + # Get a TGT for the computer. + mach_tgt = self.get_tgt(mach_creds, expect_pac=True, + expected_groups=mach_expected_groups, + expect_client_claims=True, + expected_client_claims=expected_claims, + unexpected_client_claims=unexpected_claims) + + if tgs_mach_sids is not None: + # Replace the SIDs in the PAC with the ones provided by the test. + mach_tgt = self.ticket_with_sids(mach_tgt, + tgs_mach_sids_mapped, + tgs_mach_domain_sid, + tgs_mach_rid, + set_user_flags=tgs_mach_set_user_flags, + reset_user_flags=tgs_mach_reset_user_flags) + + if mod_msg: + self.assertFalse(tgs_to_krbtgt, + 'device claims are omitted for a krbtgt request, ' + 'so specifying mod_values is probably a mistake!') + + # Change the value of attributes used for claims. + mod_msg.dn = mach_dn + samdb.modify(mod_msg) + + domain_sid = samdb.get_domain_sid() + + subkey = self.RandomKey(user_tgt.session_key.etype) + + armor_subkey = self.RandomKey(subkey.etype) + explicit_armor_key = self.generate_armor_key(armor_subkey, + mach_tgt.session_key) + armor_key = kcrypto.cf2(explicit_armor_key.key, + subkey.key, + b'explicitarmor', + b'tgsarmor') + armor_key = Krb5EncryptionKey(armor_key, None) + + target_creds, sname = self.get_target( + to_krbtgt=tgs_to_krbtgt, + compound_id=tgs_compound_id, + compression=tgs_compression) + srealm = target_creds.get_realm() + + decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + kdc_options = '0' + if pac_options_claims: + pac_options = '1' # claims support + else: + pac_options = '0' # no claims support + + requester_sid = None + if tgs_to_krbtgt: + requester_sid = user_sid + + if not tgs_compound_id: + expected_claims = None + unexpected_claims = None + + # Get a service ticket for the user, using the computer's TGT as an + # armor TGT. The claim value should not have changed. + + kdc_exchange_dict = self.tgs_exchange_dict( + creds=user_creds, + expected_crealm=user_tgt.crealm, + expected_cname=user_tgt.cname, + expected_srealm=srealm, + expected_sname=sname, + ticket_decryption_key=decryption_key, + generate_fast_fn=self.generate_simple_fast, + generate_fast_armor_fn=self.generate_ap_req, + check_rep_fn=self.generic_check_kdc_rep, + check_kdc_private_fn=self.generic_check_kdc_private, + tgt=user_tgt, + armor_key=armor_key, + armor_tgt=mach_tgt, + armor_subkey=armor_subkey, + pac_options=pac_options, + authenticator_subkey=subkey, + kdc_options=kdc_options, + expect_pac=True, + expect_pac_attrs=tgs_to_krbtgt, + expect_pac_attrs_pac_request=tgs_to_krbtgt, + expected_sid=tgs_user_sid, + expected_requester_sid=requester_sid, + expected_domain_sid=tgs_user_domain_sid, + expected_device_domain_sid=tgs_mach_domain_sid, + expected_groups=tgs_expected_mapped, + unexpected_groups=None, + expect_client_claims=True, + expected_client_claims=None, + expect_device_info=bool(tgs_compound_id), + expected_device_groups=tgs_device_expected_mapped, + expect_device_claims=bool(tgs_compound_id), + expected_device_claims=expected_claims, + unexpected_device_claims=unexpected_claims) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=srealm, + sname=sname, + etypes=etypes) + self.check_reply(rep, KRB_TGS_REP) + + device_claims_cases = [ + { + # Make a TGS request containing claims, but omit the Claims Valid + # SID. + 'test': 'device to service no claims valid sid', + 'groups': { + # Some groups to test how the device info is generated. + 'foo': (GroupType.DOMAIN_LOCAL, {mach}), + 'bar': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'claims': [ + { + # 2.5.5.10 + 'enabled': True, + 'attribute': 'middleName', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['computer'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + 'expected': True, + 'mod_values': ['bar'], + }, + ], + 'as:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # Omit the Claims Valid SID, and verify that this doesn't + # affect the propagation of claims into the final ticket. + + # Some extra SIDs to show how they are propagated into the + # final ticket. + ('S-1-5-22-1-2-3-4', SidType.EXTRA_SID, default_attrs), + ('S-1-5-22-1-2-3-5', SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + (security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + ('S-1-5-22-1-2-3-4', SidType.EXTRA_SID, default_attrs), + ('S-1-5-22-1-2-3-5', SidType.EXTRA_SID, default_attrs), + frozenset([ + ('foo', SidType.RESOURCE_SID, resource_attrs), + ('bar', SidType.RESOURCE_SID, resource_attrs), + ]), + }, + }, + { + # Make a TGS request containing claims to a service that lacks + # support for compound identity. The claims are not propagated to + # the final ticket. + 'test': 'device to service no compound id', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {mach}), + 'bar': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'claims': [ + { + # 2.5.5.10 + 'enabled': True, + 'attribute': 'middleName', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['computer'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + 'expected': True, + 'mod_values': ['bar'], + }, + ], + 'as:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # Compound identity is unsupported. + 'tgs:compound_id': False, + 'tgs:expected': { + (security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # The Compounded Authentication SID should not be present. + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + }, + }, + { + # Make a TGS request containing claims to a service, but don't + # specify support for claims in PA-PAC-OPTIONS. We still expect the + # final PAC to contain claims. + 'test': 'device to service no claims support in pac options', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {mach}), + 'bar': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'claims': [ + { + # 2.5.5.10 + 'enabled': True, + 'attribute': 'middleName', + 'single_valued': True, + 'source_type': 'AD', + 'for_classes': ['computer'], + 'value_type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + 'expected': True, + 'mod_values': ['bar'], + }, + ], + 'as:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # Claims are unsupported. + 'pac-options:claims-support': False, + 'tgs:expected': { + (security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + frozenset([ + ('foo', SidType.RESOURCE_SID, resource_attrs), + ('bar', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + ] + + def test_auth_silo_claim(self): + self.run_auth_silo_claim_test() + + def test_auth_silo_claim_unenforced(self): + # The claim is not present if the silo is unenforced. + self.run_auth_silo_claim_test(enforced=False, + expect_claim=False) + + def test_auth_silo_claim_not_a_member(self): + # The claim is not present if the user is not a member of the silo. + self.run_auth_silo_claim_test(add_to_silo=False, + expect_claim=False) + + def test_auth_silo_claim_unassigned(self): + # The claim is not present if the user is not assigned to the silo. + self.run_auth_silo_claim_test(assigned=False, + expect_claim=False) + + def test_auth_silo_claim_assigned_to_wrong_dn(self): + samdb = self.get_samdb() + + # The claim is not present if the user is assigned to some other DN. + self.run_auth_silo_claim_test(assigned=self.get_server_dn(samdb), + expect_claim=False) + + def run_auth_silo_claim_test(self, *, + enforced=True, + add_to_silo=True, + assigned=True, + expect_claim=True): + # Create a new authentication silo. + silo = self.create_authn_silo(enforced=enforced) + + account_options = None + if assigned is not False: + if assigned is True: + assigned = silo.dn + + account_options = { + 'additional_details': self.freeze({ + # The user is assigned to the authentication silo we just + # created, or to some DN specified by a test. + 'msDS-AssignedAuthNPolicySilo': str(assigned), + }), + } + + # Create the user account. + creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts=account_options) + + if add_to_silo: + # Add the account to the silo. + self.add_to_group(str(creds.get_dn()), + silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + claim_id = self.create_authn_silo_claim_id() + + if expect_claim: + expected_claims = { + claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + # Expect a claim containing the name of the silo. + 'values': (silo.name,), + }, + } + unexpected_claims = None + else: + expected_claims = None + unexpected_claims = {claim_id} + + # Get a TGT and check whether the claim is present or missing. + self.get_tgt(creds, + expect_pac=True, + expect_client_claims=True, + expected_client_claims=expected_claims, + unexpected_client_claims=unexpected_claims) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/compatability_tests.py b/python/samba/tests/krb5/compatability_tests.py new file mode 100755 index 0000000..e1ebe18 --- /dev/null +++ b/python/samba/tests/krb5/compatability_tests.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.tests.krb5.kdc_base_test import KDCBaseTest +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +from samba.tests.krb5.rfc4120_constants import ( + AES128_CTS_HMAC_SHA1_96, + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_PREAUTH_REQUIRED, + KRB_AS_REP, + KRB_ERROR, + KU_AS_REP_ENC_PART, + KU_PA_ENC_TIMESTAMP, + PADATA_ENC_TIMESTAMP, + PADATA_ETYPE_INFO2, + NT_PRINCIPAL, + NT_SRV_INST, +) + +global_asn1_print = False +global_hexdump = False + +HEIMDAL_ENC_AS_REP_PART_TYPE_TAG = 0x79 +# MIT uses the EncTGSRepPart tag for the EncASRepPart +MIT_ENC_AS_REP_PART_TYPE_TAG = 0x7A + +ENC_PA_REP_FLAG = 0x00010000 + + +class CompatabilityTests(KDCBaseTest): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def test_mit_EncASRepPart_tag(self): + creds = self.get_user_creds() + (enc, _) = self.as_req(creds) + self.assertEqual(MIT_ENC_AS_REP_PART_TYPE_TAG, enc[0]) + + def test_heimdal_EncASRepPart_tag(self): + creds = self.get_user_creds() + (enc, _) = self.as_req(creds) + self.assertEqual(HEIMDAL_ENC_AS_REP_PART_TYPE_TAG, enc[0]) + + def test_mit_EncryptedData_kvno(self): + creds = self.get_user_creds() + (_, enc) = self.as_req(creds) + if 'kvno' in enc: + self.fail("kvno present in EncryptedData") + + def test_heimdal_EncryptedData_kvno(self): + creds = self.get_user_creds() + (_, enc) = self.as_req(creds) + if 'kvno' not in enc: + self.fail("kvno absent in EncryptedData") + + def test_mit_EncASRepPart_FAST_support(self): + creds = self.get_user_creds() + (enc, _) = self.as_req(creds) + self.assertEqual(MIT_ENC_AS_REP_PART_TYPE_TAG, enc[0]) + as_rep = self.der_decode(enc, asn1Spec=krb5_asn1.EncTGSRepPart()) + flags = int(as_rep['flags'], base=2) + # MIT sets enc-pa-rep, flag bit 15 + # RFC 6806 11. Negotiation of FAST and Detecting Modified Requests + self.assertTrue(ENC_PA_REP_FLAG & flags) + + def test_heimdal_and_windows_EncASRepPart_FAST_support(self): + creds = self.get_user_creds() + (enc, _) = self.as_req(creds) + self.assertEqual(HEIMDAL_ENC_AS_REP_PART_TYPE_TAG, enc[0]) + as_rep = self.der_decode(enc, asn1Spec=krb5_asn1.EncASRepPart()) + flags = as_rep['flags'] + flags = int(as_rep['flags'], base=2) + # Heimdal and Windows does set enc-pa-rep, flag bit 15 + # RFC 6806 11. Negotiation of FAST and Detecting Modified Requests + self.assertTrue(ENC_PA_REP_FLAG & flags) + + def test_mit_arcfour_salt(self): + creds = self.get_user_creds() + etypes = (ARCFOUR_HMAC_MD5,) + (rep, *_) = self.as_pre_auth_req(creds, etypes) + self.check_preauth_rep(rep) + etype_info2 = self.get_etype_info2(rep) + if 'salt' not in etype_info2[0]: + self.fail( + "(MIT) Salt not populated for ARCFOUR_HMAC_MD5 encryption") + + def test_heimdal_arcfour_salt(self): + creds = self.get_user_creds() + etypes = (ARCFOUR_HMAC_MD5,) + (rep, *_) = self.as_pre_auth_req(creds, etypes) + self.check_preauth_rep(rep) + etype_info2 = self.get_etype_info2(rep) + if 'salt' in etype_info2[0]: + self.fail( + "(Heimdal) Salt populated for ARCFOUR_HMAC_MD5 encryption") + + def as_pre_auth_req(self, creds, etypes): + user = creds.get_username() + realm = creds.get_realm() + + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, + names=["krbtgt", realm]) + + till = self.get_KerberosTime(offset=36000) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + padata = None + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + + return (rep, cname, sname, realm, till) + + def check_preauth_rep(self, rep): + self.assertIsNotNone(rep) + self.assertEqual(rep['msg-type'], KRB_ERROR) + self.assertEqual(rep['error-code'], KDC_ERR_PREAUTH_REQUIRED) + + def get_etype_info2(self, rep): + + rep_padata = self.der_decode( + rep['e-data'], + asn1Spec=krb5_asn1.METHOD_DATA()) + + for pa in rep_padata: + if pa['padata-type'] == PADATA_ETYPE_INFO2: + etype_info2 = pa['padata-value'] + break + + etype_info2 = self.der_decode( + etype_info2, + asn1Spec=krb5_asn1.ETYPE_INFO2()) + return etype_info2 + + def as_req(self, creds): + etypes = ( + AES256_CTS_HMAC_SHA1_96, + AES128_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5) + (rep, cname, sname, realm, till) = self.as_pre_auth_req(creds, etypes) + self.check_preauth_rep(rep) + + etype_info2 = self.get_etype_info2(rep) + key = self.PasswordKey_from_etype_info2(creds, etype_info2[0]) + + (patime, pausec) = self.get_KerberosTimeWithUsec() + pa_ts = self.PA_ENC_TS_ENC_create(patime, pausec) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + pa_ts = self.EncryptedData_create(key, KU_PA_ENC_TIMESTAMP, pa_ts) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.EncryptedData()) + + pa_ts = self.PA_DATA_create(PADATA_ENC_TIMESTAMP, pa_ts) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + padata = [pa_ts] + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + msg_type = rep['msg-type'] + self.assertEqual(msg_type, KRB_AS_REP) + + enc_part = rep['enc-part'] + enc_as_rep_part = key.decrypt( + KU_AS_REP_ENC_PART, rep['enc-part']['cipher']) + return (enc_as_rep_part, enc_part) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/conditional_ace_tests.py b/python/samba/tests/krb5/conditional_ace_tests.py new file mode 100755 index 0000000..f8dc0ef --- /dev/null +++ b/python/samba/tests/krb5/conditional_ace_tests.py @@ -0,0 +1,5588 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd 2023 +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from collections import OrderedDict +from functools import partial +import re +from string import Formatter + +import ldb + +from samba import dsdb, ntstatus +from samba.dcerpc import claims, krb5pac, netlogon, security +from samba.ndr import ndr_pack, ndr_unpack +from samba.sd_utils import escaped_claim_id + +from samba.tests import DynamicTestCase, env_get_var_value +from samba.tests.krb5.authn_policy_tests import ( + AuditEvent, + AuditReason, + AuthnPolicyBaseTests, +) +from samba.tests.krb5.raw_testcase import RawKerberosTest +from samba.tests.krb5.rfc4120_constants import ( + KDC_ERR_BADOPTION, + KDC_ERR_GENERIC, + KDC_ERR_POLICY, + NT_PRINCIPAL, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +SidType = RawKerberosTest.SidType + +global_asn1_print = False +global_hexdump = False + + +# When used as a test outcome, indicates that the test can cause a Windows +# server to crash, and is to be run with caution. +CRASHES_WINDOWS = object() + + +class ConditionalAceBaseTests(AuthnPolicyBaseTests): + # Constants for group SID attributes. + default_attrs = security.SE_GROUP_DEFAULT_FLAGS + resource_attrs = default_attrs | security.SE_GROUP_RESOURCE + + aa_asserted_identity = ( + security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY) + service_asserted_identity = security.SID_SERVICE_ASSERTED_IDENTITY + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls._setup = False + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + if not self._setup: + samdb = self.get_samdb() + cls = type(self) + + # Create a machine account with which to perform FAST. + cls._mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + + # Create an account with which to perform SamLogon. + cls._mach_creds_ntlm = self._get_creds( + account_type=self.AccountType.USER, + ntlm=True) + + # Create some new groups. + + group0_name = self.get_new_username() + group0_dn = self.create_group(samdb, group0_name) + cls._group0_sid = self.get_objectSid(samdb, group0_dn) + + group1_name = self.get_new_username() + group1_dn = self.create_group(samdb, group1_name) + cls._group1_sid = self.get_objectSid(samdb, group1_dn) + + # Create machine accounts with which to perform FAST that belong to + # various arrangements of the groups. + + cls._member_of_both_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'member_of': (group0_dn, group1_dn)}) + + cls._member_of_one_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'member_of': (group1_dn,)}) + + cls._member_of_both_creds_ntlm = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'member_of': (group0_dn, group1_dn), + 'kerberos_enabled': False, + }) + + # Create some authentication silos. + cls._unenforced_silo = self.create_authn_silo(enforced=False) + cls._enforced_silo = self.create_authn_silo(enforced=True) + + # Create machine accounts with which to perform FAST that belong to + # the respective silos. + + cls._member_of_unenforced_silo = self._get_creds( + account_type=self.AccountType.COMPUTER, + assigned_silo=self._unenforced_silo, + cached=True) + self.add_to_group(str(self._member_of_unenforced_silo.get_dn()), + self._unenforced_silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + cls._member_of_enforced_silo = self._get_creds( + account_type=self.AccountType.COMPUTER, + assigned_silo=self._enforced_silo, + cached=True) + self.add_to_group(str(self._member_of_enforced_silo.get_dn()), + self._enforced_silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + cls._member_of_enforced_silo_ntlm = self._get_creds( + account_type=self.AccountType.USER, + assigned_silo=self._enforced_silo, + ntlm=True, + cached=True) + self.add_to_group(str(self._member_of_enforced_silo_ntlm.get_dn()), + self._enforced_silo.dn, + 'msDS-AuthNPolicySiloMembers', + expect_attr=False) + + # Create a couple of multi‐valued string claims for testing claim + # value comparisons. + + cls.claim0_attr = 'carLicense' + cls.claim0_id = self.get_new_username() + self.create_claim(cls.claim0_id, + enabled=True, + attribute=cls.claim0_attr, + single_valued=False, + source_type='AD', + for_classes=['computer', 'user'], + value_type=claims.CLAIM_TYPE_STRING) + + cls.claim1_attr = 'departmentNumber' + cls.claim1_id = self.get_new_username() + self.create_claim(cls.claim1_id, + enabled=True, + attribute=cls.claim1_attr, + single_valued=False, + source_type='AD', + for_classes=['computer', 'user'], + value_type=claims.CLAIM_TYPE_STRING) + + cls._setup = True + + # For debugging purposes. Prints out the SDDL representation of + # authentication policy conditions set by the Windows GUI. + def _print_authn_policy_sddl(self, policy_id): + policy_dn = self.get_authn_policies_dn() + policy_dn.add_child(f'CN={policy_id}') + + attrs = [ + 'msDS-ComputerAllowedToAuthenticateTo', + 'msDS-ServiceAllowedToAuthenticateFrom', + 'msDS-ServiceAllowedToAuthenticateTo', + 'msDS-UserAllowedToAuthenticateFrom', + 'msDS-UserAllowedToAuthenticateTo', + ] + + samdb = self.get_samdb() + res = samdb.search(policy_dn, scope=ldb.SCOPE_BASE, attrs=attrs) + self.assertEqual(1, len(res), + f'Authentication policy {policy_id} not found') + result = res[0] + + def print_sddl(attr): + sd = result.get(attr, idx=0) + if sd is None: + return + + sec_desc = ndr_unpack(security.descriptor, sd) + print(f'{attr}: {sec_desc.as_sddl()}') + + for attr in attrs: + print_sddl(attr) + + def sddl_array_from_sids(self, sids): + def sddl_from_sid_entry(sid_entry): + sid, _, _ = sid_entry + return f'SID({sid})' + + return f"{{{', '.join(map(sddl_from_sid_entry, sids))}}}" + + def allow_if(self, condition): + return f'O:SYD:(XA;;CR;;;WD;({condition}))' + + +@DynamicTestCase +class ConditionalAceTests(ConditionalAceBaseTests): + @classmethod + def setUpDynamicTestCases(cls): + FILTER = env_get_var_value('FILTER', allow_missing=True) + + # These operators are arranged so that each operator precedes its own + # affixes. + op_names = OrderedDict([ + ('!=', 'does not equal'), + ('!', 'not'), + ('&&', 'and'), + ('<=', 'is less than or equals'), + ('<', 'is less than'), + ('==', 'equals'), + ('>=', 'exceeds or equals'), + ('>', 'exceeds'), + ('Not_Any_of', 'matches none of'), + ('Any_of', 'matches any of'), + ('Not_Contains', 'does not contain'), + ('Contains', 'contains'), + ('Not_Member_of_Any', 'the user belongs to none of'), + ('Not_Device_Member_of_Any', 'the device belongs to none of'), # TODO: no test for this yet + ('Device_Member_of_Any', 'the device belongs to any of'), # TODO: no test for this yet + ('Not_Device_Member_of', 'the device does not belong to'), # TODO: no test for this yet + ('Device_Member_of', 'the device belongs to'), + ('Not_Exists', 'there does not exist'), + ('Exists', 'there exists'), + ('Member_of_Any', 'the user belongs to any of'), + ('Not_Member_of', 'the user does not belong to'), + ('Member_of', 'the user belongs to'), + ('||', 'or'), + ]) + + # This is a safety measure to ensure correct ordering of op_names + keys = list(op_names.keys()) + for i in range(len(keys)): + for j in range(i + 1, len(keys)): + if keys[i] in keys[j]: + raise AssertionError((keys[i], keys[j])) + + for case in cls.pac_claim_cases: + if len(case) == 3: + pac_claims, expression, outcome = case + claim_map = None + elif len(case) == 4: + pac_claims, expression, claim_map, outcome = case + else: + raise AssertionError( + f'found {len(case)} items in case, expected 3–4') + + expression_name = expression + for op, op_name in op_names.items(): + expression_name = expression_name.replace(op, op_name) + + name = f'{pac_claims}_{expression_name}' + + if claim_map is not None: + name += f'_{claim_map}' + + name = re.sub(r'\W+', '_', name) + if len(name) > 150: + name = f'{name[:125]}+{len(name) - 125}‐more' + + if FILTER and not re.search(FILTER, name): + continue + + cls.generate_dynamic_test('test_pac_claim_cmp', name, + pac_claims, expression, claim_map, + outcome) + + for case in cls.claim_against_claim_cases: + lhs, op, rhs, outcome = case + op_name = op_names[op] + + name = f'{lhs}_{op_name}_{rhs}' + + name = re.sub(r'\W+', '_', name) + if FILTER and not re.search(FILTER, name): + continue + + cls.generate_dynamic_test('test_cmp', name, + lhs, op, rhs, outcome) + + for case in cls.claim_against_literal_cases: + lhs, op, rhs, outcome = case + op_name = op_names[op] + + name = f'{lhs}_{op_name}_literal_{rhs}' + + name = re.sub(r'\W+', '_', name) + if FILTER and not re.search(FILTER, name): + continue + + cls.generate_dynamic_test('test_cmp', name, + lhs, op, rhs, outcome, True) + + def test_allowed_from_member_of_each(self): + # Create an authentication policy that allows accounts belonging to + # both groups. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;(Member_of ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account does not + # belong to both groups. + armor_tgt = self.get_tgt(self._member_of_one_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._member_of_both_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_member_of_any(self): + # Create an authentication policy that allows accounts belonging to + # either group. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;(Member_of_Any ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account belongs to + # neither group. + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._member_of_one_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_not_member_of_each(self): + # Create an authentication policy that allows accounts not belonging to + # both groups. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;(Not_Member_of ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account belongs to + # both groups. + armor_tgt = self.get_tgt(self._member_of_both_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._member_of_one_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_not_member_of_any(self): + # Create an authentication policy that allows accounts belonging to + # neither group. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;(Not_Member_of_Any ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account belongs to one + # of the groups. + armor_tgt = self.get_tgt(self._member_of_one_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_member_of_each_deny(self): + # Create an authentication policy that denies accounts belonging to + # both groups, and allows other accounts. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XD;;CR;;;WD;(Member_of ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))' + f'(A;;CR;;;WD)'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account belongs to + # both groups. + armor_tgt = self.get_tgt(self._member_of_both_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._member_of_one_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_member_of_any_deny(self): + # Create an authentication policy that denies accounts belonging to + # either group, and allows other accounts. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XD;;CR;;;WD;(Member_of_Any ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))' + f'(A;;CR;;;WD)'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account belongs to + # either group. + armor_tgt = self.get_tgt(self._member_of_one_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_not_member_of_each_deny(self): + # Create an authentication policy that denies accounts not belonging to + # both groups, and allows other accounts. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XD;;CR;;;WD;(Not_Member_of ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))' + f'(A;;CR;;;WD)'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account doesn’t belong + # to both groups. + armor_tgt = self.get_tgt(self._member_of_one_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._member_of_both_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_not_member_of_any_deny(self): + # Create an authentication policy that denies accounts belonging to + # neither group, and allows other accounts. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XD;;CR;;;WD;(Not_Member_of_Any ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))' + f'(A;;CR;;;WD)'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account belongs to + # neither group. + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._member_of_one_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_unenforced_silo_equals(self): + # Create an authentication policy that allows accounts belonging to the + # unenforced silo. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo == ' + f'"{self._unenforced_silo}"))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # As the policy is unenforced, the ‘ad://ext/AuthenticationSilo’ claim + # will not be present in the TGT, and the ACE will never allow access. + + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_unenforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_enforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + def test_allowed_from_enforced_silo_equals(self): + # Create an authentication policy that allows accounts belonging to the + # enforced silo. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo == ' + f'"{self._enforced_silo}"))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error if the machine account does not + # belong to the silo. + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_unenforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + # Otherwise, authentication should succeed. + armor_tgt = self.get_tgt(self._member_of_enforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_unenforced_silo_not_equals(self): + # Create an authentication policy that allows accounts not belonging to + # the unenforced silo. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo != ' + f'"{self._unenforced_silo}"))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication fails unless the account belongs to a silo + # other than the unenforced silo. + + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_unenforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_enforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_enforced_silo_not_equals(self): + # Create an authentication policy that allows accounts not belonging to + # the enforced silo. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo != ' + f'"{self._enforced_silo}"))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication always fails, as none of the machine + # accounts belong to a silo that is not the enforced one. (The + # unenforced silo doesn’t count, as it will never appear in a claim.) + + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_unenforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_enforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + def test_allowed_from_unenforced_silo_equals_deny(self): + # Create an authentication policy that denies accounts belonging to the + # unenforced silo, and allows other accounts. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XD;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo == ' + f'"{self._unenforced_silo}"))' + f'(A;;CR;;;WD)'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication fails unless the account belongs to a silo + # other than the unenforced silo. + + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_unenforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_enforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_enforced_silo_equals_deny(self): + # Create an authentication policy that denies accounts belonging to the + # enforced silo, and allows other accounts. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XD;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo == ' + f'"{self._enforced_silo}"))' + f'(A;;CR;;;WD)'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication always fails, as none of the machine + # accounts belong to a silo that is not the enforced one. (The + # unenforced silo doesn’t count, as it will never appear in a claim.) + + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_unenforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_enforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + def test_allowed_from_unenforced_silo_not_equals_deny(self): + # Create an authentication policy that denies accounts not belonging to + # the unenforced silo, and allows other accounts. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XD;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo != ' + f'"{self._unenforced_silo}"))' + f'(A;;CR;;;WD)'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication always fails, as the unenforced silo will + # never appear in a claim. + + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_unenforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_enforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + def test_allowed_from_enforced_silo_not_equals_deny(self): + # Create an authentication policy that denies accounts not belonging to + # the enforced silo, and allows other accounts. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XD;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo != ' + f'"{self._enforced_silo}"))' + f'(A;;CR;;;WD)'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication fails unless the account belongs to the + # enforced silo. + + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_unenforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + armor_tgt = self.get_tgt(self._member_of_enforced_silo) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_from_claim_equals_claim(self): + # Create a couple of claim types. + + claim0_id = self.get_new_username() + self.create_claim(claim0_id, + enabled=True, + attribute='carLicense', + single_valued=True, + source_type='AD', + for_classes=['computer'], + value_type=claims.CLAIM_TYPE_STRING) + + claim1_id = self.get_new_username() + self.create_claim(claim1_id, + enabled=True, + attribute='comment', + single_valued=True, + source_type='AD', + for_classes=['computer'], + value_type=claims.CLAIM_TYPE_STRING) + + # Create an authentication policy that allows accounts having the two + # claims be equal. + policy = self.create_authn_policy( + enforced=True, + user_allowed_from=( + f'O:SYD:(XA;;CR;;;WD;' + f'(@User.{claim0_id} == @User.{claim1_id}))'), + ) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + armor_tgt = self.get_tgt(self._mach_creds) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=KDC_ERR_POLICY) + + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'additional_details': ( + ('carLicense', 'foo'), + ('comment', 'foo'), + ), + }) + armor_tgt = self.get_tgt( + mach_creds, + expect_client_claims=True, + expected_client_claims={ + claim0_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + claim1_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': ('foo',), + }, + }) + self._get_tgt(client_creds, armor_tgt=armor_tgt, + expected_error=0) + + def test_allowed_to_client_equals(self): + client_claim_attr = 'carLicense' + client_claim_value = 'foo bar' + client_claim_values = client_claim_value, + + client_claim_id = self.get_new_username() + self.create_claim(client_claim_id, + enabled=True, + attribute=client_claim_attr, + single_valued=True, + source_type='AD', + for_classes=['user'], + value_type=claims.CLAIM_TYPE_STRING) + + # Create an authentication policy that allows authorization if the + # client has a particular claim value. + policy = self.create_authn_policy( + enforced=True, + computer_allowed_to=( + f'O:SYD:(XA;;CR;;;WD;' + f'((@User.{client_claim_id} == "{client_claim_value}")))'), + ) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + armor_tgt = self.get_tgt(self._mach_creds) + + # Create a user account without the claim value. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + # Show that obtaining a service ticket is denied. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=armor_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + # Create a user account with the claim value. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'additional_details': ( + (client_claim_attr, client_claim_values), + ), + }) + tgt = self.get_tgt( + client_creds, + expect_client_claims=True, + expected_client_claims={ + client_claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': client_claim_values, + }, + }) + # Show that obtaining a service ticket is allowed. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=armor_tgt) + + def test_allowed_to_device_equals(self): + device_claim_attr = 'carLicense' + device_claim_value = 'bar' + device_claim_values = device_claim_value, + + device_claim_id = self.get_new_username() + self.create_claim(device_claim_id, + enabled=True, + attribute=device_claim_attr, + single_valued=True, + source_type='AD', + for_classes=['computer'], + value_type=claims.CLAIM_TYPE_STRING) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER) + tgt = self.get_tgt(client_creds) + + # Create an authentication policy that allows authorization if the + # device has a particular claim value. + policy = self.create_authn_policy( + enforced=True, + computer_allowed_to=( + f'O:SYD:(XA;;CR;;;WD;' + f'(@Device.{device_claim_id} == "{device_claim_value}"))'), + ) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + armor_tgt = self.get_tgt(self._mach_creds) + # Show that obtaining a service ticket is denied when the claim value + # is not present. + self._tgs_req( + tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=armor_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + check_patypes=False) + + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'additional_details': ( + (device_claim_attr, device_claim_values), + ), + }) + armor_tgt = self.get_tgt( + mach_creds, + expect_client_claims=True, + expected_client_claims={ + device_claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': device_claim_values, + }, + }) + # Show that obtaining a service ticket is allowed when the claim value + # is present. + self._tgs_req(tgt, 0, client_creds, target_creds, + armor_tgt=armor_tgt) + + claim_against_claim_cases = [ + # If either side is missing, the result is unknown. + ((), '==', (), None), + ((), '!=', (), None), + ('a', '==', (), None), + ((), '==', 'b', None), + # Straightforward equality and inequality checks work. + ('foo', '==', 'foo', True), + ('foo', '==', 'bar', False), + ('foo', '!=', 'foo', False), + ('foo', '!=', 'bar', True), + # We can perform less‐than and greater‐than operations. + ('cat', '<', 'dog', True), + ('cat', '<=', 'dog', True), + ('cat', '>', 'dog', False), + ('cat', '>=', 'dog', False), + ('foo', '<=', 'foo', True), + ('foo', '>=', 'foo', True), + ('foo', '<', 'foo bar', True), + ('foo bar', '>', 'foo', True), + # String comparison is case‐sensitive. + ('foo bar', '==', 'Foo BAR', True), + ('foo bar', '==', 'FOO BAR', True), + ('ćàț', '==', 'ĆÀȚ', True), + ('ḽ', '==', 'Ḽ', True), + ('ⅸ', '==', 'Ⅸ', True), + ('ꙭ', '==', 'Ꙭ', True), + ('ⱦ', '==', 'Ⱦ', True), # Lowercased variant added in Unicode 5.0. + ('ԛԣ', '==', 'ԚԢ', True), # All added in Unicode 5.1. + ('foo', '<', 'foo', True), + ('ćàș', '<', 'ĆÀȚ', True), + ('cat', '<', 'ćàț', True), + # This is done by converting to UPPER CASE. Hence, both ‘A’ (U+41) and + # ‘a’ (U+61) compare less than ‘_’ (U+5F). + ('A', '<', '_', True), + ('a', '<', '_', True), + # But not all uppercased/lowercased pairs are considered to be equal in + # this way. + ('ß', '<', 'ẞ', True), + ('ß', '>', 'SS', True), + ('ⳬ', '>', 'Ⳬ', True), # Added in Unicode 5.2. + ('ʞ', '<', 'Ʞ', True), # Uppercased variant added in Unicode 6.0. + ('ʞ', '<', 'ʟ', True), # U+029E < U+029F < U+A7B0 (upper variant, Ʞ) + ('ꞧ', '>', 'Ꞧ', True), # Added in Unicode 6.0. + ('ɜ', '<', 'Ɜ', True), # Uppercased variant added in Unicode 7.0. + # + # Strings are compared as UTF‐16 code units, rather than as Unicode + # codepoints. So while you might expect ‘𐀀’ (U+10000) to compare + # greater than ‘豈’ (U+F900), it is actually considered to be the + # *smaller* of the pair. That is because it is encoded as a sequence of + # two code units, 0xd800 and 0xdc00, which combination compares less + # than the single code unit 0xf900. + ('ퟻ', '<', '𐀀', True), + ('𐀀', '<', '豈', True), + ('ퟻ', '<', '豈', True), + # Composites can be compared. + (('foo', 'bar'), '==', ('foo', 'bar'), True), + (('foo', 'bar'), '==', ('foo', 'baz'), False), + # The individual components don’t have to match in case. + (('foo', 'bar'), '==', ('FOO', 'BAR'), True), + # Nor must they match in order. + (('foo', 'bar'), '==', ('bar', 'foo'), True), + # Composites of different lengths compare unequal. + (('foo', 'bar'), '!=', 'foo', True), + (('foo', 'bar'), '!=', ('foo', 'bar', 'baz'), True), + # But composites don’t have a defined ordering, and aren’t considered + # greater or lesser than one another. + (('foo', 'bar'), '<', ('foo', 'bar'), None), + (('foo', 'bar'), '<=', ('foo', 'bar'), None), + (('foo', 'bar'), '>', ('foo', 'bar', 'baz'), None), + (('foo', 'bar'), '>=', ('foo', 'bar', 'baz'), None), + # We can test for containment. + (('foo', 'bar'), 'Contains', ('FOO'), True), + (('foo', 'bar'), 'Contains', ('foo', 'bar'), True), + (('foo', 'bar'), 'Not_Contains', ('foo', 'bar'), False), + (('foo', 'bar'), 'Contains', ('foo', 'bar', 'baz'), False), + (('foo', 'bar'), 'Not_Contains', ('foo', 'bar', 'baz'), True), + # We can test whether the operands have any elements in common. + ('foo', 'Any_of', 'foo', True), + (('foo', 'bar'), 'Any_of', 'BAR', True), + (('foo', 'bar'), 'Any_of', 'baz', False), + (('foo', 'bar'), 'Not_Any_of', 'baz', True), + (('foo', 'bar'), 'Any_of', ('bar', 'baz'), True), + (('foo', 'bar'), 'Not_Any_of', ('bar', 'baz'), False), + ] + + claim_against_literal_cases = [ + # String comparisons also work against literals. + ('foo bar', '==', '"foo bar"', True), + # Composites can be compared with literals. + ((), '==', '{{}}', None), + ('foo', '!=', '{{}}', True), + ('bar', '==', '{{"bar"}}', True), + (('apple', 'banana'), '==', '{{"APPLE", "BANANA"}}', True), + (('apple', 'banana'), '==', '{{"BANANA", "APPLE"}}', True), + (('apple', 'banana'), '==', '{{"apple", "banana", "apple"}}', False), + # We can test for containment. + ((), 'Contains', '{{}}', False), + ((), 'Not_Contains', '{{}}', True), + ((), 'Contains', '{{"foo"}}', None), + ((), 'Not_Contains', '{{"foo", "bar"}}', None), + ('foo', 'Contains', '{{}}', False), + ('bar', 'Contains', '{{"bar"}}', True), + (('foo', 'bar'), 'Contains', '{{"foo", "bar"}}', True), + (('foo', 'bar'), 'Contains', '{{"foo", "bar", "baz"}}', False), + # The right‐hand side of Contains or Not_Contains does not have to be a + # composite. + ('foo', 'Contains', '"foo"', True), + (('foo', 'bar'), 'Not_Contains', '"foo"', False), + # It’s fine if the right‐hand side contains duplicate elements. + (('foo', 'bar'), 'Contains', '{{"foo", "bar", "bar"}}', True), + # We can test whether the operands have any elements in common. + ((), 'Any_of', '{{}}', None), + ((), 'Not_Any_of', '{{}}', None), + ('foo', 'Any_of', '{{}}', False), + ('foo', 'Not_Any_of', '{{}}', True), + ('bar', 'Any_of', '{{"bar"}}', True), + (('foo', 'bar'), 'Any_of', '{{"bar", "baz"}}', True), + (('foo', 'bar'), 'Any_of', '{{"baz"}}', False), + # The right‐hand side of Any_of or Not_Any_of must be a composite. + ('foo', 'Any_of', '"foo"', None), + (('foo', 'bar'), 'Not_Any_of', '"baz"', None), + # A string won’t compare equal to a numeric literal. + ('42', '==', '"42"', True), + ('42', '==', '42', None), + # Nor can composites that mismatch in type be compared. + (('123', '456'), '==', '{{"123", "456"}}', True), + (('654', '321'), '==', '{{654, 321}}', None), + (('foo', 'bar'), 'Contains', '{{1, 2, 3}}', None), + ] + + def _test_cmp_with_args(self, lhs, op, rhs, outcome, rhs_is_literal=False): + # Construct a conditional ACE expression that evaluates to True if the + # two claim values are equal. + if rhs_is_literal: + self.assertIsInstance(rhs, str) + rhs = rhs.format(self=self) + expression = f'(@User.{self.claim0_id} {op} {rhs})' + else: + expression = f'(@User.{self.claim0_id} {op} @User.{self.claim1_id})' + + # Create an authentication policy that will allow authentication when + # the expression is true, and a second that will deny authentication in + # the same circumstance. By observing the results of authenticating + # against each of these policies in turn, we can determine whether the + # expression evaluates to a True, False, or Unknown value. + + allowed_sddl = f'O:SYD:(XA;;CR;;;WD;{expression})' + denied_sddl = f'O:SYD:(XD;;CR;;;WD;{expression})(A;;CR;;;WD)' + + allowed_policy = self.create_authn_policy( + enforced=True, + user_allowed_from=allowed_sddl) + denied_policy = self.create_authn_policy( + enforced=True, + user_allowed_from=denied_sddl) + + # Create a user account assigned to each policy. + allowed_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=allowed_policy) + denied_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=denied_policy) + + additional_details = () + if lhs: + additional_details += ((self.claim0_attr, lhs),) + if rhs and not rhs_is_literal: + additional_details += ((self.claim1_attr, rhs),) + + # Create a computer account with the provided attribute values. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'additional_details': additional_details}) + + def expected_values(val): + if isinstance(val, (str, bytes)): + return val, + + return val + + expected_client_claims = {} + if lhs: + expected_client_claims[self.claim0_id] = { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': expected_values(lhs), + } + if rhs and not rhs_is_literal: + expected_client_claims[self.claim1_id] = { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': expected_values(rhs), + } + + # Fetch the computer account’s TGT, and ensure it contains the claims. + armor_tgt = self.get_tgt( + mach_creds, + expect_client_claims=bool(expected_client_claims) or None, + expected_client_claims=expected_client_claims) + + # The first or the second authentication request is expected to succeed + # if the outcome is True or False, respectively. An Unknown outcome, + # represented by None, will result in a policy error in either case. + allowed_error = 0 if outcome is True else KDC_ERR_POLICY + denied_error = 0 if outcome is False else KDC_ERR_POLICY + + # Attempt to authenticate and ensure that we observe the expected + # results. + self._get_tgt(allowed_creds, armor_tgt=armor_tgt, + expected_error=allowed_error) + self._get_tgt(denied_creds, armor_tgt=armor_tgt, + expected_error=denied_error) + + pac_claim_cases = [ + # Test a very simple expression with various claims. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{non_empty_string}', claims.CLAIM_TYPE_STRING, ['foo bar']), + ]), + ], '{non_empty_string}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{zero_uint}', claims.CLAIM_TYPE_UINT64, [0]), + ]), + ], '{zero_uint}', False), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{nonzero_uint}', claims.CLAIM_TYPE_UINT64, [1]), + ]), + ], '{nonzero_uint}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{zero_uints}', claims.CLAIM_TYPE_UINT64, [0, 0]), + ]), + ], '{zero_uints}', KDC_ERR_GENERIC), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{zero_and_one_uint}', claims.CLAIM_TYPE_UINT64, [0, 1]), + ]), + ], '{zero_and_one_uint}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{one_and_zero_uint}', claims.CLAIM_TYPE_UINT64, [1, 0]), + ]), + ], '{one_and_zero_uint}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{zero_int}', claims.CLAIM_TYPE_INT64, [0]), + ]), + ], '{zero_int}', False), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{nonzero_int}', claims.CLAIM_TYPE_INT64, [1]), + ]), + ], '{nonzero_int}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{zero_ints}', claims.CLAIM_TYPE_INT64, [0, 0]), + ]), + ], '{zero_ints}', KDC_ERR_GENERIC), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{zero_and_one_int}', claims.CLAIM_TYPE_INT64, [0, 1]), + ]), + ], '{zero_and_one_int}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{one_and_zero_int}', claims.CLAIM_TYPE_INT64, [1, 0]), + ]), + ], '{one_and_zero_int}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{false_boolean}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '{false_boolean}', False), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{true_boolean}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '{true_boolean}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{false_booleans}', claims.CLAIM_TYPE_BOOLEAN, [0, 0]), + ]), + ], '{false_booleans}', KDC_ERR_GENERIC), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{false_and_true_boolean}', claims.CLAIM_TYPE_BOOLEAN, [0, 1]), + ]), + ], '{false_and_true_boolean}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{true_and_false_boolean}', claims.CLAIM_TYPE_BOOLEAN, [1, 0]), + ]), + ], '{true_and_false_boolean}', True), + # Test a basic comparison against a literal. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['foo bar']), + ]), + ], '{a} == "foo bar"', True), + # Claims can be compared against one another. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['foo bar']), + ('{b}', claims.CLAIM_TYPE_STRING, ['FOO BAR']), + ]), + ], '{a} == {b}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{b}', claims.CLAIM_TYPE_STRING, ['FOO', 'BAR', 'BAZ']), + ('{a}', claims.CLAIM_TYPE_STRING, ['foo', 'bar', 'baz']), + ]), + ], '{a} != {b}', False), + # Certificate claims are also valid. + ([ + (claims.CLAIMS_SOURCE_TYPE_CERTIFICATE, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['foo']), + ]), + ], '{a} == "foo"', True), + # Other claim source types are ignored. + ([ + (0, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['foo']), + ]), + ], '{a} == "foo"', None), + ([ + (3, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['foo']), + ]), + ], '{a} == "foo"', None), + # If multiple claims have the same ID, the *last* one takes precedence. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['this is not the value…']), + ('{a}', claims.CLAIM_TYPE_STRING, ['…nor is this…']), + ]), + (claims.CLAIMS_SOURCE_TYPE_CERTIFICATE, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['…and this isn’t either.']), + ]), + (claims.CLAIMS_SOURCE_TYPE_CERTIFICATE, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['here’s the actual value!']), + ]), + (3, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['this is a red herring.']), + ]), + ], '{a} == "here’s the actual value!"', True), + # Claim values can be empty. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{empty_claim_string}', claims.CLAIM_TYPE_STRING, []), + ]), + ], '{empty_claim_string} != "foo bar"', None), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{empty_claim_boolean}', claims.CLAIM_TYPE_BOOLEAN, []), + ]), + ], 'Exists {empty_claim_boolean}', None), + # Test unsigned integer equality. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_UINT64, [42]), + ]), + ], '{a} == 42', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_UINT64, [0]), + ]), + ], '{a} == 3', False), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_UINT64, [1, 2, 3]), + ]), + ], '{a} == {{1, 2, 3}}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_UINT64, [4, 5, 6]), + ]), + ], '{a} != {{1, 2, 3}}', True), + # Test unsigned integer comparison. Ensure we don’t run into any + # integer overflow issues. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_UINT64, [1 << 32]), + ]), + ], '{a} > 0', True), + # Test signed integer comparisons. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_INT64, [42]), + ]), + ], '{a} == 42', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_INT64, [42 << 32]), + ]), + ], f'{{a}} == {42 << 32}', True), + # Test boolean claims. Be careful! Windows will *crash* if you send it + # claims that aren’t real booleans (not 0 or 1). I doubt Microsoft will + # consider this a security issue though. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [2]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [3]), + ]), + ], '{a} == {b}', (None, CRASHES_WINDOWS)), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '{a} == {b}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '{a} == 42', None), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '{a} && {b}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '{a} && {b}', False), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '{a} && {b}', False), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '{a} || {b}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '{a} || {b}', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '{a} || {b}', False), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '!({a})', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '!(!(!(!({a}))))', False), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '!({a} && {a})', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '{a} && !({b} || {b})', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '!({a}) || !({a})', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [0]), + ]), + ], '{a} && !({b})', None), + # Expressions containing the ‘not’ operator are occasionally evaluated + # inconsistently, as evidenced here. ‘a || !a’ evaluates to ‘unknown’… + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '{a} || !({a})', None), + # …but ‘!a || a’ — the same expression, just with the operands switched + # round — evaluates to ‘true’. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '!({a}) || {a}', True), + # This inconsistency is not observed with other boolean expressions, + # such as ‘a || a’. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '{a} || ({a} || {a})', True), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{b}', claims.CLAIM_TYPE_BOOLEAN, [1]), + ]), + ], '({b} || {b}) || {b}', True), + # Test a very large claim. Much larger than this, and + # conditional_ace_encode_binary() will refuse to encode the conditions. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{large_claim}', claims.CLAIM_TYPE_STRING, ['z' * 4900]), + ]), + ], f'{{large_claim}} == "{"z" * 4900}"', True), + # Test an even larger claim. Windows does not appear to like receiving + # a claim this large. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{larger_claim}', claims.CLAIM_TYPE_STRING, ['z' * 100000]), + ]), + ], '{larger_claim} > "z"', (True, CRASHES_WINDOWS)), + # Test a great number of claims. Windows does not appear to like + # receiving this many claims. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{many_claims}', claims.CLAIM_TYPE_UINT64, + list(range(0, 100000))), + ]), + ], '{many_claims} Any_of "99999"', (True, CRASHES_WINDOWS)), + # Test a claim with a very long name. Much larger than this, and + # conditional_ace_encode_binary() will refuse to encode the conditions. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{long_name}', claims.CLAIM_TYPE_STRING, ['a']), + ]), + ], '{long_name} == "a"', {'long_name': 'z' * 4900}, True), + # Test attribute name escaping. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{escaped_claim}', claims.CLAIM_TYPE_STRING, ['claim value']), + ]), + ], '{escaped_claim} == "claim value"', + {'escaped_claim': '(:foo:! /&/ :bar:!)'}, True), + # Test a claim whose name consists entirely of dots. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{dotty_claim}', claims.CLAIM_TYPE_STRING, ['a']), + ]), + ], '{dotty_claim} == "a"', {'dotty_claim': '...'}, True), + # Test a claim whose name consists of the first thousand non‐zero + # Unicode codepoints. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{1000_unicode}', claims.CLAIM_TYPE_STRING, ['a']), + ]), + ], '{1000_unicode} == "a"', + {'1000_unicode': ''.join(map(chr, range(1, 1001)))}, True), + # Test a claim whose name consists of some higher Unicode codepoints, + # including non‐BMP ones. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{higher_unicode}', claims.CLAIM_TYPE_STRING, ['a']), + ]), + ], '{higher_unicode} == "a"', + {'higher_unicode': ''.join(map(chr, range(0xfe00, 0x10800)))}, True), + # Duplicate claim values are not allowed… + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_INT64, [42, 42, 42]), + ]), + ], '{a} == {a}', KDC_ERR_GENERIC), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_UINT64, [42, 42]), + ]), + ], '{a} == {a}', KDC_ERR_GENERIC), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['foo', 'foo']), + ]), + ], '{a} == {a}', KDC_ERR_GENERIC), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_STRING, ['FOO', 'foo']), + ]), + ], '{a} == {a}', KDC_ERR_GENERIC), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{a}', claims.CLAIM_TYPE_BOOLEAN, [0, 0]), + ]), + ], '{a} == {a}', KDC_ERR_GENERIC), + # …but it’s OK if duplicate values are spread across multiple claim + # entries. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{dup}', claims.CLAIM_TYPE_STRING, ['foo']), + ('{dup}', claims.CLAIM_TYPE_STRING, ['foo']), + ]), + (claims.CLAIMS_SOURCE_TYPE_CERTIFICATE, [ + ('{dup}', claims.CLAIM_TYPE_UINT64, [42]), + ('{dup}', claims.CLAIM_TYPE_UINT64, [42]), + ]), + (claims.CLAIMS_SOURCE_TYPE_CERTIFICATE, [ + ('{dup}', claims.CLAIM_TYPE_STRING, ['foo']), + ('{dup}', claims.CLAIM_TYPE_STRING, ['foo']), + ('{dup}', claims.CLAIM_TYPE_STRING, ['foo', 'bar']), + ('{dup}', claims.CLAIM_TYPE_STRING, ['foo', 'bar']), + ]), + ], '{dup} == {dup}', True), + # Test invalid claim types. Be careful! Windows will *crash* if you + # send it invalid claim types. I doubt Microsoft will consider this a + # security issue though. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{invalid_sid}', 5, []), + ]), + ], '{invalid_sid} == {invalid_sid}', (None, CRASHES_WINDOWS)), + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{invalid_octet_string}', 16, []), + ]), + ], '{invalid_octet_string} == {invalid_octet_string}', (None, CRASHES_WINDOWS)), + # Sending an empty string will crash Windows. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{empty_string}', claims.CLAIM_TYPE_STRING, ['']), + ]), + ], '{empty_string}', (None, CRASHES_WINDOWS)), + # But sending empty arrays is OK. + ([ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + ('{empty_array}', claims.CLAIM_TYPE_INT64, []), + ('{empty_array}', claims.CLAIM_TYPE_UINT64, []), + ('{empty_array}', claims.CLAIM_TYPE_BOOLEAN, []), + ('{empty_array}', claims.CLAIM_TYPE_STRING, []), + ]), + ], '{empty_array}', None), + ] + + def _test_pac_claim_cmp_with_args(self, + pac_claims, + expression, + claim_map, + outcome): + self.assertIsInstance(expression, str) + + try: + outcome, crashes_windows = outcome + self.assertIs(crashes_windows, CRASHES_WINDOWS) + if not self.crash_windows: + self.skipTest('test crashes Windows servers') + except TypeError: + self.assertIsNot(outcome, CRASHES_WINDOWS) + + if claim_map is None: + claim_map = {} + + claim_ids = {} + + def get_claim_id(claim_name): + claim = claim_ids.get(claim_name) + if claim is None: + claim = claim_map.pop(claim_name, None) + if claim is None: + claim = self.get_new_username() + + claim_ids[claim_name] = claim + + return claim + + def formatted_claim_expression(expr): + formatter = Formatter() + result = [] + + for literal_text, field_name, format_spec, conversion in ( + formatter.parse(expr)): + self.assertFalse(format_spec, + f'format specifier ({format_spec}) should ' + f'not be specified') + self.assertFalse(conversion, + f'conversion ({conversion}) should not be ' + 'specified') + + result.append(literal_text) + + if field_name is not None: + self.assertTrue(field_name, + 'a field name should be specified') + + claim_id = get_claim_id(field_name) + claim_id = escaped_claim_id(claim_id) + result.append(f'@User.{claim_id}') + + return ''.join(result) + + # Construct the conditional ACE expression. + expression = formatted_claim_expression(expression) + + self.assertFalse(claim_map, 'unused claim mapping(s) remain') + + # Create an authentication policy that will allow authentication when + # the expression is true, and a second that will deny authentication in + # the same circumstance. By observing the results of authenticating + # against each of these policies in turn, we can determine whether the + # expression evaluates to a True, False, or Unknown value. + + allowed_sddl = f'O:SYD:(XA;;CR;;;WD;({expression}))' + denied_sddl = f'O:SYD:(XD;;CR;;;WD;({expression}))(A;;CR;;;WD)' + + allowed_policy = self.create_authn_policy( + enforced=True, + user_allowed_from=allowed_sddl) + denied_policy = self.create_authn_policy( + enforced=True, + user_allowed_from=denied_sddl) + + # Create a user account assigned to each policy. + allowed_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=allowed_policy) + denied_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=denied_policy) + + # Create a computer account. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + + def expected_values(val): + if isinstance(val, (str, bytes)): + return val, + + return val + + # Fetch the computer account’s TGT. + armor_tgt = self.get_tgt(mach_creds) + + if pac_claims: + # Replace the claims in the PAC with our own. + armor_tgt = self.modified_ticket( + armor_tgt, + modify_pac_fn=partial(self.set_pac_claims, + client_claims=pac_claims, + claim_ids=claim_ids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # The first or the second authentication request is expected to succeed + # if the outcome is True or False, respectively. An Unknown outcome, + # represented by None, will result in a policy error in either case. + if outcome is True: + allowed_error, denied_error = 0, KDC_ERR_POLICY + elif outcome is False: + allowed_error, denied_error = KDC_ERR_POLICY, 0 + elif outcome is None: + allowed_error, denied_error = KDC_ERR_POLICY, KDC_ERR_POLICY + else: + allowed_error, denied_error = outcome, outcome + + # Attempt to authenticate and ensure that we observe the expected + # results. + self._get_tgt(allowed_creds, armor_tgt=armor_tgt, + expected_error=allowed_error) + self._get_tgt(denied_creds, armor_tgt=armor_tgt, + expected_error=denied_error) + + def test_rbcd_without_aa_asserted_identity(self): + service_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Member_of SID({self.aa_asserted_identity})', + service_sids=service_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Member_of SID({self.aa_asserted_identity})', + service_sids=service_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_with_aa_asserted_identity(self): + service_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = service_sids | { + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Member_of SID({self.aa_asserted_identity})', + service_sids=service_sids, + expected_groups=expected_groups) + + self._rbcd(target_policy=f'Member_of SID({self.aa_asserted_identity})', + service_sids=service_sids, + expected_groups=expected_groups) + + def test_rbcd_without_service_asserted_identity(self): + service_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Member_of SID({self.service_asserted_identity})', + service_sids=service_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Member_of SID({self.service_asserted_identity})', + service_sids=service_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_with_service_asserted_identity(self): + service_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The Application Authority Asserted Identity SID has replaced the + # Service Asserted Identity SID. + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Member_of SID({self.service_asserted_identity})', + service_sids=service_sids, + expected_groups=expected_groups) + + self._rbcd(target_policy=f'Member_of SID({self.service_asserted_identity})', + service_sids=service_sids, + expected_groups=expected_groups) + + def test_rbcd_without_claims_valid(self): + service_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Member_of SID({security.SID_CLAIMS_VALID})', + service_sids=service_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Member_of SID({security.SID_CLAIMS_VALID})', + service_sids=service_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_with_claims_valid(self): + service_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = service_sids | { + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Member_of SID({security.SID_CLAIMS_VALID})', + service_sids=service_sids, + expected_groups=expected_groups) + + self._rbcd(target_policy=f'Member_of SID({security.SID_CLAIMS_VALID})', + service_sids=service_sids, + expected_groups=expected_groups) + + def test_rbcd_without_compounded_authentication(self): + service_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + service_sids=service_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + service_sids=service_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_with_compounded_authentication(self): + service_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + service_sids=service_sids, + expected_groups=expected_groups) + + self._rbcd(target_policy=f'Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + service_sids=service_sids, + expected_groups=expected_groups) + + def test_rbcd_client_without_aa_asserted_identity(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Member_of SID({self.aa_asserted_identity})', + client_sids=client_sids) + + self._rbcd(target_policy=f'Member_of SID({self.aa_asserted_identity})', + client_sids=client_sids) + + def test_rbcd_client_with_aa_asserted_identity(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Member_of SID({self.aa_asserted_identity})', + client_sids=client_sids, + expected_groups=client_sids) + + self._rbcd(target_policy=f'Member_of SID({self.aa_asserted_identity})', + client_sids=client_sids, + expected_groups=client_sids) + + def test_rbcd_client_without_service_asserted_identity(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Member_of SID({self.service_asserted_identity})', + client_sids=client_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Member_of SID({self.service_asserted_identity})', + client_sids=client_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_client_with_service_asserted_identity(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Not_Member_of SID({self.service_asserted_identity})', + client_sids=client_sids, + expected_groups=client_sids) + + self._rbcd(target_policy=f'Not_Member_of SID({self.service_asserted_identity})', + client_sids=client_sids, + expected_groups=client_sids) + + def test_rbcd_client_without_claims_valid(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Member_of SID({security.SID_CLAIMS_VALID})', + client_sids=client_sids) + + self._rbcd(target_policy=f'Member_of SID({security.SID_CLAIMS_VALID})', + client_sids=client_sids) + + def test_rbcd_client_with_claims_valid(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Member_of SID({security.SID_CLAIMS_VALID})', + client_sids=client_sids, + expected_groups=client_sids) + + self._rbcd(target_policy=f'Member_of SID({security.SID_CLAIMS_VALID})', + client_sids=client_sids, + expected_groups=client_sids) + + def test_rbcd_client_without_compounded_authentication(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + client_sids=client_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + client_sids=client_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_client_with_compounded_authentication(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Not_Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + client_sids=client_sids, + expected_groups=client_sids) + + self._rbcd(target_policy=f'Not_Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + client_sids=client_sids, + expected_groups=client_sids) + + def test_rbcd_device_without_aa_asserted_identity(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Device_Member_of SID({self.aa_asserted_identity})', + device_sids=device_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Device_Member_of SID({self.aa_asserted_identity})', + device_sids=device_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_device_without_aa_asserted_identity_not_memberof(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Not_Device_Member_of SID({self.aa_asserted_identity})', + device_sids=device_sids) + + self._rbcd(target_policy=f'Not_Device_Member_of SID({self.aa_asserted_identity})', + device_sids=device_sids) + + def test_rbcd_device_with_aa_asserted_identity(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Device_Member_of SID({self.aa_asserted_identity})', + device_sids=device_sids) + + self._rbcd(target_policy=f'Device_Member_of SID({self.aa_asserted_identity})', + device_sids=device_sids) + + def test_rbcd_device_without_service_asserted_identity(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Device_Member_of SID({self.service_asserted_identity})', + device_sids=device_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Device_Member_of SID({self.service_asserted_identity})', + device_sids=device_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_device_with_service_asserted_identity(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Device_Member_of SID({self.service_asserted_identity})', + device_sids=device_sids) + + self._rbcd(target_policy=f'Device_Member_of SID({self.service_asserted_identity})', + device_sids=device_sids) + + def test_rbcd_device_without_claims_valid(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Device_Member_of SID({security.SID_CLAIMS_VALID})', + device_sids=device_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Device_Member_of SID({security.SID_CLAIMS_VALID})', + device_sids=device_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_device_with_claims_valid(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Device_Member_of SID({security.SID_CLAIMS_VALID})', + device_sids=device_sids) + + self._rbcd(target_policy=f'Device_Member_of SID({security.SID_CLAIMS_VALID})', + device_sids=device_sids) + + def test_rbcd_device_without_compounded_authentication(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._rbcd(f'Device_Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + device_sids=device_sids, + code=KDC_ERR_BADOPTION, + status=ntstatus.NT_STATUS_UNSUCCESSFUL, + edata=self.expect_padata_outer) + + self._rbcd(target_policy=f'Device_Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + device_sids=device_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_rbcd_device_with_compounded_authentication(self): + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs), + } + + self._rbcd(f'Device_Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + device_sids=device_sids) + + self._rbcd(target_policy=f'Device_Member_of SID({security.SID_COMPOUNDED_AUTHENTICATION})', + device_sids=device_sids) + + def test_rbcd(self): + self._rbcd('Member_of SID({service_sid})') + + def test_rbcd_device_from_rodc(self): + self._rbcd('Member_of SID({service_sid})', + device_from_rodc=True, + code=(0, CRASHES_WINDOWS)) + + def test_rbcd_service_from_rodc(self): + self._rbcd('Member_of SID({service_sid})', + service_from_rodc=True) + + def test_rbcd_device_and_service_from_rodc(self): + self._rbcd('Member_of SID({service_sid})', + service_from_rodc=True, + device_from_rodc=True, + code=(0, CRASHES_WINDOWS)) + + def test_rbcd_client_from_rodc(self): + self._rbcd('Member_of SID({service_sid})', + client_from_rodc=True) + + def test_rbcd_client_and_device_from_rodc(self): + self._rbcd('Member_of SID({service_sid})', + client_from_rodc=True, + device_from_rodc=True, + code=(0, CRASHES_WINDOWS)) + + def test_rbcd_client_and_service_from_rodc(self): + self._rbcd('Member_of SID({service_sid})', + client_from_rodc=True, + service_from_rodc=True) + + def test_rbcd_all_from_rodc(self): + self._rbcd('Member_of SID({service_sid})', + client_from_rodc=True, + service_from_rodc=True, + device_from_rodc=True, + code=(0, CRASHES_WINDOWS)) + + def test_delegating_proxy_in_world_group_rbcd(self): + self._check_delegating_proxy_in_group_rbcd(security.SID_WORLD) + + def test_delegating_proxy_in_network_group_rbcd(self): + self._check_delegating_proxy_not_in_group_rbcd(security.SID_NT_NETWORK) + + def test_delegating_proxy_in_authenticated_users_rbcd(self): + self._check_delegating_proxy_in_group_rbcd( + security.SID_NT_AUTHENTICATED_USERS) + + def test_delegating_proxy_in_aa_asserted_identity_rbcd(self): + self._check_delegating_proxy_in_group_rbcd( + security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY) + + def test_delegating_proxy_in_service_asserted_identity_rbcd(self): + self._check_delegating_proxy_not_in_group_rbcd( + security.SID_SERVICE_ASSERTED_IDENTITY) + + def test_delegating_proxy_in_compounded_authentication_rbcd(self): + self._check_delegating_proxy_not_in_group_rbcd( + security.SID_COMPOUNDED_AUTHENTICATION) + + def test_delegating_proxy_in_claims_valid_rbcd(self): + self._check_delegating_proxy_in_group_rbcd(security.SID_CLAIMS_VALID) + + def test_device_in_world_group_rbcd(self): + self._check_device_in_group_rbcd(security.SID_WORLD) + + def test_device_in_network_group_rbcd(self): + self._check_device_not_in_group_rbcd(security.SID_NT_NETWORK) + + def test_device_in_authenticated_users_rbcd(self): + self._check_device_in_group_rbcd(security.SID_NT_AUTHENTICATED_USERS) + + def test_device_in_aa_asserted_identity_rbcd(self): + self._check_device_in_group_rbcd( + security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY) + + def test_device_in_service_asserted_identity_rbcd(self): + self._check_device_not_in_group_rbcd( + security.SID_SERVICE_ASSERTED_IDENTITY) + + def test_device_in_compounded_authentication_rbcd(self): + self._check_device_not_in_group_rbcd( + security.SID_COMPOUNDED_AUTHENTICATION) + + def test_device_in_claims_valid_rbcd(self): + self._check_device_in_group_rbcd(security.SID_CLAIMS_VALID) + + def _check_delegating_proxy_in_group_rbcd(self, group): + self._check_membership_rbcd(group, expect_in_group=True) + + def _check_delegating_proxy_not_in_group_rbcd(self, group): + self._check_membership_rbcd(group, expect_in_group=False) + + def _check_device_in_group_rbcd(self, group): + self._check_membership_rbcd(group, expect_in_group=True, device=True) + + def _check_device_not_in_group_rbcd(self, group): + self._check_membership_rbcd(group, expect_in_group=False, device=True) + + def _check_membership_rbcd(self, + group, + *, + expect_in_group, + device=False): + """Test that authentication succeeds or fails when the delegating proxy + is required to belong to a certain group. + """ + + sddl_op = 'Device_Member_of' if device else 'Member_of' + + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'service'}) + service_tgt = self.get_tgt(service_creds) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + domain_sid_str = samdb.get_domain_sid() + domain_sid = security.dom_sid(domain_sid_str) + + # Require the principal to belong to a certain group. + in_group_sddl = self.allow_if(f'{sddl_op} {{SID({group})}}') + in_group_descriptor = security.descriptor.from_sddl(in_group_sddl, + domain_sid) + + # Create a target account that allows RBCD if the principal belongs to + # the group. + in_group_target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'additional_details': ( + ('msDS-AllowedToActOnBehalfOfOtherIdentity', + ndr_pack(in_group_descriptor)), + ), + }) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + in_group_target_key = self.TicketDecryptionKey_from_creds( + in_group_target_creds) + in_group_target_etypes = in_group_target_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + pac_options = '1001' # supports claims, RBCD + + success_result = 0, None, None + failure_result = ( + KDC_ERR_BADOPTION, + ntstatus.NT_STATUS_UNSUCCESSFUL, + self.expect_padata_outer, + ) + + code, status, expect_edata = (success_result if expect_in_group + else failure_result) + + # Test whether obtaining a service ticket with RBCD is allowed. + self._tgs_req(service_tgt, + code, + service_creds, + in_group_target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options=pac_options, + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=in_group_target_key, + expected_sid=client_sid, + expected_supported_etypes=in_group_target_etypes, + expected_proxy_target=in_group_target_creds.get_spn(), + expected_transited_services=expected_transited_services, + expected_status=status, + expect_edata=expect_edata) + + effective_client_creds = service_creds if code else client_creds + self.check_tgs_log(effective_client_creds, in_group_target_creds, + checked_creds=service_creds, + status=status) + + # Require the principal not to belong to a certain group. + not_in_group_sddl = self.allow_if(f'Not_{sddl_op} {{SID({group})}}') + not_in_group_descriptor = security.descriptor.from_sddl( + not_in_group_sddl, domain_sid) + + # Create a target account that allows RBCD if the principal does not + # belong to the group. + not_in_group_target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'additional_details': ( + ('msDS-AllowedToActOnBehalfOfOtherIdentity', + ndr_pack(not_in_group_descriptor)), + ), + }) + + not_in_group_target_key = self.TicketDecryptionKey_from_creds( + not_in_group_target_creds) + not_in_group_target_etypes = ( + not_in_group_target_creds.tgs_supported_enctypes) + + code, status, expect_edata = (failure_result if expect_in_group + else success_result) + + # Test whether obtaining a service ticket with RBCD is allowed. + self._tgs_req(service_tgt, + code, + service_creds, + not_in_group_target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options=pac_options, + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=not_in_group_target_key, + expected_sid=client_sid, + expected_supported_etypes=not_in_group_target_etypes, + expected_proxy_target=not_in_group_target_creds.get_spn(), + expected_transited_services=expected_transited_services, + expected_status=status, + expect_edata=expect_edata) + + effective_client_creds = service_creds if code else client_creds + self.check_tgs_log(effective_client_creds, not_in_group_target_creds, + checked_creds=service_creds, + status=status) + + def _rbcd(self, + rbcd_expression=None, + *, + code=0, + status=None, + event=AuditEvent.OK, + reason=AuditReason.NONE, + edata=False, + target_policy=None, + client_from_rodc=False, + service_from_rodc=False, + device_from_rodc=False, + client_sids=None, + client_claims=None, + service_sids=None, + service_claims=None, + device_sids=None, + device_claims=None, + expected_groups=None, + expected_claims=None): + try: + code, crashes_windows = code + self.assertIs(crashes_windows, CRASHES_WINDOWS) + if not self.crash_windows: + self.skipTest('test crashes Windows servers') + except TypeError: + self.assertIsNot(code, CRASHES_WINDOWS) + + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + domain_sid_str = samdb.get_domain_sid() + domain_sid = security.dom_sid(domain_sid_str) + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'allowed_replication_mock': client_from_rodc, + 'revealed_to_mock_rodc': client_from_rodc, + }) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + checksum_key = self.get_krbtgt_checksum_key() + + if client_from_rodc or service_from_rodc or device_from_rodc: + rodc_krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + rodc_krbtgt_key = self.TicketDecryptionKey_from_creds(rodc_krbtgt_creds) + rodc_checksum_key = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: rodc_krbtgt_key, + } + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'allowed_replication_mock': device_from_rodc, + 'revealed_to_mock_rodc': device_from_rodc, + }) + mach_tgt = self.get_tgt(mach_creds) + device_modify_pac_fn = [] + if device_sids is not None: + device_modify_pac_fn.append(partial(self.set_pac_sids, + new_sids=device_sids)) + if device_claims is not None: + device_modify_pac_fn.append(partial(self.set_pac_claims, + client_claims=device_claims)) + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=device_modify_pac_fn, + new_ticket_key=rodc_krbtgt_key if device_from_rodc else None, + checksum_keys=rodc_checksum_key if device_from_rodc else checksum_key) + + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'id': 1, + 'allowed_replication_mock': service_from_rodc, + 'revealed_to_mock_rodc': service_from_rodc, + }) + service_tgt = self.get_tgt(service_creds) + + service_modify_pac_fn = [] + if service_sids is not None: + service_modify_pac_fn.append(partial(self.set_pac_sids, + new_sids=service_sids)) + if service_claims is not None: + service_modify_pac_fn.append(partial(self.set_pac_claims, + client_claims=service_claims)) + service_tgt = self.modified_ticket( + service_tgt, + modify_pac_fn=service_modify_pac_fn, + new_ticket_key=rodc_krbtgt_key if service_from_rodc else None, + checksum_keys=rodc_checksum_key if service_from_rodc else checksum_key) + + if target_policy is None: + policy = None + assigned_policy = None + else: + sddl = f'O:SYD:(XA;;CR;;;WD;({target_policy.format(service_sid=service_creds.get_sid())}))' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=sddl) + assigned_policy = str(policy.dn) + + if rbcd_expression is not None: + sddl = f'O:SYD:(XA;;CR;;;WD;({rbcd_expression.format(service_sid=service_creds.get_sid())}))' + else: + sddl = 'O:SYD:(A;;CR;;;WD)' + descriptor = security.descriptor.from_sddl(sddl, domain_sid) + descriptor = ndr_pack(descriptor) + + # Create a target account with the assigned policy. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'assigned_policy': assigned_policy, + 'additional_details': ( + ('msDS-AllowedToActOnBehalfOfOtherIdentity', descriptor), + ), + }) + + client_service_tkt = self.get_service_ticket( + client_tgt, + service_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + client_modify_pac_fn = [] + if client_sids is not None: + client_modify_pac_fn.append(partial(self.set_pac_sids, + new_sids=client_sids)) + if client_claims is not None: + client_modify_pac_fn.append(partial(self.set_pac_claims, + client_claims=client_claims)) + client_service_tkt = self.modified_ticket(client_service_tkt, + modify_pac_fn=client_modify_pac_fn, + checksum_keys=rodc_checksum_key if client_from_rodc else checksum_key) + + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + service_name = service_creds.get_username() + if service_name[-1] == '$': + service_name = service_name[:-1] + expected_transited_services = [ + f'host/{service_name}@{service_creds.get_realm()}' + ] + + expected_groups = self.map_sids(expected_groups, None, domain_sid_str) + + # Show that obtaining a service ticket with RBCD is allowed. + self._tgs_req(service_tgt, code, service_creds, target_creds, + armor_tgt=mach_tgt, + kdc_options=kdc_options, + pac_options='1001', # supports claims, RBCD + expected_cname=client_cname, + expected_account_name=client_username, + additional_ticket=client_service_tkt, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_groups=expected_groups, + expect_client_claims=bool(expected_claims) or None, + expected_client_claims=expected_claims, + expected_supported_etypes=target_etypes, + expected_proxy_target=target_creds.get_spn(), + expected_transited_services=expected_transited_services, + expected_status=status, + expect_edata=edata) + + if code: + effective_client_creds = service_creds + else: + effective_client_creds = client_creds + + self.check_tgs_log(effective_client_creds, target_creds, + policy=policy, + checked_creds=service_creds, + status=status, + event=event, + reason=reason) + + def test_tgs_claims_valid_missing(self): + """Test that the Claims Valid SID is not added to the PAC when + performing a TGS‐REQ.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_claims_valid_missing_from_rodc(self): + """Test that the Claims Valid SID *is* added to the PAC when + performing a TGS‐REQ with an RODC‐issued TGT.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = client_sids | { + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_from_rodc=True, + client_sids=client_sids, + expected_groups=expected_groups) + + def test_tgs_aa_asserted_identity(self): + """Test performing a TGS‐REQ with the Authentication Identity Asserted + Identity SID present.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_aa_asserted_identity_no_attrs(self): + """Test performing a TGS‐REQ with the Authentication Identity Asserted + Identity SID present, albeit without any attributes.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # Put the Asserted Identity SID in the PAC without any flags set. + (self.aa_asserted_identity, SidType.EXTRA_SID, 0), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_aa_asserted_identity_from_rodc(self): + """Test that the Authentication Identity Asserted Identity SID in an + RODC‐issued PAC is preserved when performing a TGS‐REQ.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_from_rodc=True, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_aa_asserted_identity_from_rodc_no_attrs_from_rodc(self): + """Test that the Authentication Identity Asserted Identity SID without + attributes in an RODC‐issued PAC is preserved when performing a + TGS‐REQ.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # Put the Asserted Identity SID in the PAC without any flags set. + (self.aa_asserted_identity, SidType.EXTRA_SID, 0), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The SID in the resulting PAC has the default attributes. + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_from_rodc=True, + client_sids=client_sids, + expected_groups=expected_groups) + + def test_tgs_compound_authentication(self): + """Test performing a TGS‐REQ with the Compounded Authentication SID + present.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_compound_authentication_from_rodc(self): + """Test that the Compounded Authentication SID in an + RODC‐issued PAC is not preserved when performing a TGS‐REQ.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_from_rodc=True, + client_sids=client_sids, + expected_groups=expected_groups) + + def test_tgs_asserted_identity_missing(self): + """Test that the Authentication Identity Asserted Identity SID is not + added to the PAC when performing a TGS‐REQ.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_asserted_identity_missing_from_rodc(self): + """Test that the Authentication Identity Asserted Identity SID is not + added to an RODC‐issued PAC when performing a TGS‐REQ.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_from_rodc=True, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_service_asserted_identity(self): + """Test performing a TGS‐REQ with the Service Asserted Identity SID + present.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_service_asserted_identity_from_rodc(self): + """Test that the Service Asserted Identity SID in an + RODC‐issued PAC is not preserved when performing a TGS‐REQ.""" + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # Don’t expect the Service Asserted Identity SID. + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(use_fast=False, + client_from_rodc=True, + client_sids=client_sids, + expected_groups=expected_groups) + + def test_tgs_without_aa_asserted_identity(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({self.aa_asserted_identity})', + client_sids=client_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_aa_asserted_identity_client_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({self.aa_asserted_identity})', + client_from_rodc=True, + client_sids=client_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_aa_asserted_identity_device_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({self.aa_asserted_identity})', + device_from_rodc=True, + client_sids=client_sids, + code=(KDC_ERR_POLICY, CRASHES_WINDOWS), + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_aa_asserted_identity_both_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({self.aa_asserted_identity})', + client_from_rodc=True, + device_from_rodc=True, + client_sids=client_sids, + code=(KDC_ERR_POLICY, CRASHES_WINDOWS), + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_with_aa_asserted_identity(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({self.aa_asserted_identity})', + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_with_aa_asserted_identity_client_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = client_sids | { + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({self.aa_asserted_identity})', + client_from_rodc=True, + client_sids=client_sids, + expected_groups=expected_groups) + + def test_tgs_with_aa_asserted_identity_device_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({self.aa_asserted_identity})', + device_from_rodc=True, + client_sids=client_sids, + expected_groups=client_sids, + code=(0, CRASHES_WINDOWS)) + + def test_tgs_with_aa_asserted_identity_both_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + expected_groups = client_sids | { + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({self.aa_asserted_identity})', + client_from_rodc=True, + device_from_rodc=True, + client_sids=client_sids, + expected_groups=expected_groups, + code=(0, CRASHES_WINDOWS)) + + def test_tgs_without_service_asserted_identity(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({self.service_asserted_identity})', + client_sids=client_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_service_asserted_identity_client_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({self.service_asserted_identity})', + client_from_rodc=True, + client_sids=client_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_service_asserted_identity_device_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({self.service_asserted_identity})', + device_from_rodc=True, + client_sids=client_sids, + code=(KDC_ERR_POLICY, CRASHES_WINDOWS), + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_service_asserted_identity_both_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({self.service_asserted_identity})', + client_from_rodc=True, + device_from_rodc=True, + client_sids=client_sids, + code=(KDC_ERR_POLICY, CRASHES_WINDOWS), + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_with_service_asserted_identity(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({self.service_asserted_identity})', + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_with_service_asserted_identity_client_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({self.service_asserted_identity})', + client_from_rodc=True, + client_sids=client_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_with_service_asserted_identity_device_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({self.service_asserted_identity})', + device_from_rodc=True, + client_sids=client_sids, + expected_groups=client_sids, + code=(0, CRASHES_WINDOWS)) + + def test_tgs_with_service_asserted_identity_both_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({self.service_asserted_identity})', + client_from_rodc=True, + device_from_rodc=True, + client_sids=client_sids, + code=(KDC_ERR_POLICY, CRASHES_WINDOWS), + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_claims_valid(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({security.SID_CLAIMS_VALID})', + client_sids=client_sids, + code=KDC_ERR_POLICY, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_claims_valid_client_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + expected_groups = client_sids | { + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({security.SID_CLAIMS_VALID})', + client_from_rodc=True, + client_sids=client_sids, + expected_groups=expected_groups) + + def test_tgs_without_claims_valid_device_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + self._tgs(f'Member_of SID({security.SID_CLAIMS_VALID})', + device_from_rodc=True, + client_sids=client_sids, + code=(KDC_ERR_POLICY, CRASHES_WINDOWS), + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + edata=self.expect_padata_outer) + + def test_tgs_without_claims_valid_both_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + expected_groups = client_sids | { + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({security.SID_CLAIMS_VALID})', + client_from_rodc=True, + device_from_rodc=True, + client_sids=client_sids, + expected_groups=expected_groups, + code=(0, CRASHES_WINDOWS)) + + def test_tgs_with_claims_valid(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({security.SID_CLAIMS_VALID})', + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_with_claims_valid_client_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({security.SID_CLAIMS_VALID})', + client_from_rodc=True, + client_sids=client_sids, + expected_groups=client_sids) + + def test_tgs_with_claims_valid_device_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({security.SID_CLAIMS_VALID})', + device_from_rodc=True, + client_sids=client_sids, + expected_groups=client_sids, + code=(0, CRASHES_WINDOWS)) + + def test_tgs_with_claims_valid_both_from_rodc(self): + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + self._tgs(f'Member_of SID({security.SID_CLAIMS_VALID})', + client_from_rodc=True, + device_from_rodc=True, + client_sids=client_sids, + expected_groups=client_sids, + code=(0, CRASHES_WINDOWS)) + + def _tgs(self, + target_policy=None, + *, + code=0, + event=AuditEvent.OK, + reason=AuditReason.NONE, + status=None, + edata=False, + use_fast=True, + client_from_rodc=None, + device_from_rodc=None, + client_sids=None, + client_claims=None, + device_sids=None, + device_claims=None, + expected_groups=None, + expected_claims=None): + try: + code, crashes_windows = code + self.assertIs(crashes_windows, CRASHES_WINDOWS) + if not self.crash_windows: + self.skipTest('test crashes Windows servers') + except TypeError: + self.assertIsNot(code, CRASHES_WINDOWS) + + if not use_fast: + self.assertIsNone(device_from_rodc) + self.assertIsNone(device_sids) + self.assertIsNone(device_claims) + + if client_from_rodc is None: + client_from_rodc = False + + if device_from_rodc is None: + device_from_rodc = False + + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'allowed_replication_mock': client_from_rodc, + 'revealed_to_mock_rodc': client_from_rodc, + }) + client_sid = client_creds.get_sid() + + client_username = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + client_tkt_options = 'forwardable' + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + checksum_key = self.get_krbtgt_checksum_key() + + if client_from_rodc or device_from_rodc: + rodc_krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + rodc_krbtgt_key = self.TicketDecryptionKey_from_creds(rodc_krbtgt_creds) + rodc_checksum_key = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: rodc_krbtgt_key, + } + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags) + + client_modify_pac_fn = [] + if client_sids is not None: + client_modify_pac_fn.append(partial(self.set_pac_sids, + new_sids=client_sids)) + if client_claims is not None: + client_modify_pac_fn.append(partial(self.set_pac_claims, + client_claims=client_claims)) + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=client_modify_pac_fn, + new_ticket_key=rodc_krbtgt_key if client_from_rodc else None, + checksum_keys=rodc_checksum_key if client_from_rodc else checksum_key) + + if use_fast: + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'allowed_replication_mock': device_from_rodc, + 'revealed_to_mock_rodc': device_from_rodc, + }) + mach_tgt = self.get_tgt(mach_creds) + device_modify_pac_fn = [] + if device_sids is not None: + device_modify_pac_fn.append(partial(self.set_pac_sids, + new_sids=device_sids)) + if device_claims is not None: + device_modify_pac_fn.append(partial(self.set_pac_claims, + client_claims=device_claims)) + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=device_modify_pac_fn, + new_ticket_key=rodc_krbtgt_key if device_from_rodc else None, + checksum_keys=rodc_checksum_key if device_from_rodc else checksum_key) + else: + mach_tgt = None + + if target_policy is None: + policy = None + assigned_policy = None + else: + sddl = f'O:SYD:(XA;;CR;;;WD;({target_policy.format(client_sid=client_creds.get_sid())}))' + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=sddl) + assigned_policy = str(policy.dn) + + # Create a target account with the assigned policy. + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'assigned_policy': assigned_policy}) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + samdb = self.get_samdb() + domain_sid_str = samdb.get_domain_sid() + + expected_groups = self.map_sids(expected_groups, None, domain_sid_str) + + # Show that obtaining a service ticket is allowed. + self._tgs_req(client_tgt, code, client_creds, target_creds, + armor_tgt=mach_tgt, + expected_cname=client_cname, + expected_account_name=client_username, + decryption_key=target_decryption_key, + expected_sid=client_sid, + expected_groups=expected_groups, + expect_client_claims=bool(expected_claims) or None, + expected_client_claims=expected_claims, + expected_supported_etypes=target_etypes, + expected_status=status, + expect_edata=edata) + + self.check_tgs_log(client_creds, target_creds, + policy=policy, + checked_creds=client_creds, + status=status, + event=event, + reason=reason) + + def test_conditional_ace_allowed_from_user_allow(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly allows the machine + # account for a user. + allowed = (f'O:SYD:(XA;;CR;;;{mach_creds.get_sid()};' + f'(Member_of SID({mach_creds.get_sid()})))') + denied = 'O:SYD:(D;;CR;;;WD)' + policy = self.create_authn_policy(enforced=True, + user_allowed_from=allowed, + service_allowed_from=denied) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that authentication succeeds. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=0) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy) + + def test_conditional_ace_allowed_from_user_deny(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly denies the machine + # account for a user. + allowed = 'O:SYD:(A;;CR;;;WD)' + denied = (f'O:SYD:(XD;;CR;;;{mach_creds.get_sid()};' + f'(Member_of SID({mach_creds.get_sid()})))' + f'(A;;CR;;;WD)') + policy = self.create_authn_policy(enforced=True, + user_allowed_from=denied, + service_allowed_from=allowed) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=policy) + + # Show that we get a policy error when trying to authenticate. + self._get_tgt(client_creds, armor_tgt=mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + +class DeviceRestrictionTests(ConditionalAceBaseTests): + def test_pac_groups_not_present(self): + """Test that authentication fails if the device does not belong to some + required groups. + """ + + required_sids = { + ('S-1-2-3-4', SidType.EXTRA_SID, self.default_attrs), + ('S-1-9-8-7', SidType.EXTRA_SID, self.default_attrs), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that requires the device to belong to + # certain groups. + client_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + # Show that authentication fails. + self._armored_as_req(client_creds, + self.get_krbtgt_creds(), + mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=client_policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_pac_groups_present(self): + """Test that authentication succeeds if the device belongs to some + required groups. + """ + + required_sids = { + ('S-1-2-3-4', SidType.EXTRA_SID, self.default_attrs), + ('S-1-9-8-7', SidType.EXTRA_SID, self.default_attrs), + } + + device_sids = required_sids | { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the required groups to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=device_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to belong to + # certain groups. + client_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + # Show that authentication succeeds. + self._armored_as_req(client_creds, + self.get_krbtgt_creds(), + mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=client_policy) + + def test_pac_resource_groups_present(self): + """Test that authentication succeeds if the device belongs to some + required resource groups. + """ + + required_sids = { + ('S-1-2-3-4', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-5', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-6', SidType.RESOURCE_SID, self.resource_attrs), + } + + device_sids = required_sids | { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the required groups to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=device_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to belong to + # certain groups. + client_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + # Show that authentication fails. + self._armored_as_req(client_creds, + self.get_krbtgt_creds(), + mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=client_policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_pac_resource_groups_present_to_service_sid_compression(self): + """Test that authentication succeeds if the device belongs to some + required resource groups, and the request is to a service that supports + SID compression. + """ + + required_sids = { + ('S-1-2-3-4', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-5', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-6', SidType.RESOURCE_SID, self.resource_attrs), + } + + device_sids = required_sids | { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the required groups to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=device_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to belong to + # certain groups. + client_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'target'}) + + # Show that authentication fails. + self._armored_as_req(client_creds, + target_creds, + mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=client_policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_pac_resource_groups_present_to_service_no_sid_compression(self): + """Test that authentication succeeds if the device belongs to some + required resource groups, and the request is to a service that does not + support SID compression. + """ + + required_sids = { + ('S-1-2-3-4', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-5', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-6', SidType.RESOURCE_SID, self.resource_attrs), + } + + device_sids = required_sids | { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the required groups to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=device_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to belong to + # certain groups. + client_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'id': 'target', + 'supported_enctypes': ( + security.KERB_ENCTYPE_RC4_HMAC_MD5) | ( + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK), + 'sid_compression_support': False, + }) + + # Show that authentication fails. + self._armored_as_req(client_creds, + target_creds, + mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=client_policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_pac_well_known_groups_not_present(self): + """Test that authentication fails if the device does not belong to one + or more required well‐known groups. + """ + + required_sids = { + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Modify the machine account’s TGT to contain only the SID of the + # machine account’s primary group. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=device_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to belong to + # certain groups. + client_policy_sddl = self.allow_if( + f'Member_of_any {self.sddl_array_from_sids(required_sids)}') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + # Show that authentication fails. + self._armored_as_req(client_creds, + self.get_krbtgt_creds(), + mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=client_policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_pac_device_info(self): + """Test the groups of the client and the device after performing a + FAST‐armored AS‐REQ. + """ + + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the required groups to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=device_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'target'}) + + expected_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The client’s groups are to include the Asserted Identity and + # Claims Valid SIDs. + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + samdb = self.get_samdb() + domain_sid_str = samdb.get_domain_sid() + + expected_sids = self.map_sids(expected_sids, None, domain_sid_str) + + # Show that authentication succeeds. Check that the groups in the PAC + # are as expected. + self._armored_as_req(client_creds, + target_creds, + mach_tgt, + expected_groups=expected_sids, + expect_device_info=False, + expected_device_groups=None) + + self.check_as_log( + client_creds, + armor_creds=mach_creds) + + def test_pac_claims_not_present(self): + """Test that authentication fails if the device does not have a + required claim. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that requires the device to have a + # certain claim. + client_policy_sddl = self.allow_if( + f'@User.{escaped_claim_id(claim_id)} == "{claim_value}"') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + # Show that authentication fails. + self._armored_as_req(client_creds, + self.get_krbtgt_creds(), + mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=client_policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_pac_claims_present(self): + """Test that authentication succeeds if the device has a required + claim. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + pac_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (claim_id, claims.CLAIM_TYPE_STRING, [claim_value]), + ]), + ] + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the required claim to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=partial(self.set_pac_claims, + client_claims=pac_claims), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to have a + # certain claim. + client_policy_sddl = self.allow_if( + f'@User.{escaped_claim_id(claim_id)} == "{claim_value}"') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + # Show that authentication succeeds. + self._armored_as_req(client_creds, + self.get_krbtgt_creds(), + mach_tgt) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=client_policy) + + def test_pac_claims_invalid(self): + """Test that authentication fails if the device’s required claim is not + valid. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + pac_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (claim_id, claims.CLAIM_TYPE_STRING, [claim_value]), + ]), + ] + + # The device’s SIDs do not include the Claims Valid SID. + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the SIDs and the required claim to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=[ + partial(self.set_pac_claims, client_claims=pac_claims), + partial(self.set_pac_sids, new_sids=device_sids)], + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to have a + # certain claim. + client_policy_sddl = self.allow_if( + f'@User.{escaped_claim_id(claim_id)} == "{claim_value}"') + client_policy = self.create_authn_policy( + enforced=True, user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + # Show that authentication fails. + self._armored_as_req(client_creds, + self.get_krbtgt_creds(), + mach_tgt, + expected_error=KDC_ERR_POLICY) + + self.check_as_log( + client_creds, + armor_creds=mach_creds, + client_policy=client_policy, + client_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_DEVICE_RESTRICTION, + reason=AuditReason.ACCESS_DENIED, + status=ntstatus.NT_STATUS_INVALID_WORKSTATION) + + def test_device_in_world_group(self): + self._check_device_in_group(security.SID_WORLD) + + def test_device_in_network_group(self): + self._check_device_not_in_group(security.SID_NT_NETWORK) + + def test_device_in_authenticated_users(self): + self._check_device_in_group(security.SID_NT_AUTHENTICATED_USERS) + + def test_device_in_aa_asserted_identity(self): + self._check_device_in_group( + security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY) + + def test_device_in_service_asserted_identity(self): + self._check_device_not_in_group(security.SID_SERVICE_ASSERTED_IDENTITY) + + def test_device_in_compounded_authentication(self): + self._check_device_not_in_group(security.SID_COMPOUNDED_AUTHENTICATION) + + def test_device_in_claims_valid(self): + self._check_device_in_group(security.SID_CLAIMS_VALID) + + def _check_device_in_group(self, group): + self._check_device_membership(group, expect_in_group=True) + + def _check_device_not_in_group(self, group): + self._check_device_membership(group, expect_in_group=False) + + def _check_device_membership(self, group, *, expect_in_group): + """Test that authentication succeeds or fails when the device is + required to belong to a certain group. + """ + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that requires the device to belong to + # a certain group. + in_group_sddl = self.allow_if(f'Member_of {{SID({group})}}') + in_group_policy = self.create_authn_policy( + enforced=True, user_allowed_from=in_group_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=in_group_policy) + + krbtgt_creds = self.get_krbtgt_creds() + + # Test whether authentication succeeds or fails. + self._armored_as_req( + client_creds, + krbtgt_creds, + mach_tgt, + expected_error=0 if expect_in_group else KDC_ERR_POLICY) + + policy_success_args = {} + policy_failure_args = { + 'client_policy_status': ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + 'event': AuditEvent.KERBEROS_DEVICE_RESTRICTION, + 'reason': AuditReason.ACCESS_DENIED, + 'status': ntstatus.NT_STATUS_INVALID_WORKSTATION, + } + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=in_group_policy, + **(policy_success_args if expect_in_group + else policy_failure_args)) + + # Create an authentication policy that requires the device not to belong + # to the group. + not_in_group_sddl = self.allow_if(f'Not_Member_of {{SID({group})}}') + not_in_group_policy = self.create_authn_policy( + enforced=True, user_allowed_from=not_in_group_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=not_in_group_policy) + + # Test whether authentication succeeds or fails. + self._armored_as_req( + client_creds, + krbtgt_creds, + mach_tgt, + expected_error=KDC_ERR_POLICY if expect_in_group else 0) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=not_in_group_policy, + **(policy_failure_args if expect_in_group + else policy_success_args)) + + +class TgsReqServicePolicyTests(ConditionalAceBaseTests): + def test_pac_groups_not_present(self): + """Test that authorization succeeds if the client does not belong to + some required groups. + """ + + required_sids = { + ('S-1-2-3-4', SidType.EXTRA_SID, self.default_attrs), + ('S-1-9-8-7', SidType.EXTRA_SID, self.default_attrs), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Create an authentication policy that requires the client to belong to + # certain groups. + target_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization fails. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=target_policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_pac_groups_present(self): + """Test that authorization succeeds if the client belongs to some + required groups. + """ + + required_sids = { + ('S-1-2-3-4', SidType.EXTRA_SID, self.default_attrs), + ('S-1-9-8-7', SidType.EXTRA_SID, self.default_attrs), + } + + client_sids = required_sids | { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Add the required groups to the client’s TGT. + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=client_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the client to belong to + # certain groups. + target_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization succeeds. + self._tgs_req(client_tgt, 0, client_creds, target_creds, armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, + policy=target_policy) + + def test_pac_resource_groups_present_to_service_sid_compression(self): + """Test that authorization succeeds if the client belongs to some + required resource groups, and the request is to a service that supports + SID compression. + """ + + required_sids = { + ('S-1-2-3-4', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-5', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-6', SidType.RESOURCE_SID, self.resource_attrs), + } + + client_sids = required_sids | { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Add the required groups to the client’s TGT. + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=client_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the client to belong to + # certain groups. + target_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization fails. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=target_policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_pac_resource_groups_present_to_service_no_sid_compression(self): + """Test that authorization succeeds if the client belongs to some + required resource groups, and the request is to a service that does not + support SID compression. + """ + + required_sids = { + ('S-1-2-3-4', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-5', SidType.RESOURCE_SID, self.resource_attrs), + ('S-1-2-3-6', SidType.RESOURCE_SID, self.resource_attrs), + } + + client_sids = required_sids | { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Add the required groups to the client’s TGT. + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=client_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the client to belong to + # certain groups. + target_policy_sddl = self.allow_if( + f'Member_of {self.sddl_array_from_sids(required_sids)}') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy, + additional_details={ + 'msDS-SupportedEncryptionTypes': str(( + security.KERB_ENCTYPE_RC4_HMAC_MD5) | ( + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK) | ( + security.KERB_ENCTYPE_RESOURCE_SID_COMPRESSION_DISABLED))}) + + # Show that authorization fails. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=target_policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_pac_well_known_groups_not_present(self): + """Test that authorization fails if the client does not belong to one + or more required well‐known groups. + """ + + required_sids = { + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs), + (self.aa_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + (self.service_asserted_identity, SidType.EXTRA_SID, self.default_attrs), + } + + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Modify the client’s TGT to contain only the SID of the client’s + # primary group. + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=client_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the client to belong to + # certain groups. + target_policy_sddl = self.allow_if( + f'Member_of_any {self.sddl_array_from_sids(required_sids)}') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization fails. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, target_creds, + policy=target_policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_pac_device_info(self): + self._run_pac_device_info_test() + + def test_pac_device_info_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy) + + def test_pac_device_info_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True) + + def test_pac_device_info_existing_device_info(self): + self._run_pac_device_info_test(existing_device_info=True) + + def test_pac_device_info_existing_device_info_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + existing_device_info=True) + + def test_pac_device_info_existing_device_info_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + existing_device_info=True) + + def test_pac_device_info_existing_device_claims(self): + self._run_pac_device_info_test(existing_device_claims=True) + + def test_pac_device_info_existing_device_claims_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + existing_device_claims=True) + + def test_pac_device_info_existing_device_claims_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + existing_device_claims=True) + + def test_pac_device_info_existing_device_info_and_claims(self): + self._run_pac_device_info_test(existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_existing_device_info_and_claims_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_existing_device_info_and_claims_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support(self): + self._run_pac_device_info_test(compound_id_support=False) + + def test_pac_device_info_no_compound_id_support_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + compound_id_support=False) + + def test_pac_device_info_no_compound_id_support_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + compound_id_support=False) + + def test_pac_device_info_no_compound_id_support_existing_device_info(self): + self._run_pac_device_info_test(compound_id_support=False, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_existing_device_info_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + compound_id_support=False, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_existing_device_info_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + compound_id_support=False, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_existing_device_claims(self): + self._run_pac_device_info_test(compound_id_support=False, + existing_device_claims=True) + + def test_pac_device_info_no_compound_id_support_existing_device_claims_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + compound_id_support=False, + existing_device_claims=True) + + def test_pac_device_info_no_compound_id_support_existing_device_claims_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + compound_id_support=False, + existing_device_claims=True) + + def test_pac_device_info_no_compound_id_support_existing_device_info_and_claims(self): + self._run_pac_device_info_test(compound_id_support=False, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_existing_device_info_and_claims_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + compound_id_support=False, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_existing_device_info_and_claims_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + compound_id_support=False, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_info(self): + self._run_pac_device_info_test(device_claims_valid=False, + compound_id_support=False, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_info_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + device_claims_valid=False, + compound_id_support=False, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_info_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + device_claims_valid=False, + compound_id_support=False, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_claims(self): + self._run_pac_device_info_test(device_claims_valid=False, + compound_id_support=False, + existing_device_claims=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_claims_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + device_claims_valid=False, + compound_id_support=False, + existing_device_claims=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_claims_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + device_claims_valid=False, + compound_id_support=False, + existing_device_claims=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_info_and_claims(self): + self._run_pac_device_info_test(device_claims_valid=False, + compound_id_support=False, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_info_and_claims_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + device_claims_valid=False, + compound_id_support=False, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_info_and_claims_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + device_claims_valid=False, + compound_id_support=False, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_claims_valid(self): + self._run_pac_device_info_test(device_claims_valid=False) + + def test_pac_device_info_no_claims_valid_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + device_claims_valid=False) + + def test_pac_device_info_no_claims_valid_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + device_claims_valid=False) + + def test_pac_device_info_no_claims_valid_existing_device_info(self): + self._run_pac_device_info_test(device_claims_valid=False, + existing_device_info=True) + + def test_pac_device_info_no_claims_valid_existing_device_info_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + device_claims_valid=False, + existing_device_info=True) + + def test_pac_device_info_no_claims_valid_existing_device_info_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + device_claims_valid=False, + existing_device_info=True) + + def test_pac_device_info_no_claims_valid_existing_device_claims(self): + self._run_pac_device_info_test(device_claims_valid=False, + existing_device_claims=True) + + def test_pac_device_info_no_claims_valid_existing_device_claims_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + device_claims_valid=False, + existing_device_claims=True) + + def test_pac_device_info_no_claims_valid_existing_device_claims_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + device_claims_valid=False, + existing_device_claims=True) + + def test_pac_device_info_no_claims_valid_existing_device_info_and_claims(self): + self._run_pac_device_info_test(device_claims_valid=False, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_claims_valid_existing_device_info_and_claims_target_policy(self): + target_policy = self.allow_if('Device_Member_of {{SID({device_0})}}') + self._run_pac_device_info_test(target_policy=target_policy, + device_claims_valid=False, + existing_device_claims=True, + existing_device_info=True) + + def test_pac_device_info_no_claims_valid_existing_device_info_and_claims_rodc_issued(self): + self._run_pac_device_info_test(rodc_issued=True, + device_claims_valid=False, + existing_device_claims=True, + existing_device_info=True) + + def _run_pac_device_info_test(self, *, + target_policy=None, + rodc_issued=False, + compound_id_support=True, + device_claims_valid=True, + existing_device_claims=False, + existing_device_info=False): + """Test the groups of the client and the device after performing a + FAST‐armored TGS‐REQ. + """ + + client_claim_id = 'the name of the client’s client claim' + client_claim_value = 'the value of the client’s client claim' + + client_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (client_claim_id, claims.CLAIM_TYPE_STRING, [client_claim_value]), + ]), + ] + + if not rodc_issued: + expected_client_claims = { + client_claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': (client_claim_value,), + }, + } + else: + expected_client_claims = None + + device_claim_id = 'the name of the device’s client claim' + device_claim_value = 'the value of the device’s client claim' + + device_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (device_claim_id, claims.CLAIM_TYPE_STRING, [device_claim_value]), + ]), + ] + + existing_claim_id = 'the name of an existing device claim' + existing_claim_value = 'the value of an existing device claim' + + existing_claims = [ + (claims.CLAIMS_SOURCE_TYPE_CERTIFICATE, [ + (existing_claim_id, claims.CLAIM_TYPE_STRING, [existing_claim_value]), + ]), + ] + + if rodc_issued: + expected_device_claims = None + elif existing_device_info and existing_device_claims: + expected_device_claims = { + existing_claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_CERTIFICATE, + 'type': claims.CLAIM_TYPE_STRING, + 'values': (existing_claim_value,), + }, + } + elif compound_id_support and not existing_device_info and not existing_device_claims: + expected_device_claims = { + device_claim_id: { + 'source_type': claims.CLAIMS_SOURCE_TYPE_AD, + 'type': claims.CLAIM_TYPE_STRING, + 'values': (device_claim_value,), + }, + } + else: + expected_device_claims = None + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # This to ensure we have EXTRA_SIDS set already, as + # windows won't set that flag otherwise when adding one + # more + ('S-1-2-3-4', SidType.EXTRA_SID, self.default_attrs), + } + + device_sid_0 = 'S-1-3-4-5' + device_sid_1 = 'S-1-4-5-6' + + policy_sids = { + 'device_0': device_sid_0, + 'device_1': device_sid_1, + } + + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (device_sid_0, SidType.EXTRA_SID, self.resource_attrs), + (device_sid_1, SidType.EXTRA_SID, self.resource_attrs), + } + + if device_claims_valid: + device_sids.add((security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs)) + + checksum_key = self.get_krbtgt_checksum_key() + + # Modify the machine account’s TGT to contain only the SID of the + # machine account’s primary group. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=[ + partial(self.set_pac_sids, + new_sids=device_sids), + partial(self.set_pac_claims, client_claims=device_claims), + ], + checksum_keys=checksum_key) + + # Create a user account. + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'allowed_replication_mock': rodc_issued, + 'revealed_to_mock_rodc': rodc_issued, + }) + client_tgt = self.get_tgt(client_creds) + + client_modify_pac_fns = [ + partial(self.set_pac_sids, + new_sids=client_sids), + partial(self.set_pac_claims, client_claims=client_claims), + ] + + if existing_device_claims: + client_modify_pac_fns.append( + partial(self.set_pac_claims, device_claims=existing_claims)) + if existing_device_info: + # These are different from the SIDs in the device’s TGT. + existing_sid_0 = 'S-1-7-8-9' + existing_sid_1 = 'S-1-9-8-7' + + policy_sids.update({ + 'existing_0': existing_sid_0, + 'existing_1': existing_sid_1, + }) + + existing_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (existing_sid_0, SidType.EXTRA_SID, self.resource_attrs), + (existing_sid_1, SidType.EXTRA_SID, self.resource_attrs), + } + + client_modify_pac_fns.append(partial( + self.set_pac_device_sids, new_sids=existing_sids, user_rid=mach_creds.get_rid())) + + if rodc_issued: + rodc_krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + rodc_krbtgt_key = self.TicketDecryptionKey_from_creds(rodc_krbtgt_creds) + rodc_checksum_key = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: rodc_krbtgt_key, + } + + # Modify the client’s TGT to contain only the SID of the client’s + # primary group. + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=client_modify_pac_fns, + new_ticket_key=rodc_krbtgt_key if rodc_issued else None, + checksum_keys=rodc_checksum_key if rodc_issued else checksum_key) + + if target_policy is None: + policy = None + assigned_policy = None + else: + policy = self.create_authn_policy( + enforced=True, + computer_allowed_to=target_policy.format_map(policy_sids)) + assigned_policy = str(policy.dn) + + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'supported_enctypes': + security.KERB_ENCTYPE_RC4_HMAC_MD5 + | security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96, + # Indicate that Compound Identity is supported. + 'compound_id_support': compound_id_support, + 'assigned_policy': assigned_policy, + }) + + expected_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The client’s groups are not to include the Asserted Identity and + # Claims Valid SIDs. + } + if rodc_issued: + expected_sids.add((security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs)) + else: + expected_sids.add(('S-1-2-3-4', SidType.EXTRA_SID, self.default_attrs)) + + if rodc_issued: + expected_device_sids = None + elif existing_device_info: + expected_device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-7-8-9', SidType.EXTRA_SID, self.resource_attrs), + ('S-1-9-8-7', SidType.EXTRA_SID, self.resource_attrs), + } + elif compound_id_support and not existing_device_claims: + expected_sids.add((security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs)) + + expected_device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-3-4-5', SidType.EXTRA_SID, self.resource_attrs), + ('S-1-4-5-6', SidType.EXTRA_SID, self.resource_attrs), + } + + if device_claims_valid: + expected_device_sids.add(frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, self.default_attrs)])) + else: + expected_device_sids = None + + samdb = self.get_samdb() + domain_sid_str = samdb.get_domain_sid() + + expected_sids = self.map_sids(expected_sids, None, domain_sid_str) + # The device SIDs will be put into the PAC unmodified. + expected_device_sids = self.map_sids(expected_device_sids, None, domain_sid_str) + + # Show that authorization succeeds. + self._tgs_req(client_tgt, 0, client_creds, target_creds, armor_tgt=mach_tgt, + expected_groups=expected_sids, + expect_device_info=bool(expected_device_sids), + expected_device_domain_sid=domain_sid_str, + expected_device_groups=expected_device_sids, + expect_client_claims=True, + expected_client_claims=expected_client_claims, + expect_device_claims=bool(expected_device_claims), + expected_device_claims=expected_device_claims) + + self.check_tgs_log(client_creds, target_creds, policy=policy) + + def test_pac_extra_sids_behaviour(self): + """Test the groups of the client and the device after performing a + FAST‐armored TGS‐REQ. + """ + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + client_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Modify the client’s TGT to contain only the SID of the client’s + # primary group. + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=partial(self.set_pac_sids, + new_sids=client_sids), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Indicate that Compound Identity is supported. + target_creds, _ = self.get_target(to_krbtgt=False, compound_id=True) + + expected_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_COMPOUNDED_AUTHENTICATION, SidType.EXTRA_SID, self.default_attrs) + # The client’s groups are not to include the Asserted Identity and + # Claims Valid SIDs. + } + + samdb = self.get_samdb() + domain_sid_str = samdb.get_domain_sid() + + expected_sids = self.map_sids(expected_sids, None, domain_sid_str) + + # Show that authorization succeeds. + self._tgs_req(client_tgt, 0, client_creds, target_creds, armor_tgt=mach_tgt, + expected_groups=expected_sids) + + self.check_tgs_log(client_creds, target_creds) + + def test_pac_claims_not_present(self): + """Test that authentication fails if the device does not have a + required claim. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that requires the device to have a + # certain claim. + target_policy_sddl = self.allow_if( + f'@User.{escaped_claim_id(claim_id)} == "{claim_value}"') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization fails. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, + target_creds, + policy=target_policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_pac_claims_present(self): + """Test that authentication succeeds if the user has a required + claim. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + pac_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (claim_id, claims.CLAIM_TYPE_STRING, [claim_value]), + ]), + ] + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that requires the user to have a + # certain claim. + target_policy_sddl = self.allow_if( + f'@User.{escaped_claim_id(claim_id)} == "{claim_value}"') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Add the required claim to the client’s TGT. + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=partial(self.set_pac_claims, + client_claims=pac_claims), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization succeeds. + self._tgs_req(client_tgt, 0, client_creds, target_creds, armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, + policy=target_policy) + + def test_pac_claims_invalid(self): + """Test that authentication fails if the device’s required claim is not + valid. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + pac_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (claim_id, claims.CLAIM_TYPE_STRING, [claim_value]), + ]), + ] + + # The device’s SIDs do not include the Claims Valid SID. + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that requires the device to have a + # certain claim. + target_policy_sddl = self.allow_if( + f'@User.{escaped_claim_id(claim_id)} == "{claim_value}"') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Add the SIDs and the required claim to the client’s TGT. + client_tgt = self.modified_ticket( + client_tgt, + modify_pac_fn=[ + partial(self.set_pac_claims, client_claims=pac_claims), + partial(self.set_pac_sids, new_sids=device_sids)], + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization fails. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, + target_creds, + policy=target_policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_pac_device_claims_not_present(self): + """Test that authorization fails if the device does not have a + required claim. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that requires the device to have a + # certain device claim. + target_policy_sddl = self.allow_if( + f'@Device.{escaped_claim_id(claim_id)} == "{claim_value}"') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization fails. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, + target_creds, + policy=target_policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_pac_device_claims_present(self): + """Test that authorization succeeds if the device has a required claim. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + pac_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (claim_id, claims.CLAIM_TYPE_STRING, [claim_value]), + ]), + ] + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the required claim to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=partial(self.set_pac_claims, + client_claims=pac_claims), + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to have a + # certain device claim. + target_policy_sddl = self.allow_if( + f'@Device.{escaped_claim_id(claim_id)} == "{claim_value}"') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization succeeds. + self._tgs_req(client_tgt, 0, client_creds, target_creds, armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, + policy=target_policy) + + def test_pac_device_claims_invalid(self): + """Test that authorization fails if the device’s required claim is not + valid. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + pac_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (claim_id, claims.CLAIM_TYPE_STRING, [claim_value]), + ]), + ] + + # The device’s SIDs do not include the Claims Valid SID. + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the SIDs and the required claim to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=[ + partial(self.set_pac_claims, client_claims=pac_claims), + partial(self.set_pac_sids, new_sids=device_sids)], + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to have a + # certain claim. + target_policy_sddl = self.allow_if( + f'@Device.{escaped_claim_id(claim_id)} == "{claim_value}"') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization fails. + self._tgs_req( + client_tgt, KDC_ERR_POLICY, client_creds, target_creds, + armor_tgt=mach_tgt, + expect_edata=self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_tgs_log( + client_creds, + target_creds, + policy=target_policy, + status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.KERBEROS_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_pac_device_claims_invalid_no_attrs(self): + """Test that authorization fails if the device’s required claim is not + valid. + """ + + claim_id = 'the name of the claim' + claim_value = 'the value of the claim' + + pac_claims = [ + (claims.CLAIMS_SOURCE_TYPE_AD, [ + (claim_id, claims.CLAIM_TYPE_STRING, [claim_value]), + ]), + ] + + device_sids = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The device’s SIDs include the Claims Valid SID, but it has no + # attributes. + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, 0), + } + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Add the SIDs and the required claim to the machine account’s TGT. + mach_tgt = self.modified_ticket( + mach_tgt, + modify_pac_fn=[ + partial(self.set_pac_claims, client_claims=pac_claims), + partial(self.set_pac_sids, new_sids=device_sids)], + checksum_keys=self.get_krbtgt_checksum_key()) + + # Create an authentication policy that requires the device to have a + # certain claim. + target_policy_sddl = self.allow_if( + f'@Device.{escaped_claim_id(claim_id)} == "{claim_value}"') + target_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=target_policy_sddl) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + # Show that authorization succeeds. + self._tgs_req(client_tgt, 0, client_creds, target_creds, armor_tgt=mach_tgt) + + self.check_tgs_log(client_creds, target_creds, + policy=target_policy) + + def test_simple_as_req_client_and_target_policy(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly allows the machine + # account for a user. + client_policy_sddl = f'O:SYD:(XA;;CR;;;{mach_creds.get_sid()};(Member_of {{SID({mach_creds.get_sid()}), SID({mach_creds.get_sid()})}}))' + client_policy = self.create_authn_policy(enforced=True, + user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + # Create an authentication policy that applies to a computer and + # explicitly allows the user account to obtain a service ticket. + target_policy_sddl = f'O:SYD:(XA;;CR;;;{client_creds.get_sid()};(Member_of SID({client_creds.get_sid()})))' + target_policy = self.create_authn_policy(enforced=True, + computer_allowed_to=target_policy_sddl) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=target_policy) + + expected_groups = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + # Show that obtaining a service ticket with an AS‐REQ is allowed. + self._armored_as_req(client_creds, + target_creds, + mach_tgt, + expected_groups=expected_groups) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=client_policy, + server_policy=target_policy) + + def test_device_in_world_group(self): + self._check_device_in_group(security.SID_WORLD) + + def test_device_in_network_group(self): + self._check_device_not_in_group(security.SID_NT_NETWORK) + + def test_device_in_authenticated_users(self): + self._check_device_in_group(security.SID_NT_AUTHENTICATED_USERS) + + def test_device_in_aa_asserted_identity(self): + self._check_device_in_group( + security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY) + + def test_device_in_service_asserted_identity(self): + self._check_device_not_in_group(security.SID_SERVICE_ASSERTED_IDENTITY) + + def test_device_in_compounded_authentication(self): + self._check_device_not_in_group(security.SID_COMPOUNDED_AUTHENTICATION) + + def test_device_in_claims_valid(self): + self._check_device_in_group(security.SID_CLAIMS_VALID) + + def _check_device_in_group(self, group): + self._check_device_membership(group, expect_in_group=True) + + def _check_device_not_in_group(self, group): + self._check_device_membership(group, expect_in_group=False) + + def _check_device_membership(self, group, *, expect_in_group): + """Test that authentication succeeds or fails when the device is + required to belong to a certain group. + """ + + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'id': 'device'}) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that requires the device to belong to + # a certain group. + in_group_sddl = self.allow_if(f'Device_Member_of {{SID({group})}}') + in_group_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=in_group_sddl) + + # Create a user account. + client_creds = self._get_creds(account_type=self.AccountType.USER) + client_tgt = self.get_tgt(client_creds) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=in_group_policy) + + tgs_success_args = {} + tgs_failure_args = { + 'expect_edata': self.expect_padata_outer, + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + } + + # Test whether authorization succeeds or fails. + self._tgs_req(client_tgt, + 0 if expect_in_group else KDC_ERR_POLICY, + client_creds, + target_creds, + armor_tgt=mach_tgt, + **(tgs_success_args if expect_in_group + else tgs_failure_args)) + + policy_success_args = {} + policy_failure_args = { + 'status': ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + 'event': AuditEvent.KERBEROS_SERVER_RESTRICTION, + 'reason': AuditReason.ACCESS_DENIED, + } + + self.check_tgs_log(client_creds, target_creds, + policy=in_group_policy, + **(policy_success_args if expect_in_group + else policy_failure_args)) + + # Create an authentication policy that requires the device not to belong + # to the group. + not_in_group_sddl = self.allow_if( + f'Not_Device_Member_of {{SID({group})}}') + not_in_group_policy = self.create_authn_policy( + enforced=True, computer_allowed_to=not_in_group_sddl) + + # Create a target account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=not_in_group_policy) + + # Test whether authorization succeeds or fails. + self._tgs_req(client_tgt, + KDC_ERR_POLICY if expect_in_group else 0, + client_creds, + target_creds, + armor_tgt=mach_tgt, + **(tgs_failure_args if expect_in_group + else tgs_success_args)) + + self.check_tgs_log(client_creds, target_creds, + policy=not_in_group_policy, + **(policy_failure_args if expect_in_group + else policy_success_args)) + + def test_simple_as_req_client_policy_only(self): + # Create a machine account with which to perform FAST. + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER) + mach_tgt = self.get_tgt(mach_creds) + + # Create an authentication policy that explicitly allows the machine + # account for a user. + client_policy_sddl = f'O:SYD:(XA;;CR;;;{mach_creds.get_sid()};(Member_of SID({mach_creds.get_sid()})))' + client_policy = self.create_authn_policy(enforced=True, + user_allowed_from=client_policy_sddl) + + # Create a user account with the assigned policy. + client_creds = self._get_creds(account_type=self.AccountType.USER, + assigned_policy=client_policy) + + expected_groups = { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, self.default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, SidType.EXTRA_SID, self.default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, self.default_attrs), + } + + # Show that obtaining a service ticket with an AS‐REQ is allowed. + self._armored_as_req(client_creds, + self.get_krbtgt_creds(), + mach_tgt, + expected_groups=expected_groups) + + self.check_as_log(client_creds, + armor_creds=mach_creds, + client_policy=client_policy) + + +class SamLogonTests(ConditionalAceBaseTests): + # These tests show that although conditional ACEs work with SamLogon, + # claims do not appear to be used at all. + + def test_samlogon_allowed_to_computer_member_of(self): + # Create an authentication policy that applies to a computer and + # requires that the account should belong to both groups. + allowed = (f'O:SYD:(XA;;CR;;;WD;(Member_of ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))') + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # When the account is a member of both groups, network SamLogon + # succeeds. + self._test_samlogon(creds=self._member_of_both_creds_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(self._member_of_both_creds_ntlm, + server_policy=policy) + + # Interactive SamLogon also succeeds. + self._test_samlogon(creds=self._member_of_both_creds_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(self._member_of_both_creds_ntlm, + server_policy=policy) + + # When the account is a member of neither group, network SamLogon + # fails. + self._test_samlogon( + creds=self._mach_creds_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + self._mach_creds_ntlm, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Interactive SamLogon also fails. + self._test_samlogon( + creds=self._mach_creds_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + self._mach_creds_ntlm, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_service_member_of(self): + # Create an authentication policy that applies to a managed service and + # requires that the account should belong to both groups. + allowed = (f'O:SYD:(XA;;CR;;;WD;(Member_of ' + f'{{SID({self._group0_sid}), SID({self._group1_sid})}}))') + policy = self.create_authn_policy(enforced=True, + service_allowed_to=allowed) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # When the account is a member of both groups, network SamLogon + # succeeds. + self._test_samlogon(creds=self._member_of_both_creds_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + self.check_samlogon_network_log(self._member_of_both_creds_ntlm, + server_policy=policy) + + # Interactive SamLogon also succeeds. + self._test_samlogon(creds=self._member_of_both_creds_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + self.check_samlogon_interactive_log(self._member_of_both_creds_ntlm, + server_policy=policy) + + # When the account is a member of neither group, network SamLogon + # fails. + self._test_samlogon( + creds=self._mach_creds_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + self._mach_creds_ntlm, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Interactive SamLogon also fails. + self._test_samlogon( + creds=self._mach_creds_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + self._mach_creds_ntlm, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_computer_silo(self): + # Create an authentication policy that applies to a computer and + # requires that the account belong to the enforced silo. + allowed = (f'O:SYD:(XA;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo == ' + f'"{self._enforced_silo}"))') + policy = self.create_authn_policy(enforced=True, + computer_allowed_to=allowed) + + # Create a computer account with the assigned policy. + target_creds = self._get_creds(account_type=self.AccountType.COMPUTER, + assigned_policy=policy) + + # Even though the account is a member of the silo, its claims are + # ignored, and network SamLogon fails. + self._test_samlogon( + creds=self._member_of_enforced_silo_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + self._member_of_enforced_silo_ntlm, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Interactive SamLogon also fails. + self._test_samlogon( + creds=self._member_of_enforced_silo_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + self._member_of_enforced_silo_ntlm, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + def test_samlogon_allowed_to_service_silo(self): + # Create an authentication policy that applies to a managed service and + # requires that the account belong to the enforced silo. + allowed = (f'O:SYD:(XA;;CR;;;WD;' + f'(@User.ad://ext/AuthenticationSilo == ' + f'"{self._enforced_silo}"))') + policy = self.create_authn_policy(enforced=True, + service_allowed_to=allowed) + + # Create a managed service account with the assigned policy. + target_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + assigned_policy=policy) + + # Even though the account is a member of the silo, its claims are + # ignored, and network SamLogon fails. + self._test_samlogon( + creds=self._member_of_enforced_silo_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_network_log( + self._member_of_enforced_silo_ntlm, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + # Interactive SamLogon also fails. + self._test_samlogon( + creds=self._member_of_enforced_silo_ntlm, + domain_joined_mach_creds=target_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED) + + self.check_samlogon_interactive_log( + self._member_of_enforced_silo_ntlm, + server_policy=policy, + server_policy_status=ntstatus.NT_STATUS_AUTHENTICATION_FIREWALL_FAILED, + event=AuditEvent.NTLM_SERVER_RESTRICTION, + reason=AuditReason.ACCESS_DENIED) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/device_tests.py b/python/samba/tests/krb5/device_tests.py new file mode 100755 index 0000000..ec2fce6 --- /dev/null +++ b/python/samba/tests/krb5/device_tests.py @@ -0,0 +1,2211 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd 2022 +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +import random +import re + +from samba.dcerpc import netlogon, security +from samba.tests import DynamicTestCase, env_get_var_value +from samba.tests.krb5 import kcrypto +from samba.tests.krb5.kdc_base_test import GroupType, KDCBaseTest, Principal +from samba.tests.krb5.raw_testcase import Krb5EncryptionKey, RawKerberosTest +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KRB_TGS_REP, +) + +SidType = RawKerberosTest.SidType + +global_asn1_print = False +global_hexdump = False + + +@DynamicTestCase +class DeviceTests(KDCBaseTest): + # Placeholder objects that represent accounts undergoing testing. + user = object() + mach = object() + trust_user = object() + trust_mach = object() + + # Constants for group SID attributes. + default_attrs = security.SE_GROUP_DEFAULT_FLAGS + resource_attrs = default_attrs | security.SE_GROUP_RESOURCE + + asserted_identity = security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY + compounded_auth = security.SID_COMPOUNDED_AUTHENTICATION + + user_trust_domain = 'S-1-5-21-123-456-111' + mach_trust_domain = 'S-1-5-21-123-456-222' + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + # Some general information on how Windows handles device info: + + # All the SIDs in the computer's info3.sids end up in device.domain_groups + # (if they are in any domain), or in device.sids (if they are not). Even if + # netlogon.NETLOGON_EXTRA_SIDS is not set. + + # The remainder of the SIDs in device.domain_groups come from an LDAP + # search of the computer's domain-local groups. + + # None of the SIDs in the computer's logon_info.resource_groups.groups go + # anywhere. Even if netlogon.NETLOGON_RESOURCE_GROUPS is set. + + # In summary: + # info3.base.groups => device.groups + # info3.sids => device.sids (if not in a domain) + # info3.sids => device.domain_groups (if in a domain) + # searched-for domain-local groups => device.domain_groups + + # These searched-for domain-local groups are based on _all_ the groups in + # info3.base.groups and info3.sids. So if the account is no longer a member + # of a (universal or global) group that belongs to a domain-local group, + # but has that universal or global group in info3.base.groups or + # info3.sids, then the domain-local group will still get added to the + # PAC. But the resource groups don't affect this (presumably, they are + # being filtered out). Also, those groups the search is based on do not go + # in themselves, even if they are domain-local groups. + + cases = [ + { + # Make a TGS request to the krbtgt. + 'test': 'basic to krbtgt', + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + # Indicate this request is to the krbtgt. + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + # Make a TGS request to a service that supports SID compression. + 'test': 'device to service compressed', + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + # The compounded authentication SID indicates that we used FAST + # with a device's TGT. + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + # Make a TGS request to a service that lacks support for SID + # compression. + 'test': 'device to service uncompressed', + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # SID compression is unsupported. + 'tgs:compression': False, + # There is no change in the reply PAC. + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + # Make a TGS request to a service that lacks support for compound + # identity. + 'test': 'device to service no compound id', + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # Compound identity is unsupported. + 'tgs:compound_id': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + # The Compounded Authentication SID should not be present. + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'universal groups to krbtgt', + 'groups': { + # The user and computer each belong to a couple of universal + # groups. + 'group0': (GroupType.UNIVERSAL, {'group1'}), + 'group1': (GroupType.UNIVERSAL, {user}), + 'group2': (GroupType.UNIVERSAL, {'group3'}), + 'group3': (GroupType.UNIVERSAL, {mach}), + }, + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The user's groups appear in the PAC of the TGT. + ('group0', SidType.BASE_SID, default_attrs), + ('group1', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # So too for the computer's groups. + ('group2', SidType.BASE_SID, default_attrs), + ('group3', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The user's groups appear in the TGS reply PAC. + ('group0', SidType.BASE_SID, default_attrs), + ('group1', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'universal groups to service', + 'groups': { + 'group0': (GroupType.UNIVERSAL, {'group1'}), + 'group1': (GroupType.UNIVERSAL, {user}), + 'group2': (GroupType.UNIVERSAL, {'group3'}), + 'group3': (GroupType.UNIVERSAL, {mach}), + }, + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('group0', SidType.BASE_SID, default_attrs), + ('group1', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + ('group2', SidType.BASE_SID, default_attrs), + ('group3', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('group0', SidType.BASE_SID, default_attrs), + ('group1', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The computer's groups appear in the device info structure of + # the TGS reply PAC. + ('group2', SidType.BASE_SID, default_attrs), + ('group3', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'domain-local groups to krbtgt', + 'groups': { + # The user and computer each belong to a couple of domain-local + # groups. + 'group0': (GroupType.DOMAIN_LOCAL, {'group1'}), + 'group1': (GroupType.DOMAIN_LOCAL, {user}), + 'group2': (GroupType.DOMAIN_LOCAL, {'group3'}), + 'group3': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The user's domain-local group memberships do not appear. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # Nor do the computer's. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The user's groups do not appear in the TGS reply PAC. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'domain-local groups to service compressed', + 'groups': { + 'group0': (GroupType.DOMAIN_LOCAL, {'group1'}), + 'group1': (GroupType.DOMAIN_LOCAL, {user}), + 'group2': (GroupType.DOMAIN_LOCAL, {'group3'}), + 'group3': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # These groups appear as resource SIDs. + ('group0', SidType.RESOURCE_SID, resource_attrs), + ('group1', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The computer's groups appear together as resource SIDs. + frozenset([ + ('group2', SidType.RESOURCE_SID, resource_attrs), + ('group3', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'domain-local groups to service uncompressed', + 'groups': { + 'group0': (GroupType.DOMAIN_LOCAL, {'group1'}), + 'group1': (GroupType.DOMAIN_LOCAL, {user}), + 'group2': (GroupType.DOMAIN_LOCAL, {'group3'}), + 'group3': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The user's groups now appear as extra SIDs. + ('group0', SidType.EXTRA_SID, resource_attrs), + ('group1', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The computer's groups are still resource SIDs. + frozenset([ + ('group2', SidType.RESOURCE_SID, resource_attrs), + ('group3', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Test what happens if the computer is removed from a group prior to + # the TGS request. + { + 'test': 'remove transitive domain-local groups to krbtgt', + 'groups': { + # The computer is transitively a member of a couple of + # domain-local groups... + 'dom-local-outer-0': (GroupType.DOMAIN_LOCAL, {'dom-local-inner'}), + 'dom-local-outer-1': (GroupType.DOMAIN_LOCAL, {'universal-inner'}), + # ...via another domain-local group and a universal group. + 'dom-local-inner': (GroupType.DOMAIN_LOCAL, {mach}), + 'universal-inner': (GroupType.UNIVERSAL, {mach}), + }, + # Just prior to the TGS request, the computer is removed from both + # inner groups. Domain-local groups will have not been added to the + # PAC at this point. + 'tgs:mach:removed': { + 'dom-local-inner', + 'universal-inner', + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # Only the universal group appears in the PAC. + ('universal-inner', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'remove transitive domain-local groups to service compressed', + 'groups': { + 'dom-local-outer-0': (GroupType.DOMAIN_LOCAL, {'dom-local-inner'}), + 'dom-local-outer-1': (GroupType.DOMAIN_LOCAL, {'universal-inner'}), + 'dom-local-inner': (GroupType.DOMAIN_LOCAL, {mach}), + 'universal-inner': (GroupType.UNIVERSAL, {mach}), + }, + 'tgs:mach:removed': { + 'dom-local-inner', + 'universal-inner', + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + ('universal-inner', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The universal group appears in the device info... + ('universal-inner', SidType.BASE_SID, default_attrs), + # ...along with the second domain-local group, even though the + # computer no longer belongs to it. + frozenset([ + ('dom-local-outer-1', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'remove transitive domain-local groups to service uncompressed', + 'groups': { + 'dom-local-outer-0': (GroupType.DOMAIN_LOCAL, {'dom-local-inner'}), + 'dom-local-outer-1': (GroupType.DOMAIN_LOCAL, {'universal-inner'}), + 'dom-local-inner': (GroupType.DOMAIN_LOCAL, {mach}), + 'universal-inner': (GroupType.UNIVERSAL, {mach}), + }, + 'tgs:mach:removed': { + 'dom-local-inner', + 'universal-inner', + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + ('universal-inner', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + ('universal-inner', SidType.BASE_SID, default_attrs), + frozenset([ + ('dom-local-outer-1', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Test what happens if the computer is added to a group prior to the + # TGS request. + { + 'test': 'add transitive domain-local groups to krbtgt', + 'groups': { + # We create a pair of groups, to be used presently. + 'dom-local-outer': (GroupType.DOMAIN_LOCAL, {'universal-inner'}), + 'universal-inner': (GroupType.UNIVERSAL, {}), + }, + # Just prior to the TGS request, the computer is added to the inner + # group. + 'tgs:mach:added': { + 'universal-inner', + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'add transitive domain-local groups to service compressed', + 'groups': { + 'dom-local-outer': (GroupType.DOMAIN_LOCAL, {'universal-inner'}), + 'universal-inner': (GroupType.UNIVERSAL, {}), + }, + 'tgs:mach:added': { + 'universal-inner', + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The computer was not a member of the universal group at the + # time of obtaining a TGT, and said group did not make it into + # the PAC. Group expansion is only concerned with domain-local + # groups, none of which the machine currently belongs + # to. Therefore, neither group is present in the device info + # structure. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'add transitive domain-local groups to service uncompressed', + 'groups': { + 'dom-local-outer': (GroupType.DOMAIN_LOCAL, {'universal-inner'}), + 'universal-inner': (GroupType.UNIVERSAL, {}), + }, + 'tgs:mach:added': { + 'universal-inner', + }, + 'as:mach:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Simulate a machine ticket coming in over a trust. + { + 'test': 'from trust domain-local groups to service compressed', + 'groups': { + # The machine belongs to a couple of domain-local groups in our + # domain. + 'foo': (GroupType.DOMAIN_LOCAL, {trust_mach}), + 'bar': (GroupType.DOMAIN_LOCAL, {'foo'}), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + # The machine SID is from a different domain. + 'tgs:mach_sid': trust_mach, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The domain-local groups end up in the device info. + frozenset([ + ('foo', SidType.RESOURCE_SID, resource_attrs), + ('bar', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'from trust domain-local groups to service uncompressed', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {trust_mach}), + 'bar': (GroupType.DOMAIN_LOCAL, {'foo'}), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': False, + 'tgs:mach_sid': trust_mach, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + frozenset([ + ('foo', SidType.RESOURCE_SID, resource_attrs), + ('bar', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Simulate the user ticket coming in over a trust. + { + 'test': 'user from trust domain-local groups to krbtgt', + 'groups': { + # The user belongs to a couple of domain-local groups in our + # domain. + 'group0': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'group1': (GroupType.DOMAIN_LOCAL, {'group0'}), + }, + 'tgs:to_krbtgt': True, + # Both SIDs are from a different domain. + 'tgs:user_sid': trust_user, + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # The dummy resource SID remains in the PAC. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + }, + { + 'test': 'user from trust domain-local groups to service compressed', + 'groups': { + 'group0': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'group1': (GroupType.DOMAIN_LOCAL, {'group0'}), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:user_sid': trust_user, + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + ('group0', SidType.RESOURCE_SID, resource_attrs), + ('group1', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'user from trust domain-local groups to service uncompressed', + 'groups': { + 'group0': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'group1': (GroupType.DOMAIN_LOCAL, {'group0'}), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': False, + 'tgs:user_sid': trust_user, + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + ('group0', SidType.EXTRA_SID, resource_attrs), + ('group1', SidType.EXTRA_SID, resource_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Simulate both tickets coming in over a trust. + { + 'test': 'both from trust domain-local groups to krbtgt', + 'groups': { + # The user and machine each belong to a couple of domain-local + # groups in our domain. + 'group0': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'group1': (GroupType.DOMAIN_LOCAL, {'group0'}), + 'group2': (GroupType.DOMAIN_LOCAL, {trust_mach}), + 'group3': (GroupType.DOMAIN_LOCAL, {'group2'}), + }, + 'tgs:to_krbtgt': True, + # Both SIDs are from a different domain. + 'tgs:user_sid': trust_user, + 'tgs:mach_sid': trust_mach, + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-444', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # The dummy resource SID remains in the PAC. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + }, + { + 'test': 'both from trust domain-local groups to service compressed', + 'groups': { + # The machine belongs to a couple of domain-local groups in our + # domain. + 'group0': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'group1': (GroupType.DOMAIN_LOCAL, {'group0'}), + 'group2': (GroupType.DOMAIN_LOCAL, {trust_mach}), + 'group3': (GroupType.DOMAIN_LOCAL, {'group2'}), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:user_sid': trust_user, + 'tgs:mach_sid': trust_mach, + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-444', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + ('group0', SidType.RESOURCE_SID, resource_attrs), + ('group1', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The domain-local groups end up in the device info. + frozenset([ + ('group2', SidType.RESOURCE_SID, resource_attrs), + ('group3', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'both from trust domain-local groups to service uncompressed', + 'groups': { + 'group0': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'group1': (GroupType.DOMAIN_LOCAL, {'group0'}), + 'group2': (GroupType.DOMAIN_LOCAL, {trust_mach}), + 'group3': (GroupType.DOMAIN_LOCAL, {'group2'}), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': False, + 'tgs:user_sid': trust_user, + 'tgs:mach_sid': trust_mach, + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{mach_trust_domain}-444', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + ('group0', SidType.EXTRA_SID, resource_attrs), + ('group1', SidType.EXTRA_SID, resource_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + frozenset([ + ('group2', SidType.RESOURCE_SID, resource_attrs), + ('group3', SidType.RESOURCE_SID, resource_attrs), + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Test how resource SIDs are propagated into the device info structure. + { + 'test': 'mach resource sids', + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + # Of these SIDs, the Base SIDs and Extra SIDs are all + # propagated into the device info structure, regardless of + # their attrs, while the Resource SIDs are all dropped. + (123, SidType.BASE_SID, default_attrs), + (333, SidType.BASE_SID, default_attrs), + (333, SidType.BASE_SID, resource_attrs), + (1000, SidType.BASE_SID, resource_attrs), + (497, SidType.EXTRA_SID, resource_attrs), # the Claims Valid RID. + (333, SidType.RESOURCE_SID, default_attrs), + (498, SidType.RESOURCE_SID, resource_attrs), + (99999, SidType.RESOURCE_SID, default_attrs), + (12345678, SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, default_attrs), + (333, SidType.BASE_SID, default_attrs), + (333, SidType.BASE_SID, resource_attrs), + (1000, SidType.BASE_SID, resource_attrs), + frozenset({ + (497, SidType.RESOURCE_SID, resource_attrs), + }), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Add a Base SID to the user's PAC, and confirm it is propagated into + # the PAC of the service ticket. + { + 'test': 'base sid to krbtgt', + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'base sid to service', + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Add a Base SID with resource attrs to the user's PAC, and confirm it + # is propagated into the PAC of the service ticket. + { + 'test': 'base sid resource attrs to krbtgt', + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'base sid resource attrs to service', + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (123, SidType.BASE_SID, resource_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Add a couple of Extra SIDs to the user's PAC, and confirm they are + # propagated into the PAC of the service ticket. + { + 'test': 'extra sids to krbtgt', + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-5-2-3-4', SidType.EXTRA_SID, default_attrs), + ('S-1-5-2-3-5', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-5-2-3-4', SidType.EXTRA_SID, default_attrs), + ('S-1-5-2-3-5', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'extra sids to service', + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-5-2-3-4', SidType.EXTRA_SID, default_attrs), + ('S-1-5-2-3-5', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-5-2-3-4', SidType.EXTRA_SID, default_attrs), + ('S-1-5-2-3-5', SidType.EXTRA_SID, resource_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Test what happens if we remove the CLAIMS_VALID and ASSERTED_IDENTITY + # SIDs from either of the PACs, so we can see at what point these SIDs + # are added. + { + 'test': 'removed special sids to krbtgt', + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-5-2-3-4', SidType.EXTRA_SID, default_attrs), + # We don't specify asserted identity or claims valid SIDs for + # the user... + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # ...nor for the computer. + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-5-2-3-4', SidType.EXTRA_SID, default_attrs), + # They don't show up in the service ticket. + }, + }, + { + 'test': 'removed special sids to service', + 'tgs:user:sids': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-5-2-3-4', SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + ('S-1-5-2-3-4', SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # These special SIDs don't show up in the device info either. + }, + }, + # Test a group being the primary one for the user and machine. + { + 'test': 'primary universal to krbtgt', + 'groups': { + 'primary-user': (GroupType.UNIVERSAL, {user}), + 'primary-mach': (GroupType.UNIVERSAL, {mach}), + }, + # Set these groups as the account's primary groups. + 'primary_group': 'primary-user', + 'mach:primary_group': 'primary-mach', + 'as:expected': { + # They appear in the PAC as normal. + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'primary universal to service compressed', + 'groups': { + 'primary-user': (GroupType.UNIVERSAL, {user}), + 'primary-mach': (GroupType.UNIVERSAL, {mach}), + }, + 'primary_group': 'primary-user', + 'mach:primary_group': 'primary-mach', + 'as:expected': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'primary universal to service uncompressed', + 'groups': { + 'primary-user': (GroupType.UNIVERSAL, {user}), + 'primary-mach': (GroupType.UNIVERSAL, {mach}), + }, + 'primary_group': 'primary-user', + 'mach:primary_group': 'primary-mach', + 'as:expected': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # SID compression is unsupported. + 'tgs:compression': False, + 'tgs:expected': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Test domain-local primary groups. + { + 'test': 'primary domain-local to krbtgt', + 'groups': { + 'primary-user': (GroupType.DOMAIN_LOCAL, {user}), + 'primary-mach': (GroupType.DOMAIN_LOCAL, {mach}), + }, + # Though Windows normally disallows setting domain-locals group as + # primary groups, Samba does not. + 'primary_group': 'primary-user', + 'mach:primary_group': 'primary-mach', + 'as:expected': { + # The domain-local groups appear as our primary GIDs, but do + # not appear in the base SIDs. + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'primary domain-local to service compressed', + 'groups': { + 'primary-user': (GroupType.DOMAIN_LOCAL, {user}), + 'primary-mach': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'primary_group': 'primary-user', + 'mach:primary_group': 'primary-mach', + 'as:expected': { + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'primary domain-local to service uncompressed', + 'groups': { + 'primary-user': (GroupType.DOMAIN_LOCAL, {user}), + 'primary-mach': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'primary_group': 'primary-user', + 'mach:primary_group': 'primary-mach', + 'as:expected': { + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'as:mach:expected': { + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # SID compression is unsupported. + 'tgs:compression': False, + 'tgs:expected': { + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Test the scenario where we belong to a now-domain-local group, and + # possess an old TGT issued when the group was still our primary one. + { + 'test': 'old primary domain-local to krbtgt', + 'groups': { + # Domain-local groups to which the accounts belong. + 'primary-user': (GroupType.DOMAIN_LOCAL, {user}), + 'primary-mach': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'tgs:user:sids': { + # In the PACs, the groups have the attributes of an ordinary + # group... + ('primary-user', SidType.BASE_SID, default_attrs), + # ...and remain our primary ones. + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + # The groups don't change. + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'old primary domain-local to service compressed', + 'groups': { + 'primary-user': (GroupType.DOMAIN_LOCAL, {user}), + 'primary-mach': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'tgs:user:sids': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + # The groups are added a second time to the PAC, now as + # resource groups. + ('primary-user', SidType.RESOURCE_SID, resource_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + frozenset([('primary-mach', SidType.RESOURCE_SID, resource_attrs)]), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'old primary domain-local to service uncompressed', + 'groups': { + 'primary-user': (GroupType.DOMAIN_LOCAL, {user}), + 'primary-mach': (GroupType.DOMAIN_LOCAL, {mach}), + }, + 'tgs:user:sids': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # SID compression is unsupported. + 'tgs:compression': False, + 'tgs:expected': { + ('primary-user', SidType.BASE_SID, default_attrs), + ('primary-user', SidType.PRIMARY_GID, None), + # This time, the group is added to Extra SIDs. + ('primary-user', SidType.EXTRA_SID, resource_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + ('primary-mach', SidType.BASE_SID, default_attrs), + ('primary-mach', SidType.PRIMARY_GID, None), + frozenset([('primary-mach', SidType.RESOURCE_SID, resource_attrs)]), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Test the scenario where each account possesses an old TGT issued when + # a now-domain-local group was still its primary one. The accounts no + # longer belong to those groups, which themselves belong to other + # domain-local groups. + { + 'test': 'old primary domain-local transitive to krbtgt', + 'groups': { + 'user-outer': (GroupType.DOMAIN_LOCAL, {'user-inner'}), + 'user-inner': (GroupType.DOMAIN_LOCAL, {}), + 'mach-outer': (GroupType.DOMAIN_LOCAL, {'mach-inner'}), + 'mach-inner': (GroupType.DOMAIN_LOCAL, {}), + }, + 'tgs:user:sids': { + # In the PACs, the groups have the attributes of an ordinary + # group... + ('user-inner', SidType.BASE_SID, default_attrs), + # ...and remain our primary ones. + ('user-inner', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + ('mach-inner', SidType.BASE_SID, default_attrs), + ('mach-inner', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + # The groups don't change. + ('user-inner', SidType.BASE_SID, default_attrs), + ('user-inner', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'old primary domain-local transitive to service compressed', + 'groups': { + 'user-outer': (GroupType.DOMAIN_LOCAL, {'user-inner'}), + 'user-inner': (GroupType.DOMAIN_LOCAL, {}), + 'mach-outer': (GroupType.DOMAIN_LOCAL, {'mach-inner'}), + 'mach-inner': (GroupType.DOMAIN_LOCAL, {}), + }, + 'tgs:user:sids': { + ('user-inner', SidType.BASE_SID, default_attrs), + ('user-inner', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + ('mach-inner', SidType.BASE_SID, default_attrs), + ('mach-inner', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + ('user-inner', SidType.BASE_SID, default_attrs), + ('user-inner', SidType.PRIMARY_GID, None), + # The second resource groups are added a second time to the PAC + # as resource groups. + ('user-outer', SidType.RESOURCE_SID, resource_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + ('mach-inner', SidType.BASE_SID, default_attrs), + ('mach-inner', SidType.PRIMARY_GID, None), + frozenset([('mach-outer', SidType.RESOURCE_SID, resource_attrs)]), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'old primary domain-local transitive to service uncompressed', + 'groups': { + 'user-outer': (GroupType.DOMAIN_LOCAL, {'user-inner'}), + 'user-inner': (GroupType.DOMAIN_LOCAL, {}), + 'mach-outer': (GroupType.DOMAIN_LOCAL, {'mach-inner'}), + 'mach-inner': (GroupType.DOMAIN_LOCAL, {}), + }, + 'tgs:user:sids': { + ('user-inner', SidType.BASE_SID, default_attrs), + ('user-inner', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + ('mach-inner', SidType.BASE_SID, default_attrs), + ('mach-inner', SidType.PRIMARY_GID, None), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # SID compression is unsupported. + 'tgs:compression': False, + 'tgs:expected': { + ('user-inner', SidType.BASE_SID, default_attrs), + ('user-inner', SidType.PRIMARY_GID, None), + # This time, the group is added to Extra SIDs. + ('user-outer', SidType.EXTRA_SID, resource_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + ('mach-inner', SidType.BASE_SID, default_attrs), + ('mach-inner', SidType.PRIMARY_GID, None), + frozenset([('mach-outer', SidType.RESOURCE_SID, resource_attrs)]), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + # Test how the various categories of SIDs are propagated into the + # device info structure. + { + 'test': 'device info sid grouping', + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # These base SIDs are simply propagated into the device info, + # irrespective of whatever attributes they have. + (1, SidType.BASE_SID, default_attrs), + (2, SidType.BASE_SID, 12345), + # Extra SIDs not from a domain are also propagated. + ('S-1-5-2-3-4', SidType.EXTRA_SID, 789), + ('S-1-5-20', SidType.EXTRA_SID, 999), + ('S-1-5-21', SidType.EXTRA_SID, 999), + ('S-1-6-0', SidType.EXTRA_SID, 999), + ('S-1-6-2-3-4', SidType.EXTRA_SID, 789), + # Extra SIDs from our own domain are collated into a group. + (3, SidType.EXTRA_SID, default_attrs), + (4, SidType.EXTRA_SID, 12345), + # Extra SIDs from other domains are collated into separate groups. + ('S-1-5-21-0-0-0-490', SidType.EXTRA_SID, 5), + ('S-1-5-21-0-0-0-491', SidType.EXTRA_SID, 6), + ('S-1-5-21-0-0-1-492', SidType.EXTRA_SID, 7), + ('S-1-5-21-0-0-1-493', SidType.EXTRA_SID, 8), + ('S-1-5-21-0-0-1-494', SidType.EXTRA_SID, 9), + # A non-domain SID (too few subauths), ... + ('S-1-5-21-242424-12345-2', SidType.EXTRA_SID, 1111111111), + # ... a domain SID, ... + ('S-1-5-21-242424-12345-321321-2', SidType.EXTRA_SID, 1111111111), + # ... and a non-domain SID (too many subauths). + ('S-1-5-21-242424-12345-321321-654321-2', SidType.EXTRA_SID, default_attrs), + # Special SIDs. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # Base SIDs. + (1, SidType.BASE_SID, default_attrs), + (2, SidType.BASE_SID, 12345), + # Extra SIDs from other domains. + ('S-1-5-2-3-4', SidType.EXTRA_SID, 789), + ('S-1-5-20', SidType.EXTRA_SID, 999), + ('S-1-5-21', SidType.EXTRA_SID, 999), + ('S-1-6-0', SidType.EXTRA_SID, 999), + ('S-1-6-2-3-4', SidType.EXTRA_SID, 789), + # Extra SIDs from our own domain. + frozenset({ + (3, SidType.RESOURCE_SID, default_attrs), + (4, SidType.RESOURCE_SID, 12345), + }), + # Extra SIDs from other domains. + frozenset({ + ('S-1-5-21-0-0-0-490', SidType.RESOURCE_SID, 5), + ('S-1-5-21-0-0-0-491', SidType.RESOURCE_SID, 6), + # These SIDs end up placed with the CLAIMS_VALID SID. + (security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs), + }), + frozenset({ + ('S-1-5-21-0-0-1-492', SidType.RESOURCE_SID, 7), + ('S-1-5-21-0-0-1-493', SidType.RESOURCE_SID, 8), + ('S-1-5-21-0-0-1-494', SidType.RESOURCE_SID, 9), + }), + # Non-domain SID. + ('S-1-5-21-242424-12345-2', SidType.EXTRA_SID, 1111111111), + # Domain SID. + frozenset({ + ('S-1-5-21-242424-12345-321321-2', SidType.RESOURCE_SID, 1111111111), + }), + # Non-domain SID. + ('S-1-5-21-242424-12345-321321-654321-2', SidType.EXTRA_SID, default_attrs), + # Special SIDs. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + }, + }, + { + # Test RODC-issued device claims. + 'test': 'rodc-issued device claims attack', + 'groups': { + # A couple of groups to which the machine belongs. + 'dom-local': (GroupType.DOMAIN_LOCAL, {mach}), + 'universal': (GroupType.UNIVERSAL, {mach}), + }, + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # Try to sneak a few extra SIDs into the machine's RODC-issued + # PAC. + (security.BUILTIN_RID_ADMINISTRATORS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_ENTERPRISE_READONLY_DCS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_KRBTGT, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_CERT_ADMINS, SidType.RESOURCE_SID, resource_attrs), + (security.SID_NT_SYSTEM, SidType.EXTRA_SID, default_attrs), + # Don't include the groups of which the machine is a member. + }, + # The armor ticket was issued by an RODC. + 'tgs:mach:from_rodc': True, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The machine's groups are now included. + ('universal', SidType.BASE_SID, default_attrs), + frozenset([ + ('dom-local', SidType.RESOURCE_SID, resource_attrs), + # Note that we're not considered a "member" of 'Allowed + # RODC Password Replication Group'. + ]), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + # The device groups should have been regenerated, our extra + # SIDs removed, and our elevation of privilege attack foiled. + }, + }, + { + 'test': 'rodc-issued without claims valid', + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + # The Claims Valid SID is missing. + }, + # The armor ticket was issued by an RODC. + 'tgs:mach:from_rodc': True, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + # The Claims Valid SID is still added to the device info. + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'rodc-issued without asserted identity', + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The Asserted Identity SID is missing. + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + # The armor ticket was issued by an RODC. + 'tgs:mach:from_rodc': True, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The Asserted Identity SID is not added to the device info. + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + { + 'test': 'rodc-issued asserted identity without attributes', + 'as:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:mach:sids': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The Asserted Identity SID has no attributes set. + (asserted_identity, SidType.EXTRA_SID, 0), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + # The armor ticket was issued by an RODC. + 'tgs:mach:from_rodc': True, + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:expected': { + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (compounded_auth, SidType.EXTRA_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:device:expected': { + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_DOMAIN_MEMBERS, SidType.PRIMARY_GID, None), + # The Asserted Identity SID appears in the device info with its + # attributes as normal. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + frozenset([(security.SID_CLAIMS_VALID, SidType.RESOURCE_SID, default_attrs)]), + }, + }, + ] + + @classmethod + def setUpDynamicTestCases(cls): + FILTER = env_get_var_value('FILTER', allow_missing=True) + SKIP_INVALID = env_get_var_value('SKIP_INVALID', allow_missing=True) + + for case in cls.cases: + invalid = case.pop('configuration_invalid', False) + if SKIP_INVALID and invalid: + # Some group setups are invalid on Windows, so we allow them to + # be skipped. + continue + name = case.pop('test') + name = re.sub(r'\W+', '_', name) + if FILTER and not re.search(FILTER, name): + continue + + cls.generate_dynamic_test('test_device_info', name, + dict(case)) + + def _test_device_info_with_args(self, case): + # The group arrangement for the test. + group_setup = case.pop('groups', None) + + # Groups that should be the primary group for the user and machine + # respectively. + primary_group = case.pop('primary_group', None) + mach_primary_group = case.pop('mach:primary_group', None) + + # Whether the TGS-REQ should be directed to the krbtgt. + tgs_to_krbtgt = case.pop('tgs:to_krbtgt', None) + + # Whether the target server of the TGS-REQ should support compound + # identity or resource SID compression. + tgs_compound_id = case.pop('tgs:compound_id', None) + tgs_compression = case.pop('tgs:compression', None) + + # Optional SIDs to replace those in the PACs prior to a TGS-REQ. + tgs_user_sids = case.pop('tgs:user:sids', None) + tgs_mach_sids = case.pop('tgs:mach:sids', None) + + # Whether the machine's TGT should be issued by an RODC. + tgs_mach_from_rodc = case.pop('tgs:mach:from_rodc', None) + + # Optional groups which the machine is added to or removed from prior + # to a TGS-REQ, to test how the groups in the device PAC are expanded. + tgs_mach_added = case.pop('tgs:mach:added', None) + tgs_mach_removed = case.pop('tgs:mach:removed', None) + + # Optional account SIDs to replace those in the PACs prior to a + # TGS-REQ. + tgs_user_sid = case.pop('tgs:user_sid', None) + tgs_mach_sid = case.pop('tgs:mach_sid', None) + + # User flags that may be set or reset in the PAC prior to a TGS-REQ. + tgs_mach_set_user_flags = case.pop('tgs:mach:set_user_flags', None) + tgs_mach_reset_user_flags = case.pop('tgs:mach:reset_user_flags', None) + + # The SIDs we expect to see in the PAC after a AS-REQ or a TGS-REQ. + as_expected = case.pop('as:expected', None) + as_mach_expected = case.pop('as:mach:expected', None) + tgs_expected = case.pop('tgs:expected', None) + tgs_device_expected = case.pop('tgs:device:expected', None) + + # There should be no parameters remaining in the testcase. + self.assertFalse(case, 'unexpected parameters in testcase') + + if as_expected is None: + self.assertIsNotNone(tgs_expected, + 'no set of expected SIDs is provided') + + if as_mach_expected is None: + self.assertIsNotNone(tgs_expected, + 'no set of expected machine SIDs is provided') + + if tgs_to_krbtgt is None: + tgs_to_krbtgt = False + + if tgs_compound_id is None and not tgs_to_krbtgt: + # Assume the service supports compound identity by default. + tgs_compound_id = True + + if tgs_to_krbtgt: + self.assertIsNone(tgs_device_expected, + 'device SIDs are not added for a krbtgt request') + + self.assertIsNotNone(tgs_expected, + 'no set of expected TGS SIDs is provided') + + if tgs_user_sid is not None: + self.assertIsNotNone(tgs_user_sids, + 'specified TGS-REQ user SID, but no ' + 'accompanying user SIDs provided') + + if tgs_mach_sid is not None: + self.assertIsNotNone(tgs_mach_sids, + 'specified TGS-REQ mach SID, but no ' + 'accompanying machine SIDs provided') + + if tgs_mach_set_user_flags is None: + tgs_mach_set_user_flags = 0 + else: + self.assertIsNotNone(tgs_mach_sids, + 'specified TGS-REQ set user flags, but no ' + 'accompanying machine SIDs provided') + + if tgs_mach_reset_user_flags is None: + tgs_mach_reset_user_flags = 0 + else: + self.assertIsNotNone(tgs_mach_sids, + 'specified TGS-REQ reset user flags, but no ' + 'accompanying machine SIDs provided') + + if tgs_mach_from_rodc is None: + tgs_mach_from_rodc = False + + user_use_cache = not group_setup and ( + not primary_group) + mach_use_cache = not group_setup and ( + not mach_primary_group) and ( + not tgs_mach_added) and ( + not tgs_mach_removed) + + samdb = self.get_samdb() + + domain_sid = samdb.get_domain_sid() + + # Create the user account. It needs to be freshly created rather than + # cached if there is a possibility of adding it to one or more groups. + user_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=user_use_cache) + user_dn = user_creds.get_dn() + user_sid = user_creds.get_sid() + user_name = user_creds.get_username() + + trust_user_rid = random.randint(2000, 0xfffffffe) + trust_user_sid = f'{self.user_trust_domain}-{trust_user_rid}' + + trust_mach_rid = random.randint(2000, 0xfffffffe) + trust_mach_sid = f'{self.mach_trust_domain}-{trust_mach_rid}' + + # Create the machine account. It needs to be freshly created rather + # than cached if there is a possibility of adding it to one or more + # groups. + if tgs_mach_from_rodc: + # If the machine's TGT is to be issued by an RODC, ensure the + # machine account is allowed to replicate to an RODC. + mach_opts = { + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + } + else: + mach_opts = None + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts=mach_opts, + use_cache=mach_use_cache) + mach_dn = mach_creds.get_dn() + mach_dn_str = str(mach_dn) + mach_sid = mach_creds.get_sid() + + user_principal = Principal(user_dn, user_sid) + mach_principal = Principal(mach_dn, mach_sid) + trust_user_principal = Principal(None, trust_user_sid) + trust_mach_principal = Principal(None, trust_mach_sid) + preexisting_groups = { + self.user: user_principal, + self.mach: mach_principal, + self.trust_user: trust_user_principal, + self.trust_mach: trust_mach_principal, + } + primary_groups = {} + if primary_group is not None: + primary_groups[user_principal] = primary_group + if mach_primary_group is not None: + primary_groups[mach_principal] = mach_primary_group + groups = self.setup_groups(samdb, + preexisting_groups, + group_setup, + primary_groups) + del group_setup + + if tgs_user_sid is None: + tgs_user_sid = user_sid + elif tgs_user_sid in groups: + tgs_user_sid = groups[tgs_user_sid].sid + + tgs_user_domain_sid, tgs_user_rid = tgs_user_sid.rsplit('-', 1) + + if tgs_mach_sid is None: + tgs_mach_sid = mach_sid + elif tgs_mach_sid in groups: + tgs_mach_sid = groups[tgs_mach_sid].sid + + tgs_mach_domain_sid, tgs_mach_rid = tgs_mach_sid.rsplit('-', 1) + + expected_groups = self.map_sids(as_expected, groups, + domain_sid) + mach_expected_groups = self.map_sids(as_mach_expected, groups, + domain_sid) + tgs_user_sids_mapped = self.map_sids(tgs_user_sids, groups, + tgs_user_domain_sid) + tgs_mach_sids_mapped = self.map_sids(tgs_mach_sids, groups, + tgs_mach_domain_sid) + tgs_expected_mapped = self.map_sids(tgs_expected, groups, + tgs_user_domain_sid) + tgs_device_expected_mapped = self.map_sids(tgs_device_expected, groups, + tgs_mach_domain_sid) + + user_tgt = self.get_tgt(user_creds, + expected_groups=expected_groups, + unexpected_groups=None) + + mach_tgt = self.get_tgt(mach_creds, + expected_groups=mach_expected_groups, + unexpected_groups=None) + + if tgs_user_sids is not None: + # Replace the SIDs in the user's PAC with the ones provided by the + # test. + user_tgt = self.ticket_with_sids(user_tgt, + tgs_user_sids_mapped, + tgs_user_domain_sid, + tgs_user_rid) + + if tgs_mach_sids is not None: + # Replace the SIDs in the machine's PAC with the ones provided by + # the test. + mach_tgt = self.ticket_with_sids(mach_tgt, + tgs_mach_sids_mapped, + tgs_mach_domain_sid, + tgs_mach_rid, + set_user_flags=tgs_mach_set_user_flags, + reset_user_flags=tgs_mach_reset_user_flags, + from_rodc=tgs_mach_from_rodc) + elif tgs_mach_from_rodc: + mach_tgt = self.issued_by_rodc(mach_tgt) + + if tgs_mach_removed is not None: + for removed in tgs_mach_removed: + group_dn = self.map_to_dn(removed, groups, domain_sid=None) + self.remove_from_group(mach_dn, group_dn) + + if tgs_mach_added is not None: + for added in tgs_mach_added: + group_dn = self.map_to_dn(added, groups, domain_sid=None) + self.add_to_group(mach_dn_str, group_dn, 'member', + expect_attr=False) + + subkey = self.RandomKey(user_tgt.session_key.etype) + + armor_subkey = self.RandomKey(subkey.etype) + explicit_armor_key = self.generate_armor_key(armor_subkey, + mach_tgt.session_key) + armor_key = kcrypto.cf2(explicit_armor_key.key, + subkey.key, + b'explicitarmor', + b'tgsarmor') + armor_key = Krb5EncryptionKey(armor_key, None) + + target_creds, sname = self.get_target( + to_krbtgt=tgs_to_krbtgt, + compound_id=tgs_compound_id, + compression=tgs_compression) + srealm = target_creds.get_realm() + + decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + target_supported_etypes = target_creds.tgs_supported_enctypes + + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + kdc_options = '0' + pac_options = '1' # claims support + + requester_sid = None + if tgs_to_krbtgt: + requester_sid = user_sid + + expect_resource_groups_flag = None + if tgs_mach_reset_user_flags & netlogon.NETLOGON_RESOURCE_GROUPS: + expect_resource_groups_flag = False + elif tgs_mach_set_user_flags & netlogon.NETLOGON_RESOURCE_GROUPS: + expect_resource_groups_flag = True + + # Perform a TGS-REQ with the user account. + + kdc_exchange_dict = self.tgs_exchange_dict( + creds=user_creds, + expected_crealm=user_tgt.crealm, + expected_cname=user_tgt.cname, + expected_srealm=srealm, + expected_sname=sname, + expected_account_name=user_name, + ticket_decryption_key=decryption_key, + generate_fast_fn=self.generate_simple_fast, + generate_fast_armor_fn=self.generate_ap_req, + check_rep_fn=self.generic_check_kdc_rep, + check_kdc_private_fn=self.generic_check_kdc_private, + tgt=user_tgt, + armor_key=armor_key, + armor_tgt=mach_tgt, + armor_subkey=armor_subkey, + pac_options=pac_options, + authenticator_subkey=subkey, + kdc_options=kdc_options, + expect_pac=True, + expect_pac_attrs=tgs_to_krbtgt, + expect_pac_attrs_pac_request=tgs_to_krbtgt, + expected_sid=tgs_user_sid, + expected_requester_sid=requester_sid, + expected_domain_sid=tgs_user_domain_sid, + expected_device_domain_sid=tgs_mach_domain_sid, + expected_supported_etypes=target_supported_etypes, + expect_resource_groups_flag=expect_resource_groups_flag, + expected_groups=tgs_expected_mapped, + expect_device_info=bool(tgs_compound_id), + expected_device_groups=tgs_device_expected_mapped) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=srealm, + sname=sname, + etypes=etypes) + self.check_reply(rep, KRB_TGS_REP) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/etype_tests.py b/python/samba/tests/krb5/etype_tests.py new file mode 100755 index 0000000..7ac76f9 --- /dev/null +++ b/python/samba/tests/krb5/etype_tests.py @@ -0,0 +1,597 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2022 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import itertools + +from samba.dcerpc import security + +from samba.tests import DynamicTestCase +from samba.tests.krb5.kdc_tgs_tests import KdcTgsBaseTests +from samba.tests.krb5.raw_testcase import KerberosCredentials +from samba.tests.krb5.rfc4120_constants import ( + AES128_CTS_HMAC_SHA1_96, + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_ETYPE_NOSUPP, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + + +global_asn1_print = False +global_hexdump = False + +des_bits = security.KERB_ENCTYPE_DES_CBC_MD5 | security.KERB_ENCTYPE_DES_CBC_CRC +rc4_bit = security.KERB_ENCTYPE_RC4_HMAC_MD5 +aes128_bit = security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96 +aes256_bit = security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96 +aes256_sk_bit = security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK +fast_bit = security.KERB_ENCTYPE_FAST_SUPPORTED + +etype_bits = rc4_bit | aes128_bit | aes256_bit +extra_bits = aes256_sk_bit | fast_bit + + +@DynamicTestCase +class EtypeTests(KdcTgsBaseTests): + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + self.default_supported_enctypes = self.default_etypes + if self.default_supported_enctypes is None: + lp = self.get_lp() + self.default_supported_enctypes = lp.get( + 'kdc default domain supported enctypes') + if self.default_supported_enctypes == 0: + self.default_supported_enctypes = rc4_bit | aes256_sk_bit + + def _server_creds(self, supported=None, force_nt4_hash=False, + account_type=None): + if account_type is None: + account_type= self.AccountType.COMPUTER + return self.get_cached_creds( + account_type=account_type, + opts={ + 'supported_enctypes': supported, + 'force_nt4_hash': force_nt4_hash, + }) + + def only_non_etype_bits_set(self, bits): + return bits is not None and ( + bits & extra_bits and + not (bits & etype_bits)) + + @classmethod + def setUpDynamicTestCases(cls): + all_etypes = (AES256_CTS_HMAC_SHA1_96, + AES128_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5) + + # An iterator yielding all permutations consisting of at least one + # etype. + requested_etype_cases = itertools.chain.from_iterable( + itertools.permutations(all_etypes, x) + for x in range(1, len(all_etypes) + 1)) + + # Some combinations of msDS-SupportedEncryptionTypes bits to be set on + # the target server. + supported_etype_cases = ( + # Not set. + None, + # Every possible combination of RC4, AES128, AES256, and AES256-SK. + 0, + rc4_bit, + aes256_sk_bit, + aes256_sk_bit | rc4_bit, + aes256_bit, + aes256_bit | rc4_bit, + aes256_bit | aes256_sk_bit, + aes256_bit | aes256_sk_bit | rc4_bit, + aes128_bit, + aes128_bit | rc4_bit, + aes128_bit | aes256_sk_bit, + aes128_bit | aes256_sk_bit | rc4_bit, + aes128_bit | aes256_bit, + aes128_bit | aes256_bit | rc4_bit, + aes128_bit | aes256_bit | aes256_sk_bit, + aes128_bit | aes256_bit | aes256_sk_bit | rc4_bit, + # Some combinations with an extra bit (the FAST-supported bit) set. + fast_bit, + fast_bit | rc4_bit, + fast_bit | aes256_sk_bit, + fast_bit | aes256_bit, + ) + + for _requested_etypes in requested_etype_cases: + _s = str(_requested_etypes) + _t = _s.maketrans(",", "_", "( )") + requested_etypes = _s.translate(_t) + + for _supported_etypes in supported_etype_cases: + if _supported_etypes is None: + supported_etypes = "None" + else: + supported_etypes = f'0x{_supported_etypes:X}' + + for account_type in ["member", "dc"]: + if account_type == "dc": + _account_type = cls.AccountType.SERVER + elif account_type == "member": + _account_type = cls.AccountType.COMPUTER + + for stored_type in ["aes_rc4", "rc4_only"]: + if stored_type == "aes_rc4": + force_nt4_hash = False + elif stored_type == "rc4_only": + force_nt4_hash = True + + tname = (f'{supported_etypes}_supported_' + f'{requested_etypes}_requested_' + f'{account_type}_account_' + f'stored_{stored_type}') + targs = _supported_etypes, _requested_etypes, _account_type, force_nt4_hash + cls.generate_dynamic_test('test_etype_as', tname, *targs) + cls.generate_dynamic_test('test_etype_tgs', tname, *targs) + + def _test_etype_as_with_args(self, supported_bits, requested_etypes, account_type, force_nt4_hash): + # The ticket will be encrypted with the strongest enctype for which the + # server explicitly declares support, falling back to RC4 if the server + # has no declared supported encryption types. The enctype of the + # session key is the first enctype listed in the request that the + # server supports, taking the AES-SK bit as an indication of support + # for both AES types. + + # If none of the enctypes in the request are supported by the target + # server, implicitly or explicitly, return ETYPE_NOSUPP. + + expected_error = 0 + + if not supported_bits: + # If msDS-SupportedEncryptionTypes is missing or set to zero, the + # default value, provided by smb.conf, is assumed. + supported_bits = self.default_supported_enctypes + + # If msDS-SupportedEncryptionTypes specifies only non-etype bits, we + # expect an error. + if self.only_non_etype_bits_set(supported_bits): + expected_error = KDC_ERR_ETYPE_NOSUPP + + virtual_bits = supported_bits + + if self.forced_rc4 and not (virtual_bits & rc4_bit): + # If our fallback smb.conf option is set, force in RC4 support. + virtual_bits |= rc4_bit + + if force_nt4_hash and not (virtual_bits & rc4_bit): + virtual_bits |= rc4_bit + + if virtual_bits & aes256_sk_bit: + # If strong session keys are enabled, force in the AES bits. + virtual_bits |= aes256_bit | aes128_bit + + if account_type == self.AccountType.SERVER: + virtual_bits |= etype_bits + expected_error = 0 + + virtual_etypes = KerberosCredentials.bits_to_etypes(virtual_bits) + + # The enctype of the session key is the first listed in the request + # that the server supports, implicitly or explicitly. + for requested_etype in requested_etypes: + if requested_etype in virtual_etypes: + expected_session_etype = requested_etype + break + else: + # If there is no such enctype, expect an error. + expected_error = KDC_ERR_ETYPE_NOSUPP + + # Get the credentials of the client and server accounts. + creds = self.get_client_creds() + target_creds = self._server_creds(supported=supported_bits, + account_type=account_type, + force_nt4_hash=force_nt4_hash) + if account_type == self.AccountType.SERVER: + target_supported_etypes = target_creds.tgs_supported_enctypes + target_supported_etypes |= des_bits + target_supported_etypes |= etype_bits + target_creds.set_tgs_supported_enctypes(target_supported_etypes) + supported_bits |= (target_supported_etypes & etype_bits) + + # We expect the ticket etype to be the strongest the server claims to + # support, with a fallback to RC4. + expected_etype = ARCFOUR_HMAC_MD5 + if not force_nt4_hash and supported_bits is not None: + if supported_bits & aes256_bit: + expected_etype = AES256_CTS_HMAC_SHA1_96 + elif supported_bits & aes128_bit: + expected_etype = AES128_CTS_HMAC_SHA1_96 + + # Perform the AS-REQ. + ticket = self._as_req(creds, expected_error=expected_error, + target_creds=target_creds, + etype=requested_etypes, + expected_ticket_etype=expected_etype) + if expected_error: + # There's no more to check. Return. + return + + # Check the etypes of the ticket and session key. + self.assertEqual(expected_etype, ticket.decryption_key.etype) + self.assertEqual(expected_session_etype, ticket.session_key.etype) + + def _test_etype_tgs_with_args(self, supported_bits, requested_etypes, account_type, force_nt4_hash): + expected_error = 0 + + if not supported_bits: + # If msDS-SupportedEncryptionTypes is missing or set to zero, the + # default value, provided by smb.conf, is assumed. + supported_bits = self.default_supported_enctypes + + # If msDS-SupportedEncryptionTypes specifies only non-etype bits, we + # expect an error. + if self.only_non_etype_bits_set(supported_bits): + expected_error = KDC_ERR_ETYPE_NOSUPP + + virtual_bits = supported_bits + + if self.forced_rc4 and not (virtual_bits & rc4_bit): + # If our fallback smb.conf option is set, force in RC4 support. + virtual_bits |= rc4_bit + + if force_nt4_hash and not (virtual_bits & rc4_bit): + virtual_bits |= rc4_bit + + if virtual_bits & aes256_sk_bit: + # If strong session keys are enabled, force in the AES bits. + virtual_bits |= aes256_bit | aes128_bit + + if account_type == self.AccountType.SERVER: + virtual_bits |= etype_bits + expected_error = 0 + + virtual_etypes = KerberosCredentials.bits_to_etypes(virtual_bits) + + # The enctype of the session key is the first listed in the request + # that the server supports, implicitly or explicitly. + for requested_etype in requested_etypes: + if requested_etype in virtual_etypes: + expected_session_etype = requested_etype + break + else: + # If there is no such enctype, expect an error. + expected_error = KDC_ERR_ETYPE_NOSUPP + + # Get the credentials of the client and server accounts. + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + target_creds = self._server_creds(supported=supported_bits, + account_type=account_type, + force_nt4_hash=force_nt4_hash) + if account_type == self.AccountType.SERVER: + target_supported_etypes = target_creds.tgs_supported_enctypes + target_supported_etypes |= des_bits + target_supported_etypes |= etype_bits + target_creds.set_tgs_supported_enctypes(target_supported_etypes) + supported_bits |= (target_supported_etypes & etype_bits) + + # We expect the ticket etype to be the strongest the server claims to + # support, with a fallback to RC4. + expected_etype = ARCFOUR_HMAC_MD5 + if not force_nt4_hash and supported_bits is not None: + if supported_bits & aes256_bit: + expected_etype = AES256_CTS_HMAC_SHA1_96 + elif supported_bits & aes128_bit: + expected_etype = AES128_CTS_HMAC_SHA1_96 + + # Perform the TGS-REQ. + ticket = self._tgs_req(tgt, expected_error=expected_error, + creds=creds, target_creds=target_creds, + kdc_options=str(krb5_asn1.KDCOptions('canonicalize')), + expected_supported_etypes=target_creds.tgs_supported_enctypes, + expected_ticket_etype=expected_etype, + etypes=requested_etypes) + if expected_error: + # There's no more to check. Return. + return + + # Check the etypes of the ticket and session key. + self.assertEqual(expected_etype, ticket.decryption_key.etype) + self.assertEqual(expected_session_etype, ticket.session_key.etype) + + # Perform an AS-REQ for a service ticket, specifying AES, when the target + # service only supports AES. The resulting ticket should be encrypted with + # AES, with an AES session key. + def test_as_aes_supported_aes_requested(self): + creds = self.get_client_creds() + target_creds = self._server_creds(supported=aes256_bit) + + ticket = self._as_req(creds, expected_error=0, + target_creds=target_creds, + etype=(AES256_CTS_HMAC_SHA1_96,)) + + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.decryption_key.etype) + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.session_key.etype) + + # Perform an AS-REQ for a service ticket, specifying RC4, when the target + # service only supports AES. The request should fail with an error. + def test_as_aes_supported_rc4_requested(self): + creds = self.get_client_creds() + target_creds = self._server_creds(supported=aes256_bit) + + if self.forced_rc4: + expected_error = 0 + expected_session_etype = ARCFOUR_HMAC_MD5 + else: + expected_error = KDC_ERR_ETYPE_NOSUPP + expected_session_etype = AES256_CTS_HMAC_SHA1_96 + + ticket = self._as_req(creds, expected_error=expected_error, + target_creds=target_creds, + etype=(ARCFOUR_HMAC_MD5,)) + + if not self.forced_rc4: + return + + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.decryption_key.etype) + self.assertEqual(expected_session_etype, ticket.session_key.etype) + + # Perform an AS-REQ for a service ticket, specifying AES, when the target + # service only supports AES, and supports AES256 session keys. The + # resulting ticket should be encrypted with AES, with an AES session key. + def test_as_aes_supported_aes_session_aes_requested(self): + creds = self.get_client_creds() + target_creds = self._server_creds(supported=aes256_bit | aes256_sk_bit) + + ticket = self._as_req(creds, expected_error=0, + target_creds=target_creds, + etype=(AES256_CTS_HMAC_SHA1_96,)) + + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.decryption_key.etype) + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.session_key.etype) + + # Perform an AS-REQ for a service ticket, specifying RC4, when the target + # service only supports AES, and supports AES256 session keys. The request + # should fail with an error. + def test_as_aes_supported_aes_session_rc4_requested(self): + creds = self.get_client_creds() + target_creds = self._server_creds(supported=aes256_bit | aes256_sk_bit) + + if self.forced_rc4: + expected_error = 0 + expected_session_etype = ARCFOUR_HMAC_MD5 + else: + expected_error = KDC_ERR_ETYPE_NOSUPP + expected_session_etype = AES256_CTS_HMAC_SHA1_96 + + ticket = self._as_req(creds, expected_error=expected_error, + target_creds=target_creds, + etype=(ARCFOUR_HMAC_MD5,)) + + if not self.forced_rc4: + return + + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.decryption_key.etype) + self.assertEqual(expected_session_etype, ticket.session_key.etype) + + # Perform an AS-REQ for a service ticket, specifying AES, when the target + # service only supports RC4. The request should fail with an error. + def test_as_rc4_supported_aes_requested(self): + creds = self.get_client_creds() + target_creds = self._server_creds(supported=rc4_bit) + + self._as_req(creds, expected_error=KDC_ERR_ETYPE_NOSUPP, + target_creds=target_creds, + etype=(AES256_CTS_HMAC_SHA1_96,)) + + # Perform an AS-REQ for a service ticket, specifying RC4, when the target + # service only supports RC4. The resulting ticket should be encrypted with + # RC4, with an RC4 session key. + def test_as_rc4_supported_rc4_requested(self): + creds = self.get_client_creds() + target_creds = self._server_creds(supported=rc4_bit) + + ticket = self._as_req(creds, expected_error=0, + target_creds=target_creds, + etype=(ARCFOUR_HMAC_MD5,)) + + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.decryption_key.etype) + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.session_key.etype) + + # Perform an AS-REQ for a service ticket, specifying AES, when the target + # service only supports RC4, but supports AES256 session keys. The + # resulting ticket should be encrypted with RC4, with an AES256 session + # key. + def test_as_rc4_supported_aes_session_aes_requested(self): + creds = self.get_client_creds() + target_creds = self._server_creds(supported=rc4_bit | aes256_sk_bit) + + ticket = self._as_req(creds, expected_error=0, + target_creds=target_creds, + etype=(AES256_CTS_HMAC_SHA1_96,)) + + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.decryption_key.etype) + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.session_key.etype) + + # Perform an AS-REQ for a service ticket, specifying RC4, when the target + # service only supports RC4, but supports AES256 session keys. The + # resulting ticket should be encrypted with RC4, with an RC4 session key. + def test_as_rc4_supported_aes_session_rc4_requested(self): + creds = self.get_client_creds() + target_creds = self._server_creds(supported=rc4_bit | aes256_sk_bit) + + ticket = self._as_req(creds, expected_error=0, + target_creds=target_creds, + etype=(ARCFOUR_HMAC_MD5,)) + + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.decryption_key.etype) + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.session_key.etype) + + # Perform a TGS-REQ for a service ticket, specifying AES, when the target + # service only supports AES. The resulting ticket should be encrypted with + # AES, with an AES session key. + def test_tgs_aes_supported_aes_requested(self): + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + + target_creds = self._server_creds(supported=aes256_bit) + + ticket = self._tgs_req(tgt, expected_error=0, + creds=creds, target_creds=target_creds, + etypes=(AES256_CTS_HMAC_SHA1_96,)) + + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.decryption_key.etype) + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.session_key.etype) + + # Perform a TGS-REQ for a service ticket, specifying RC4, when the target + # service only supports AES. The request should fail with an error. + def test_tgs_aes_supported_rc4_requested(self): + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + + target_creds = self._server_creds(supported=aes256_bit) + + if self.forced_rc4: + expected_error = 0 + else: + expected_error = KDC_ERR_ETYPE_NOSUPP + + ticket = self._tgs_req(tgt, expected_error=expected_error, + creds=creds, target_creds=target_creds, + etypes=(ARCFOUR_HMAC_MD5,)) + + if not self.forced_rc4: + return + + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.decryption_key.etype) + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.session_key.etype) + + # Perform a TGS-REQ for a service ticket, specifying AES, when the target + # service only supports AES, and supports AES256 session keys. The + # resulting ticket should be encrypted with AES, with an AES session key. + def test_tgs_aes_supported_aes_session_aes_requested(self): + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + + target_creds = self._server_creds(supported=aes256_bit | aes256_sk_bit) + + ticket = self._tgs_req(tgt, expected_error=0, + creds=creds, target_creds=target_creds, + etypes=(AES256_CTS_HMAC_SHA1_96,)) + + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.decryption_key.etype) + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.session_key.etype) + + # Perform a TGS-REQ for a service ticket, specifying RC4, when the target + # service only supports AES, and supports AES256 session keys. The request + # should fail with an error. + def test_tgs_aes_supported_aes_session_rc4_requested(self): + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + + target_creds = self._server_creds(supported=aes256_bit | aes256_sk_bit) + + if self.forced_rc4: + expected_error = 0 + else: + expected_error = KDC_ERR_ETYPE_NOSUPP + + ticket = self._tgs_req(tgt, expected_error=expected_error, + creds=creds, target_creds=target_creds, + etypes=(ARCFOUR_HMAC_MD5,)) + + if not self.forced_rc4: + return + + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.decryption_key.etype) + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.session_key.etype) + + # Perform a TGS-REQ for a service ticket, specifying AES, when the target + # service only supports RC4. The request should fail with an error. + def test_tgs_rc4_supported_aes_requested(self): + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + + target_creds = self._server_creds(supported=rc4_bit) + + self._tgs_req(tgt, expected_error=KDC_ERR_ETYPE_NOSUPP, + creds=creds, target_creds=target_creds, + etypes=(AES256_CTS_HMAC_SHA1_96,)) + + # Perform a TGS-REQ for a service ticket, specifying RC4, when the target + # service only supports RC4. The resulting ticket should be encrypted with + # RC4, with an RC4 session key. + def test_tgs_rc4_supported_rc4_requested(self): + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + + target_creds = self._server_creds(supported=rc4_bit) + + ticket = self._tgs_req(tgt, expected_error=0, + creds=creds, target_creds=target_creds, + etypes=(ARCFOUR_HMAC_MD5,)) + + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.decryption_key.etype) + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.session_key.etype) + + # Perform a TGS-REQ for a service ticket, specifying AES, when the target + # service only supports RC4, but supports AES256 session keys. The + # resulting ticket should be encrypted with RC4, with an AES256 session + # key. + def test_tgs_rc4_supported_aes_session_aes_requested(self): + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + + target_creds = self._server_creds(supported=rc4_bit | aes256_sk_bit) + + ticket = self._tgs_req(tgt, expected_error=0, + creds=creds, target_creds=target_creds, + etypes=(AES256_CTS_HMAC_SHA1_96,)) + + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.decryption_key.etype) + self.assertEqual(AES256_CTS_HMAC_SHA1_96, ticket.session_key.etype) + + # Perform a TGS-REQ for a service ticket, specifying RC4, when the target + # service only supports RC4, but supports AES256 session keys. The + # resulting ticket should be encrypted with RC4, with an RC4 session key. + def test_tgs_rc4_supported_aes_session_rc4_requested(self): + creds = self.get_client_creds() + tgt = self.get_tgt(creds) + + target_creds = self._server_creds(supported=rc4_bit | aes256_sk_bit) + + ticket = self._tgs_req(tgt, expected_error=0, + creds=creds, target_creds=target_creds, + etypes=(ARCFOUR_HMAC_MD5,)) + + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.decryption_key.etype) + self.assertEqual(ARCFOUR_HMAC_MD5, ticket.session_key.etype) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/fast_tests.py b/python/samba/tests/krb5/fast_tests.py new file mode 100755 index 0000000..3feafc2 --- /dev/null +++ b/python/samba/tests/krb5/fast_tests.py @@ -0,0 +1,2108 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2020 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import functools +import collections + +import ldb + +from samba.dcerpc import krb5pac, security +from samba.tests.krb5.raw_testcase import Krb5EncryptionKey, ZeroedChecksumKey +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.rfc4120_constants import ( + AD_FX_FAST_ARMOR, + AD_FX_FAST_USED, + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + FX_FAST_ARMOR_AP_REQUEST, + KDC_ERR_BAD_INTEGRITY, + KDC_ERR_ETYPE_NOSUPP, + KDC_ERR_GENERIC, + KDC_ERR_S_PRINCIPAL_UNKNOWN, + KDC_ERR_MODIFIED, + KDC_ERR_NOT_US, + KDC_ERR_POLICY, + KDC_ERR_PREAUTH_FAILED, + KDC_ERR_PREAUTH_REQUIRED, + KDC_ERR_SKEW, + KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS, + KRB_AS_REP, + KRB_TGS_REP, + KU_TGS_REQ_AUTH_DAT_SESSION, + KU_TGS_REQ_AUTH_DAT_SUBKEY, + NT_PRINCIPAL, + NT_SRV_HST, + NT_SRV_INST, + PADATA_FX_COOKIE, + PADATA_FX_FAST, + PADATA_REQ_ENC_PA_REP, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +import samba.tests.krb5.kcrypto as kcrypto + +global_asn1_print = False +global_hexdump = False + + +class FAST_Tests(KDCBaseTest): + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def test_simple(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_padata_fn': self.generate_enc_timestamp_padata + } + ]) + + def test_simple_as_req_self(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False, + 'as_req_self': True + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_padata_fn': self.generate_enc_timestamp_padata, + 'as_req_self': True + } + ], client_account=self.AccountType.COMPUTER) + + def test_simple_as_req_self_no_auth_data(self): + self._run_test_sequence( + [ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False, + 'as_req_self': True + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_padata_fn': self.generate_enc_timestamp_padata, + 'as_req_self': True, + 'expect_pac': True + } + ], + client_account=self.AccountType.COMPUTER, + client_opts={'no_auth_data_required': True}) + + def test_simple_as_req_self_pac_request_false(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False, + 'as_req_self': True + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_padata_fn': self.generate_enc_timestamp_padata, + 'as_req_self': True, + 'pac_request': False, + 'expect_pac': False + } + ], client_account=self.AccountType.COMPUTER) + + def test_simple_as_req_self_pac_request_none(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False, + 'as_req_self': True + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_padata_fn': self.generate_enc_timestamp_padata, + 'as_req_self': True, + 'pac_request': None, + 'expect_pac': True + } + ], client_account=self.AccountType.COMPUTER) + + def test_simple_as_req_self_pac_request_true(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False, + 'as_req_self': True + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_padata_fn': self.generate_enc_timestamp_padata, + 'as_req_self': True, + 'pac_request': True, + 'expect_pac': True + } + ], client_account=self.AccountType.COMPUTER) + + def test_simple_tgs(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_tgt_fn': self.get_user_tgt + } + ]) + + def test_fast_rodc_issued_armor(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_rodc_issued_mach_tgt, + }, + { + 'rep_type': KRB_AS_REP, + # Test that RODC-issued armor tickets are permitted. + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_rodc_issued_mach_tgt, + } + ], + armor_opts={ + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + }) + + def test_fast_tgs_rodc_issued_armor(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + # Test that RODC-issued armor tickets are not permitted. + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'gen_armor_tgt_fn': self.get_rodc_issued_mach_tgt, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + } + ], + armor_opts={ + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + }) + + def test_simple_enc_pa_rep(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_padata_fn': self.generate_enc_pa_rep_timestamp_padata, + 'expected_flags': 'enc-pa-rep' + } + ]) + + # Currently we only send PADATA-REQ-ENC-PA-REP for AS-REQ requests. + def test_simple_tgs_enc_pa_rep(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_tgt_fn': self.get_user_tgt, + 'gen_padata_fn': self.generate_enc_pa_rep_padata, + 'expected_flags': 'enc-pa-rep' + } + ]) + + def test_simple_no_sname(self): + expected_sname = self.get_krbtgt_sname() + + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': False, + 'sname': None, + 'expected_sname': expected_sname, + 'expect_edata': False + } + ]) + + def test_simple_tgs_no_sname(self): + expected_sname = self.get_krbtgt_sname() + + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': False, + 'gen_tgt_fn': self.get_user_tgt, + 'sname': None, + 'expected_sname': expected_sname, + 'expect_edata': False + } + ]) + + def test_fast_no_sname(self): + expected_sname = self.get_krbtgt_sname() + + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, + KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'sname': None, + 'expected_sname': expected_sname, + 'strict_edata_checking': False + } + ]) + + def test_fast_tgs_no_sname(self): + expected_sname = self.get_krbtgt_sname() + + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'sname': None, + 'expected_sname': expected_sname, + 'strict_edata_checking': False + } + ]) + + def test_fast_inner_no_sname(self): + expected_sname = self.get_krbtgt_sname() + + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, + KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'inner_req': { + 'sname': None # should be ignored + }, + 'expected_sname': expected_sname, + 'strict_edata_checking': False + } + ]) + + def test_fast_tgs_inner_no_sname(self): + expected_sname = self.get_krbtgt_sname() + + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, + KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'inner_req': { + 'sname': None # should be ignored + }, + 'expected_sname': expected_sname, + 'strict_edata_checking': False + } + ]) + + def test_simple_tgs_wrong_principal(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_tgt_fn': self.get_mach_tgt + } + ]) + + def test_simple_tgs_service_ticket(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_NOT_US, + KDC_ERR_POLICY), + 'use_fast': False, + 'gen_tgt_fn': self.get_user_service_ticket, + 'expect_edata': False + } + ]) + + def test_simple_tgs_service_ticket_mach(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_NOT_US, + KDC_ERR_POLICY), + 'use_fast': False, + 'gen_tgt_fn': self.get_mach_service_ticket, + 'expect_edata': False + } + ]) + + def test_fast_no_claims(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'pac_options': '0' + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'pac_options': '0' + } + ]) + + def test_fast_tgs_no_claims(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'pac_options': '0' + } + ]) + + def test_fast_no_claims_or_canon(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'pac_options': '0', + 'kdc_options': '0' + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'pac_options': '0', + 'kdc_options': '0' + } + ]) + + def test_fast_tgs_no_claims_or_canon(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'pac_options': '0', + 'kdc_options': '0' + } + ]) + + def test_fast_no_canon(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'kdc_options': '0' + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'kdc_options': '0' + } + ]) + + def test_fast_tgs_no_canon(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'kdc_options': '0' + } + ]) + + def test_simple_tgs_no_etypes(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': KDC_ERR_ETYPE_NOSUPP, + 'use_fast': False, + 'gen_tgt_fn': self.get_mach_tgt, + 'etypes': (), + 'expect_edata': False + } + ]) + + def test_fast_tgs_no_etypes(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': KDC_ERR_ETYPE_NOSUPP, + 'use_fast': True, + 'gen_tgt_fn': self.get_mach_tgt, + 'fast_armor': None, + 'etypes': (), + 'strict_edata_checking': False + } + ]) + + def test_simple_no_etypes(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_ETYPE_NOSUPP, + 'use_fast': False, + 'etypes': () + } + ]) + + def test_simple_fast_no_etypes(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_ETYPE_NOSUPP, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'etypes': (), + 'strict_edata_checking': False + } + ]) + + def test_empty_fast(self): + # Add an empty PA-FX-FAST in the initial AS-REQ. This should get + # rejected with a Generic error. + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, + KDC_ERR_PREAUTH_FAILED), + 'use_fast': True, + 'gen_fast_fn': self.generate_empty_fast, + 'fast_armor': None, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'expect_edata': False + } + ]) + + # Expected to fail against Windows - Windows does not produce an error. + def test_fast_unknown_critical_option(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_options': '001', # unsupported critical option + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_unarmored_as_req(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, + KDC_ERR_PREAUTH_FAILED), + 'use_fast': True, + 'fast_armor': None, # no armor, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'expect_edata': False + } + ]) + + def test_fast_invalid_armor_type(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_FAILED, + 'use_fast': True, + 'fast_armor': 0, # invalid armor type + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast_invalid_armor_type2(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_FAILED, + 'use_fast': True, + 'fast_armor': 2, # invalid armor type + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast_encrypted_challenge(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast_encrypted_challenge_as_req_self(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'as_req_self': True + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'as_req_self': True + } + ], client_account=self.AccountType.COMPUTER) + + def test_fast_encrypted_challenge_wrong_key(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_FAILED, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata_wrong_key, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast_encrypted_challenge_wrong_key_kdc(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_FAILED, + 'use_fast': True, + 'gen_padata_fn': + self.generate_enc_challenge_padata_wrong_key_kdc, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast_encrypted_challenge_no_fast(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_PREAUTH_FAILED, + KDC_ERR_PREAUTH_REQUIRED), + 'use_fast': False, + 'gen_padata_fn': self.generate_enc_challenge_padata_wrong_key + } + ]) + + # Expected to fail against Windows - Windows does not produce an error. + def test_fast_encrypted_challenge_clock_skew(self): + # The KDC is supposed to confirm that the timestamp is within its + # current clock skew, and return KRB_APP_ERR_SKEW if it is not (RFC6113 + # 5.4.6). However, this test fails against Windows, which accepts a + # skewed timestamp in the encrypted challenge. + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_SKEW, + 'use_fast': True, + 'gen_padata_fn': functools.partial( + self.generate_enc_challenge_padata, + skew=10000), + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast_invalid_tgt(self): + # The armor ticket 'sname' field is required to identify the target + # realm TGS (RFC6113 5.4.1.1). However, this test fails against + # Windows, which will still accept a service ticket identifying a + # different server principal. + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_POLICY, + KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_user_service_ticket + # ticket not identifying TGS of current + # realm + } + ]) + + # Similarly, this test fails against Windows, which accepts a service + # ticket identifying a different server principal. + def test_fast_invalid_tgt_mach(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_POLICY, + KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_service_ticket + # ticket not identifying TGS of current + # realm + } + ]) + + def test_fast_invalid_checksum_tgt(self): + # The armor ticket 'sname' field is required to identify the target + # realm TGS (RFC6113 5.4.1.1). However, this test fails against + # Windows, which will still accept a service ticket identifying a + # different server principal even if the ticket checksum is invalid. + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_POLICY, + KDC_ERR_S_PRINCIPAL_UNKNOWN), + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_service_ticket_invalid_checksum + } + ]) + + def test_fast_enc_timestamp(self): + # Provide ENC-TIMESTAMP as FAST padata when we should be providing + # ENCRYPTED-CHALLENGE - ensure that we get PREAUTH_REQUIRED. + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': (KDC_ERR_PREAUTH_REQUIRED, + KDC_ERR_POLICY), + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_timestamp_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast_tgs(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None + } + ]) + + def test_fast_tgs_armor(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST + } + ]) + + def test_fast_session_key(self): + # Ensure that specified APOptions are ignored. + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_ap_options': str(krb5_asn1.APOptions('use-session-key')) + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_ap_options': str(krb5_asn1.APOptions('use-session-key')) + } + ]) + + def test_fast_tgs_armor_session_key(self): + # Ensure that specified APOptions are ignored. + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'fast_ap_options': str(krb5_asn1.APOptions('use-session-key')) + } + ]) + + def test_fast_enc_pa_rep(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'expected_flags': 'enc-pa-rep' + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_pa_rep_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'expected_flags': 'enc-pa-rep' + } + ]) + + # Currently we only send PADATA-REQ-ENC-PA-REP for AS-REQ requests. + def test_fast_tgs_enc_pa_rep(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'gen_padata_fn': self.generate_enc_pa_rep_padata, + 'expected_flags': 'enc-pa-rep' + } + ]) + + # Currently we only send PADATA-REQ-ENC-PA-REP for AS-REQ requests. + def test_fast_tgs_armor_enc_pa_rep(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_padata_fn': self.generate_enc_pa_rep_padata, + 'expected_flags': 'enc-pa-rep' + } + ]) + + def test_fast_outer_wrong_realm(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'realm': 'TEST' # should be ignored + } + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'realm': 'TEST' # should be ignored + } + } + ]) + + def test_fast_tgs_outer_wrong_realm(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'outer_req': { + 'realm': 'TEST' # should be ignored + } + } + ]) + + def test_fast_outer_wrong_nonce(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'nonce': '123' # should be ignored + } + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'nonce': '123' # should be ignored + } + } + ]) + + def test_fast_tgs_outer_wrong_nonce(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'outer_req': { + 'nonce': '123' # should be ignored + } + } + ]) + + def test_fast_outer_wrong_flags(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'kdc-options': '11111111111111111' # should be ignored + } + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'kdc-options': '11111111111111111' # should be ignored + } + } + ]) + + def test_fast_tgs_outer_wrong_flags(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'outer_req': { + 'kdc-options': '11111111111111111' # should be ignored + } + } + ]) + + def test_fast_outer_no_sname(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'sname': None # should be ignored + } + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'sname': None # should be ignored + } + } + ]) + + def test_fast_tgs_outer_no_sname(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'outer_req': { + 'sname': None # should be ignored + } + } + ]) + + def test_fast_outer_wrong_till(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'till': '15000101000000Z' # should be ignored + } + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'outer_req': { + 'till': '15000101000000Z' # should be ignored + } + } + ]) + + def test_fast_tgs_outer_wrong_till(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'outer_req': { + 'till': '15000101000000Z' # should be ignored + } + } + ]) + + def test_fast_authdata_fast_used(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_authdata_fn': self.generate_fast_used_auth_data, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None + } + ]) + + def test_fast_authdata_fast_not_used(self): + # The AD-fx-fast-used authdata type can be included in the + # authenticator or the TGT authentication data to indicate that FAST + # must be used. The KDC must return KRB_APP_ERR_MODIFIED if it receives + # this authdata type in a request not using FAST (RFC6113 5.4.2). + self._run_test_sequence([ + # This request works without FAST. + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_tgt_fn': self.get_user_tgt + }, + # Add the 'FAST used' auth data and it now fails. + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_GENERIC), + 'use_fast': False, + 'gen_authdata_fn': self.generate_fast_used_auth_data, + 'gen_tgt_fn': self.get_user_tgt, + 'expect_edata': False + } + ]) + + def test_fast_ad_fx_fast_armor(self): + expected_sname = self.get_krbtgt_sname() + + # If the authenticator or TGT authentication data contains the + # AD-fx-fast-armor authdata type, the KDC must reject the request + # (RFC6113 5.4.1.1). + self._run_test_sequence([ + # This request works. + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None + }, + # Add the 'FAST armor' auth data and it now fails. + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, + KDC_ERR_BAD_INTEGRITY), + 'use_fast': True, + 'gen_authdata_fn': self.generate_fast_armor_auth_data, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'expected_sname': expected_sname, + 'expect_edata': False + } + ]) + + def test_fast_ad_fx_fast_armor2(self): + # Show that we can still use the AD-fx-fast-armor authorization data in + # FAST armor tickets. + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'gen_authdata_fn': self.generate_fast_armor_auth_data, + # include the auth data in the FAST armor. + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + } + ]) + + def test_fast_ad_fx_fast_armor_ticket(self): + expected_sname = self.get_krbtgt_sname() + + # If the authenticator or TGT authentication data contains the + # AD-fx-fast-armor authdata type, the KDC must reject the request + # (RFC6113 5.4.2). + self._run_test_sequence([ + # This request works. + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None + }, + # Add AD-fx-fast-armor authdata element to user TGT. This request + # fails. + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, + KDC_ERR_BAD_INTEGRITY), + 'use_fast': True, + 'gen_tgt_fn': self.gen_tgt_fast_armor_auth_data, + 'fast_armor': None, + 'expected_sname': expected_sname, + 'expect_edata': False + } + ]) + + def test_fast_ad_fx_fast_armor_enc_auth_data(self): + # If the authenticator or TGT authentication data contains the + # AD-fx-fast-armor authdata type, the KDC must reject the request + # (RFC6113 5.4.2). However, the KDC should not reject a request that + # contains this authdata type in enc-authorization-data. + self._run_test_sequence([ + # This request works. + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None + }, + # Add AD-fx-fast-armor authdata element to + # enc-authorization-data. This request also works. + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_enc_authdata_fn': self.generate_fast_armor_auth_data, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None + } + ]) + + def test_fast_ad_fx_fast_armor_ticket2(self): + self._run_test_sequence([ + # Show that we can still use the modified ticket as armor. + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.gen_tgt_fast_armor_auth_data + } + ]) + + def test_fast_tgs_service_ticket(self): + # Try to use a non-TGT ticket to establish an armor key, which fails + # (RFC6113 5.4.2). + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_NOT_US, + KDC_ERR_POLICY), + 'use_fast': True, + 'gen_tgt_fn': self.get_user_service_ticket, # fails + 'fast_armor': None + } + ]) + + def test_fast_tgs_service_ticket_mach(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_NOT_US, # fails + KDC_ERR_POLICY), + 'use_fast': True, + 'gen_tgt_fn': self.get_mach_service_ticket, + 'fast_armor': None + } + ]) + + def test_simple_tgs_no_subkey(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': False, + 'gen_tgt_fn': self.get_user_tgt, + 'include_subkey': False + } + ]) + + def test_fast_tgs_no_subkey(self): + expected_sname = self.get_krbtgt_sname() + + # Show that omitting the subkey in the TGS-REQ authenticator fails + # (RFC6113 5.4.2). + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': (KDC_ERR_GENERIC, + KDC_ERR_PREAUTH_FAILED), + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'include_subkey': False, + 'expected_sname': expected_sname, + 'expect_edata': False + } + ]) + + def test_fast_hide_client_names(self): + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_options': str(krb5_asn1.FastOptions( + 'hide-client-names')), + 'expected_anon': True + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_options': str(krb5_asn1.FastOptions( + 'hide-client-names')), + 'expected_anon': True + } + ]) + + def test_fast_tgs_hide_client_names(self): + self._run_test_sequence([ + { + 'rep_type': KRB_TGS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_tgt_fn': self.get_user_tgt, + 'fast_armor': None, + 'fast_options': str(krb5_asn1.FastOptions( + 'hide-client-names')), + 'expected_anon': True + } + ]) + + def test_fast_encrypted_challenge_replay(self): + # The KDC is supposed to check that encrypted challenges are not + # replays (RFC6113 5.4.6), but timestamps may be reused; an encrypted + # challenge is only considered a replay if the ciphertext is identical + # to a previous challenge. Windows does not perform this check. + + self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata_replay, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'repeat': 2 + } + ]) + + def test_fx_cookie_fast(self): + """Test that the FAST cookie is present and that its value is as + expected when FAST is used.""" + kdc_exchange_dict = self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt + }, + ]) + + cookie = kdc_exchange_dict.get('fast_cookie') + self.assertEqual(b'Microsoft', cookie) + + def test_fx_cookie_no_fast(self): + """Test that the FAST cookie is present and that its value is as + expected when FAST is not used.""" + kdc_exchange_dict = self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': False + }, + ]) + + cookie = kdc_exchange_dict.get('fast_cookie') + self.assertEqual(b'Microsof\x00', cookie) + + def test_unsolicited_fx_cookie_preauth(self): + """Test sending an unsolicited FX-COOKIE in an AS-REQ without + pre-authentication data.""" + + # Include a FAST cookie. + fast_cookie = self.create_fast_cookie('Samba-Test') + + kdc_exchange_dict = self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_cookie': fast_cookie, + }, + ]) + + got_cookie = kdc_exchange_dict.get('fast_cookie') + self.assertEqual(b'Microsoft', got_cookie) + + def test_unsolicited_fx_cookie_fast(self): + """Test sending an unsolicited FX-COOKIE in an AS-REQ with + pre-authentication data.""" + + # Include a FAST cookie. + fast_cookie = self.create_fast_cookie('Samba-Test') + + kdc_exchange_dict = self._run_test_sequence([ + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': KDC_ERR_PREAUTH_REQUIRED, + 'use_fast': True, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + }, + { + 'rep_type': KRB_AS_REP, + 'expected_error_mode': 0, + 'use_fast': True, + 'gen_padata_fn': self.generate_enc_challenge_padata, + 'fast_armor': FX_FAST_ARMOR_AP_REQUEST, + 'gen_armor_tgt_fn': self.get_mach_tgt, + 'fast_cookie': fast_cookie, + } + ]) + + got_cookie = kdc_exchange_dict.get('fast_cookie') + self.assertIsNone(got_cookie) + + def generate_enc_timestamp_padata(self, + kdc_exchange_dict, + callback_dict, + req_body): + key = kdc_exchange_dict['preauth_key'] + + padata = self.get_enc_timestamp_pa_data_from_key(key) + return [padata], req_body + + def generate_enc_challenge_padata(self, + kdc_exchange_dict, + callback_dict, + req_body, + skew=0): + armor_key = kdc_exchange_dict['armor_key'] + key = kdc_exchange_dict['preauth_key'] + + client_challenge_key = ( + self.generate_client_challenge_key(armor_key, key)) + padata = self.get_challenge_pa_data(client_challenge_key, skew=skew) + return [padata], req_body + + def generate_enc_challenge_padata_wrong_key_kdc(self, + kdc_exchange_dict, + callback_dict, + req_body): + armor_key = kdc_exchange_dict['armor_key'] + key = kdc_exchange_dict['preauth_key'] + + kdc_challenge_key = ( + self.generate_kdc_challenge_key(armor_key, key)) + padata = self.get_challenge_pa_data(kdc_challenge_key) + return [padata], req_body + + def generate_enc_challenge_padata_wrong_key(self, + kdc_exchange_dict, + callback_dict, + req_body): + key = kdc_exchange_dict['preauth_key'] + + padata = self.get_challenge_pa_data(key) + return [padata], req_body + + def generate_enc_challenge_padata_replay(self, + kdc_exchange_dict, + callback_dict, + req_body): + padata = callback_dict.get('replay_padata') + + if padata is None: + armor_key = kdc_exchange_dict['armor_key'] + key = kdc_exchange_dict['preauth_key'] + + client_challenge_key = ( + self.generate_client_challenge_key(armor_key, key)) + padata = self.get_challenge_pa_data(client_challenge_key) + callback_dict['replay_padata'] = padata + + return [padata], req_body + + def generate_empty_fast(self, + _kdc_exchange_dict, + _callback_dict, + _req_body, + _fast_padata, + _fast_armor, + _checksum, + _fast_options=''): + fast_padata = self.PA_DATA_create(PADATA_FX_FAST, b'') + + return fast_padata + + def _run_test_sequence(self, test_sequence, + client_account=KDCBaseTest.AccountType.USER, + client_opts=None, + armor_opts=None): + if self.strict_checking: + self.check_kdc_fast_support() + + kdc_options_default = str(krb5_asn1.KDCOptions('forwardable,' + 'canonicalize')) + + client_creds = self.get_cached_creds(account_type=client_account, + opts=client_opts) + target_creds = self.get_service_creds() + krbtgt_creds = self.get_krbtgt_creds() + + client_username = client_creds.get_username() + client_realm = client_creds.get_realm() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + krbtgt_username = krbtgt_creds.get_username() + krbtgt_realm = krbtgt_creds.get_realm() + krbtgt_sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=[krbtgt_username, krbtgt_realm]) + krbtgt_decryption_key = self.TicketDecryptionKey_from_creds( + krbtgt_creds) + krbtgt_etypes = krbtgt_creds.tgs_supported_enctypes + + target_username = target_creds.get_username()[:-1] + target_realm = target_creds.get_realm() + target_service = 'host' + target_sname = self.PrincipalName_create( + name_type=NT_SRV_HST, names=[target_service, target_username]) + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + client_decryption_key = self.TicketDecryptionKey_from_creds( + client_creds) + client_etypes = client_creds.tgs_supported_enctypes + + fast_cookie = None + preauth_etype_info2 = None + + for kdc_dict in test_sequence: + rep_type = kdc_dict.pop('rep_type') + self.assertIn(rep_type, (KRB_AS_REP, KRB_TGS_REP)) + + expected_error_mode = kdc_dict.pop('expected_error_mode') + if expected_error_mode == 0: + expected_error_mode = () + elif not isinstance(expected_error_mode, collections.abc.Container): + expected_error_mode = (expected_error_mode,) + for error in expected_error_mode: + self.assertIn(error, range(240)) + + use_fast = kdc_dict.pop('use_fast') + self.assertIs(type(use_fast), bool) + + if use_fast: + self.assertIn('fast_armor', kdc_dict) + fast_armor_type = kdc_dict.pop('fast_armor') + + if fast_armor_type is not None: + self.assertIn('gen_armor_tgt_fn', kdc_dict) + elif KDC_ERR_GENERIC not in expected_error_mode: + self.assertNotIn('gen_armor_tgt_fn', kdc_dict) + + gen_armor_tgt_fn = kdc_dict.pop('gen_armor_tgt_fn', None) + if gen_armor_tgt_fn is not None: + armor_tgt = gen_armor_tgt_fn(armor_opts) + else: + armor_tgt = None + + fast_options = kdc_dict.pop('fast_options', '') + else: + fast_armor_type = None + armor_tgt = None + + self.assertNotIn('fast_options', kdc_dict) + fast_options = None + + if rep_type == KRB_TGS_REP: + gen_tgt_fn = kdc_dict.pop('gen_tgt_fn') + tgt = gen_tgt_fn(opts=client_opts) + else: + self.assertNotIn('gen_tgt_fn', kdc_dict) + tgt = None + + if len(expected_error_mode) != 0: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + etypes = kdc_dict.pop('etypes', (AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5)) + + cname = client_cname if rep_type == KRB_AS_REP else None + crealm = client_realm + + as_req_self = kdc_dict.pop('as_req_self', False) + if as_req_self: + self.assertEqual(KRB_AS_REP, rep_type) + + if 'sname' in kdc_dict: + sname = kdc_dict.pop('sname') + else: + if as_req_self: + sname = client_cname + elif rep_type == KRB_AS_REP: + sname = krbtgt_sname + else: # KRB_TGS_REP + sname = target_sname + + if rep_type == KRB_AS_REP: + srealm = krbtgt_realm + else: # KRB_TGS_REP + srealm = target_realm + + if rep_type == KRB_TGS_REP: + tgt_cname = tgt.cname + else: + tgt_cname = client_cname + + expect_edata = kdc_dict.pop('expect_edata', None) + if expect_edata is not None: + self.assertTrue(expected_error_mode) + + expected_cname = kdc_dict.pop('expected_cname', tgt_cname) + expected_anon = kdc_dict.pop('expected_anon', + False) + expected_crealm = kdc_dict.pop('expected_crealm', client_realm) + expected_sname = kdc_dict.pop('expected_sname', sname) + expected_srealm = kdc_dict.pop('expected_srealm', srealm) + + expected_salt = client_creds.get_salt() + + authenticator_subkey = self.RandomKey(kcrypto.Enctype.AES256) + if rep_type == KRB_AS_REP: + if use_fast: + armor_key = self.generate_armor_key(authenticator_subkey, + armor_tgt.session_key) + armor_subkey = authenticator_subkey + else: + armor_key = None + armor_subkey = authenticator_subkey + else: # KRB_TGS_REP + if fast_armor_type is not None: + armor_subkey = self.RandomKey(kcrypto.Enctype.AES256) + explicit_armor_key = self.generate_armor_key( + armor_subkey, + armor_tgt.session_key) + armor_key = kcrypto.cf2(explicit_armor_key.key, + authenticator_subkey.key, + b'explicitarmor', + b'tgsarmor') + armor_key = Krb5EncryptionKey(armor_key, None) + else: + armor_key = self.generate_armor_key(authenticator_subkey, + tgt.session_key) + armor_subkey = authenticator_subkey + + if not kdc_dict.pop('include_subkey', True): + authenticator_subkey = None + + if use_fast: + generate_fast_fn = kdc_dict.pop('gen_fast_fn', None) + if generate_fast_fn is None: + generate_fast_fn = functools.partial( + self.generate_simple_fast, + fast_options=fast_options) + else: + generate_fast_fn = None + + generate_fast_armor_fn = ( + self.generate_ap_req + if fast_armor_type is not None + else None) + + def _generate_padata_copy(_kdc_exchange_dict, + _callback_dict, + req_body, + padata): + return list(padata), req_body + + pac_request = kdc_dict.pop('pac_request', None) + expect_pac = kdc_dict.pop('expect_pac', True) + + pac_options = kdc_dict.pop('pac_options', '1') # claims support + + kdc_options = kdc_dict.pop('kdc_options', kdc_options_default) + + gen_padata_fn = kdc_dict.pop('gen_padata_fn', None) + + if rep_type == KRB_AS_REP and gen_padata_fn is not None: + self.assertIsNotNone(preauth_etype_info2) + + preauth_key = self.PasswordKey_from_etype_info2( + client_creds, + preauth_etype_info2[0], + client_creds.get_kvno()) + else: + preauth_key = None + + if use_fast: + try: + fast_cookie = kdc_dict.pop('fast_cookie') + except KeyError: + pass + + generate_fast_padata_fn = gen_padata_fn + generate_padata_fn = (functools.partial(_generate_padata_copy, + padata=[fast_cookie]) + if fast_cookie is not None else None) + else: + generate_fast_padata_fn = None + generate_padata_fn = gen_padata_fn + + gen_authdata_fn = kdc_dict.pop('gen_authdata_fn', None) + if gen_authdata_fn is not None: + auth_data = [gen_authdata_fn()] + else: + auth_data = None + + gen_enc_authdata_fn = kdc_dict.pop('gen_enc_authdata_fn', None) + if gen_enc_authdata_fn is not None: + enc_auth_data = [gen_enc_authdata_fn()] + + enc_auth_data_key = authenticator_subkey + enc_auth_data_usage = KU_TGS_REQ_AUTH_DAT_SUBKEY + if enc_auth_data_key is None: + enc_auth_data_key = tgt.session_key + enc_auth_data_usage = KU_TGS_REQ_AUTH_DAT_SESSION + else: + enc_auth_data = None + + enc_auth_data_key = None + enc_auth_data_usage = None + + if not use_fast: + self.assertNotIn('inner_req', kdc_dict) + self.assertNotIn('outer_req', kdc_dict) + inner_req = kdc_dict.pop('inner_req', None) + outer_req = kdc_dict.pop('outer_req', None) + + expected_flags = kdc_dict.pop('expected_flags', None) + if expected_flags is not None: + expected_flags = krb5_asn1.TicketFlags(expected_flags) + unexpected_flags = kdc_dict.pop('unexpected_flags', None) + if unexpected_flags is not None: + unexpected_flags = krb5_asn1.TicketFlags(unexpected_flags) + + fast_ap_options = kdc_dict.pop('fast_ap_options', None) + + strict_edata_checking = kdc_dict.pop('strict_edata_checking', True) + + if rep_type == KRB_AS_REP: + if as_req_self: + expected_supported_etypes = client_etypes + decryption_key = client_decryption_key + else: + expected_supported_etypes = krbtgt_etypes + decryption_key = krbtgt_decryption_key + + kdc_exchange_dict = self.as_exchange_dict( + creds=client_creds, + expected_crealm=expected_crealm, + expected_cname=expected_cname, + expected_anon=expected_anon, + expected_srealm=expected_srealm, + expected_sname=expected_sname, + expected_supported_etypes=expected_supported_etypes, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + ticket_decryption_key=decryption_key, + generate_fast_fn=generate_fast_fn, + generate_fast_armor_fn=generate_fast_armor_fn, + generate_fast_padata_fn=generate_fast_padata_fn, + fast_armor_type=fast_armor_type, + generate_padata_fn=generate_padata_fn, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + callback_dict={}, + expected_error_mode=expected_error_mode, + expected_salt=expected_salt, + authenticator_subkey=authenticator_subkey, + preauth_key=preauth_key, + auth_data=auth_data, + armor_key=armor_key, + armor_tgt=armor_tgt, + armor_subkey=armor_subkey, + kdc_options=kdc_options, + inner_req=inner_req, + outer_req=outer_req, + expect_pac=expect_pac, + pac_request=pac_request, + pac_options=pac_options, + fast_ap_options=fast_ap_options, + strict_edata_checking=strict_edata_checking, + expect_edata=expect_edata) + else: # KRB_TGS_REP + kdc_exchange_dict = self.tgs_exchange_dict( + creds=client_creds, + expected_crealm=expected_crealm, + expected_cname=expected_cname, + expected_anon=expected_anon, + expected_srealm=expected_srealm, + expected_sname=expected_sname, + expected_supported_etypes=target_etypes, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + ticket_decryption_key=target_decryption_key, + generate_fast_fn=generate_fast_fn, + generate_fast_armor_fn=generate_fast_armor_fn, + generate_fast_padata_fn=generate_fast_padata_fn, + fast_armor_type=fast_armor_type, + generate_padata_fn=generate_padata_fn, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error_mode, + callback_dict={}, + tgt=tgt, + armor_key=armor_key, + armor_tgt=armor_tgt, + armor_subkey=armor_subkey, + authenticator_subkey=authenticator_subkey, + auth_data=auth_data, + body_checksum_type=None, + kdc_options=kdc_options, + inner_req=inner_req, + outer_req=outer_req, + expect_pac=expect_pac, + pac_request=pac_request, + pac_options=pac_options, + fast_ap_options=fast_ap_options, + strict_edata_checking=strict_edata_checking, + expect_edata=expect_edata) + + repeat = kdc_dict.pop('repeat', 1) + for _ in range(repeat): + rep = self._generic_kdc_exchange( + kdc_exchange_dict, + cname=cname, + realm=crealm, + sname=sname, + etypes=etypes, + EncAuthorizationData=enc_auth_data, + EncAuthorizationData_key=enc_auth_data_key, + EncAuthorizationData_usage=enc_auth_data_usage) + if len(expected_error_mode) == 0: + self.check_reply(rep, rep_type) + + fast_cookie = None + preauth_etype_info2 = None + + # Check whether the ticket contains a PAC. + ticket = kdc_exchange_dict['rep_ticket_creds'] + pac = self.get_ticket_pac(ticket, expect_pac=expect_pac) + if expect_pac: + self.assertIsNotNone(pac) + else: + self.assertIsNone(pac) + else: + self.check_error_rep(rep, expected_error_mode) + + if 'fast_cookie' in kdc_exchange_dict: + fast_cookie = self.create_fast_cookie( + kdc_exchange_dict['fast_cookie']) + else: + fast_cookie = None + + if KDC_ERR_PREAUTH_REQUIRED in expected_error_mode: + preauth_etype_info2 = ( + kdc_exchange_dict['preauth_etype_info2']) + else: + preauth_etype_info2 = None + + # Ensure we used all the parameters given to us. + self.assertEqual({}, kdc_dict) + + return kdc_exchange_dict + + def generate_enc_pa_rep_padata(self, + kdc_exchange_dict, + callback_dict, + req_body): + padata = self.PA_DATA_create(PADATA_REQ_ENC_PA_REP, b'') + + return [padata], req_body + + def generate_enc_pa_rep_challenge_padata(self, + kdc_exchange_dict, + callback_dict, + req_body): + padata, req_body = self.generate_enc_challenge_padata(kdc_exchange_dict, + callback_dict, + req_body) + + padata.append(self.PA_DATA_create(PADATA_REQ_ENC_PA_REP, b'')) + + return padata, req_body + + def generate_enc_pa_rep_timestamp_padata(self, + kdc_exchange_dict, + callback_dict, + req_body): + padata, req_body = self.generate_enc_timestamp_padata(kdc_exchange_dict, + callback_dict, + req_body) + + padata.append(self.PA_DATA_create(PADATA_REQ_ENC_PA_REP, b'')) + + return padata, req_body + + def generate_fast_armor_auth_data(self): + auth_data = self.AuthorizationData_create(AD_FX_FAST_ARMOR, b'') + + return auth_data + + def generate_fast_used_auth_data(self): + auth_data = self.AuthorizationData_create(AD_FX_FAST_USED, b'') + + return auth_data + + def gen_tgt_fast_armor_auth_data(self, opts): + user_tgt = self.get_user_tgt(opts) + + auth_data = self.generate_fast_armor_auth_data() + + def modify_fn(enc_part): + enc_part['authorization-data'].append(auth_data) + + return enc_part + + checksum_keys = self.get_krbtgt_checksum_key() + + # Use our modified TGT to replace the one in the request. + return self.modified_ticket(user_tgt, + modify_fn=modify_fn, + checksum_keys=checksum_keys) + + def create_fast_cookie(self, cookie): + self.assertIsNotNone(cookie) + if self.strict_checking: + self.assertNotEqual(0, len(cookie)) + + return self.PA_DATA_create(PADATA_FX_COOKIE, cookie) + + def check_kdc_fast_support(self): + # Check that the KDC supports FAST + + samdb = self.get_samdb() + + krbtgt_rid = security.DOMAIN_RID_KRBTGT + krbtgt_sid = '%s-%d' % (samdb.get_domain_sid(), krbtgt_rid) + + res = samdb.search(base='<SID=%s>' % krbtgt_sid, + scope=ldb.SCOPE_BASE, + attrs=['msDS-SupportedEncryptionTypes']) + + krbtgt_etypes = int(res[0]['msDS-SupportedEncryptionTypes'][0]) + + self.assertTrue( + security.KERB_ENCTYPE_FAST_SUPPORTED & krbtgt_etypes) + self.assertTrue( + security.KERB_ENCTYPE_COMPOUND_IDENTITY_SUPPORTED & krbtgt_etypes) + self.assertTrue( + security.KERB_ENCTYPE_CLAIMS_SUPPORTED & krbtgt_etypes) + + def get_mach_tgt(self, opts): + if opts is None: + opts = {} + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + **opts, + 'fast_support': True, + 'claims_support': True, + 'compound_id_support': True, + 'supported_enctypes': ( + security.KERB_ENCTYPE_RC4_HMAC_MD5 | + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK + ), + }) + return self.get_tgt(mach_creds) + + def get_rodc_issued_mach_tgt(self, opts): + return self.issued_by_rodc(self.get_mach_tgt(opts)) + + def get_user_tgt(self, opts): + user_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts=opts) + return self.get_tgt(user_creds) + + def get_user_service_ticket(self, opts): + user_tgt = self.get_user_tgt(opts) + service_creds = self.get_service_creds() + return self.get_service_ticket(user_tgt, service_creds) + + def get_mach_service_ticket(self, opts): + mach_tgt = self.get_mach_tgt(opts) + service_creds = self.get_service_creds() + return self.get_service_ticket(mach_tgt, service_creds) + + def get_service_ticket_invalid_checksum(self, opts): + ticket = self.get_user_service_ticket(opts) + + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + zeroed_key = ZeroedChecksumKey(krbtgt_key.key, + krbtgt_key.kvno) + + server_key = ticket.decryption_key + checksum_keys = { + krb5pac.PAC_TYPE_SRV_CHECKSUM: server_key, + krb5pac.PAC_TYPE_KDC_CHECKSUM: krbtgt_key, + krb5pac.PAC_TYPE_TICKET_CHECKSUM: zeroed_key, + } + + return self.modified_ticket( + ticket, + checksum_keys=checksum_keys, + include_checksums={krb5pac.PAC_TYPE_TICKET_CHECKSUM: True}) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/gkdi_tests.py b/python/samba/tests/krb5/gkdi_tests.py new file mode 100755 index 0000000..58a65c4 --- /dev/null +++ b/python/samba/tests/krb5/gkdi_tests.py @@ -0,0 +1,745 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Catalyst.Net Ltd 2023 +# +# 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 <https://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import secrets + +from typing import ClassVar, Optional + +from samba.dcerpc import gkdi, misc +from samba.gkdi import ( + Algorithm, + Gkid, + KEY_CYCLE_DURATION, + KEY_LEN_BYTES, + MAX_CLOCK_SKEW, + NtTime, + NtTimeDelta, + SeedKeyPair, +) +from samba.hresult import HRES_E_INVALIDARG, HRES_NTE_BAD_KEY, HRES_NTE_NO_KEY +from samba.nt_time import timedelta_from_nt_time_delta + +from samba.tests.gkdi import GetKeyError, GkdiBaseTest, ROOT_KEY_START_TIME +from samba.tests.krb5.kdc_base_test import KDCBaseTest + + +class GkdiKdcBaseTest(GkdiBaseTest, KDCBaseTest): + def new_root_key(self, *args, **kwargs) -> misc.GUID: + samdb = self.get_samdb() + domain_dn = self.get_server_dn(samdb) + return self.create_root_key(samdb, domain_dn, *args, **kwargs) + + def gkdi_conn(self) -> gkdi.gkdi: + # The seed keys used by Group Managed Service Accounts correspond to the + # Enterprise DCs SID (S-1-5-9); as such they can be retrieved only by + # server accounts. + return self.gkdi_connect( + self.dc_host, + self.get_lp(), + self.get_cached_creds(account_type=self.AccountType.SERVER), + ) + + def check_rpc_get_key( + self, root_key_id: Optional[misc.GUID], gkid: Gkid + ) -> SeedKeyPair: + got_key_pair = self.rpc_get_key( + self.gkdi_conn(), self.gmsa_sd, root_key_id, gkid + ) + expected_key_pair = self.get_key( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + gkid, + root_key_id_hint=got_key_pair.root_key_id if root_key_id is None else None, + ) + self.assertEqual( + got_key_pair.root_key_id, + expected_key_pair.root_key_id, + "root key IDs must match", + ) + self.assertEqual(got_key_pair, expected_key_pair, "key pairs must match") + + return got_key_pair + + +class GkdiExplicitRootKeyTests(GkdiKdcBaseTest): + def test_current_l0_idx(self): + """Request a key with the current L0 index. This index is regularly + incremented every 427 days or so.""" + root_key_id = self.new_root_key() + + # It actually doesn’t matter what we specify for the L1 and L2 indices. + # We’ll get the same result regardless — they just cannot specify a key + # from the future. + current_gkid = self.current_gkid() + key = self.check_rpc_get_key(root_key_id, current_gkid) + + self.assertEqual(current_gkid, key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + + def test_previous_l0_idx(self): + """Request a key with a previous L0 index.""" + root_key_id = self.new_root_key(use_start_time=ROOT_KEY_START_TIME) + + # It actually doesn’t matter what we specify for the L1 and L2 indices. + # We’ll get the same result regardless. + previous_l0_idx = self.current_gkid().l0_idx - 1 + key = self.check_rpc_get_key(root_key_id, Gkid(previous_l0_idx, 0, 0)) + + # Expect to get an L1 seed key. + self.assertIsNotNone(key.l1_key) + self.assertIsNone(key.l2_key) + self.assertEqual(Gkid(previous_l0_idx, 31, 31), key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + + def test_algorithm_sha1(self): + """Test with the SHA1 algorithm.""" + key = self.check_rpc_get_key( + self.new_root_key(hash_algorithm=Algorithm.SHA1), + self.current_gkid(), + ) + self.assertIs(Algorithm.SHA1, key.hash_algorithm) + + def test_algorithm_sha256(self): + """Test with the SHA256 algorithm.""" + key = self.check_rpc_get_key( + self.new_root_key(hash_algorithm=Algorithm.SHA256), + self.current_gkid(), + ) + self.assertIs(Algorithm.SHA256, key.hash_algorithm) + + def test_algorithm_sha384(self): + """Test with the SHA384 algorithm.""" + key = self.check_rpc_get_key( + self.new_root_key(hash_algorithm=Algorithm.SHA384), + self.current_gkid(), + ) + self.assertIs(Algorithm.SHA384, key.hash_algorithm) + + def test_algorithm_sha512(self): + """Test with the SHA512 algorithm.""" + key = self.check_rpc_get_key( + self.new_root_key(hash_algorithm=Algorithm.SHA512), + self.current_gkid(), + ) + self.assertIs(Algorithm.SHA512, key.hash_algorithm) + + def test_algorithm_none(self): + """Test without a specified algorithm.""" + key = self.check_rpc_get_key( + self.new_root_key(hash_algorithm=None), + self.current_gkid(), + ) + self.assertIs(Algorithm.SHA256, key.hash_algorithm) + + def test_future_key(self): + """Try to request a key from the future.""" + root_key_id = self.new_root_key(use_start_time=ROOT_KEY_START_TIME) + + future_gkid = self.current_gkid( + offset=timedelta_from_nt_time_delta( + NtTimeDelta(KEY_CYCLE_DURATION + MAX_CLOCK_SKEW) + ) + ) + + with self.assertRaises(GetKeyError) as err: + self.get_key(self.get_samdb(), self.gmsa_sd, root_key_id, future_gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + err.exception.args[0], + "requesting a key from the future should fail with INVALID_PARAMETER", + ) + + with self.assertRaises(GetKeyError) as rpc_err: + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, root_key_id, future_gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + rpc_err.exception.args[0], + "requesting a key from the future should fail with INVALID_PARAMETER", + ) + + def test_root_key_use_start_time_zero(self): + """Attempt to use a root key with an effective time of zero.""" + root_key_id = self.new_root_key(use_start_time=NtTime(0)) + + gkid = self.current_gkid() + + with self.assertRaises(GetKeyError) as err: + self.get_key(self.get_samdb(), self.gmsa_sd, root_key_id, gkid) + + self.assertEqual( + HRES_NTE_BAD_KEY, + err.exception.args[0], + "using a root key with an effective time of zero should fail with BAD_KEY", + ) + + with self.assertRaises(GetKeyError) as rpc_err: + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, root_key_id, gkid) + + self.assertEqual( + HRES_NTE_BAD_KEY, + rpc_err.exception.args[0], + "using a root key with an effective time of zero should fail with BAD_KEY", + ) + + def test_root_key_use_start_time_too_low(self): + """Attempt to use a root key with an effective time set too low.""" + root_key_id = self.new_root_key(use_start_time=NtTime(ROOT_KEY_START_TIME - 1)) + + gkid = self.current_gkid() + + with self.assertRaises(GetKeyError) as err: + self.get_key(self.get_samdb(), self.gmsa_sd, root_key_id, gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + err.exception.args[0], + "using a root key with too low effective time should fail with" + " INVALID_PARAMETER", + ) + + with self.assertRaises(GetKeyError) as rpc_err: + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, root_key_id, gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + rpc_err.exception.args[0], + "using a root key with too low effective time should fail with" + " INVALID_PARAMETER", + ) + + def test_before_valid(self): + """Attempt to use a key before it is valid.""" + gkid = self.current_gkid() + valid_start_time = NtTime( + gkid.start_nt_time() + KEY_CYCLE_DURATION + MAX_CLOCK_SKEW + ) + + # Using a valid root key is allowed. + valid_root_key_id = self.new_root_key(use_start_time=valid_start_time) + self.check_rpc_get_key(valid_root_key_id, gkid) + + # But attempting to use a root key that is not yet valid will result in + # an INVALID_PARAMETER error. + invalid_root_key_id = self.new_root_key(use_start_time=valid_start_time + 1) + + with self.assertRaises(GetKeyError) as err: + self.get_key(self.get_samdb(), self.gmsa_sd, invalid_root_key_id, gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + err.exception.args[0], + "using a key before it is valid should fail with INVALID_PARAMETER", + ) + + with self.assertRaises(GetKeyError) as rpc_err: + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, invalid_root_key_id, gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + rpc_err.exception.args[0], + "using a key before it is valid should fail with INVALID_PARAMETER", + ) + + def test_non_existent_root_key(self): + """Attempt to use a non‐existent root key.""" + root_key_id = misc.GUID(secrets.token_bytes(16)) + + gkid = self.current_gkid() + + with self.assertRaises(GetKeyError) as err: + self.get_key(self.get_samdb(), self.gmsa_sd, root_key_id, gkid) + + self.assertEqual( + HRES_NTE_NO_KEY, + err.exception.args[0], + "using a non‐existent root key should fail with NO_KEY", + ) + + with self.assertRaises(GetKeyError) as rpc_err: + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, root_key_id, gkid) + + self.assertEqual( + HRES_NTE_NO_KEY, + rpc_err.exception.args[0], + "using a non‐existent root key should fail with NO_KEY", + ) + + def test_root_key_wrong_length(self): + """Attempt to use a root key that is the wrong length.""" + root_key_id = self.new_root_key(data=bytes(KEY_LEN_BYTES // 2)) + + gkid = self.current_gkid() + + with self.assertRaises(GetKeyError) as err: + self.get_key(self.get_samdb(), self.gmsa_sd, root_key_id, gkid) + + self.assertEqual( + HRES_NTE_BAD_KEY, + err.exception.args[0], + "using a root key that is the wrong length should fail with BAD_KEY", + ) + + with self.assertRaises(GetKeyError) as rpc_err: + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, root_key_id, gkid) + + self.assertEqual( + HRES_NTE_BAD_KEY, + rpc_err.exception.args[0], + "using a root key that is the wrong length should fail with BAD_KEY", + ) + + +class GkdiImplicitRootKeyTests(GkdiKdcBaseTest): + _root_key: ClassVar[misc.GUID] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls._root_key = None + + def setUp(self) -> None: + super().setUp() + + if self._root_key is None: + # GKDI requires a root key to operate. Creating a root key here + # saves creating one before every test. + # + # We cannot delete this key after the tests have run, as Windows + # might have decided to cache it to be used in subsequent runs. It + # will keep a root key cached even if the corresponding AD object + # has been deleted, leading to various problems later. + cls = type(self) + cls._root_key = self.new_root_key(use_start_time=ROOT_KEY_START_TIME) + + def test_l1_seed_key(self): + """Request a key and expect to receive an L1 seed key.""" + gkid = Gkid(300, 0, 31) + key = self.check_rpc_get_key(None, gkid) + + # Expect to get an L1 seed key. + self.assertIsNotNone(key.l1_key) + self.assertIsNone(key.l2_key) + self.assertEqual(gkid, key.gkid) + + def test_l2_seed_key(self): + """Request a key and expect to receive an L2 seed key.""" + gkid = Gkid(300, 0, 0) + key = self.check_rpc_get_key(None, gkid) + + # Expect to get an L2 seed key. + self.assertIsNone(key.l1_key) + self.assertIsNotNone(key.l2_key) + self.assertEqual(gkid, key.gkid) + + def test_both_seed_keys(self): + """Request a key and expect to receive L1 and L2 seed keys.""" + gkid = Gkid(300, 1, 0) + key = self.check_rpc_get_key(None, gkid) + + # Expect to get both L1 and L2 seed keys. + self.assertIsNotNone(key.l1_key) + self.assertIsNotNone(key.l2_key) + self.assertEqual(gkid, key.gkid) + + def test_both_seed_keys_no_hint(self): + """Request a key, but don’t specify ‘root_key_id_hint’.""" + gkid = Gkid(300, 1, 0) + key = self.get_key( + self.get_samdb(), + self.gmsa_sd, + None, + gkid, + ) + + # Expect to get both L1 and L2 seed keys. + self.assertIsNotNone(key.l1_key) + self.assertIsNotNone(key.l2_key) + self.assertEqual(gkid, key.gkid) + + def test_request_l0_seed_key(self): + """Attempt to request an L0 seed key.""" + gkid = Gkid.l0_seed_key(300) + + with self.assertRaises(GetKeyError) as err: + self.get_key(self.get_samdb(), self.gmsa_sd, None, gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + err.exception.args[0], + "requesting an L0 seed key should fail with INVALID_PARAMETER", + ) + + with self.assertRaises(GetKeyError) as rpc_err: + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, None, gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + rpc_err.exception.args[0], + "requesting an L0 seed key should fail with INVALID_PARAMETER", + ) + + def test_request_l1_seed_key(self): + """Attempt to request an L1 seed key.""" + gkid = Gkid.l1_seed_key(300, 0) + + with self.assertRaises(GetKeyError) as err: + self.get_key(self.get_samdb(), self.gmsa_sd, None, gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + err.exception.args[0], + "requesting an L1 seed key should fail with INVALID_PARAMETER", + ) + + with self.assertRaises(GetKeyError) as rpc_err: + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, None, gkid) + + self.assertEqual( + HRES_E_INVALIDARG, + rpc_err.exception.args[0], + "requesting an L1 seed key should fail with INVALID_PARAMETER", + ) + + def test_request_default_seed_key(self): + """Try to make a request with the default GKID.""" + gkid = Gkid.default() + + self.assertRaises( + NotImplementedError, + self.get_key, + self.get_samdb(), + self.gmsa_sd, + None, + gkid, + ) + + self.rpc_get_key(self.gkdi_conn(), self.gmsa_sd, None, gkid) + + +class GkdiSelfTests(GkdiKdcBaseTest): + def test_current_l0_idx_l1_seed_key(self): + """Request a key with the current L0 index, expecting to receive an L1 + seed key.""" + root_key_id = self.new_root_key( + use_start_time=ROOT_KEY_START_TIME, + hash_algorithm=Algorithm.SHA512, + guid=misc.GUID("89f70521-9d66-441f-c314-1b462f9b1052"), + data=bytes.fromhex( + "a6ef87dbbbf86b6bbe55750b941f13ca99efe5185e2e2bded5b838d8a0e77647" + "0537e68cae45a7a0f4b1d6c9bf5494c3f879e172e326557cdbb6a56e8799a722" + ), + ) + + current_gkid = Gkid(255, 24, 31) + key = self.get_key( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + Gkid(255, 2, 5), + current_gkid=current_gkid, + ) + + # Expect to get an L1 seed key. + self.assertEqual(current_gkid, key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + self.assertEqual(Algorithm.SHA512, key.hash_algorithm) + self.assertEqual( + bytes.fromhex( + "bd538a073490f3cf9451c933025de9b22c97eaddaffa94b379e2b919a4bed147" + "5bc67f6a9175b139c69204c57d4300a0141ffe34d12ced84614593b1aa13af1c" + ), + key.l1_key, + ) + self.assertIsNone(key.l2_key) + + def test_current_l0_idx_l2_seed_key(self): + """Request a key with the current L0 index, expecting to receive an L2 + seed key.""" + root_key_id = self.new_root_key( + use_start_time=ROOT_KEY_START_TIME, + hash_algorithm=Algorithm.SHA512, + guid=misc.GUID("1a3d6c30-aa81-cb7f-d3fe-80775d135dfe"), + data=bytes.fromhex( + "dfd95be3153a0805c65694e7d284aace5ab0aa493350025eb8dbc6df0b4e9256" + "fb4cbfbe6237ce3732694e2608760076b67082d39abd3c0fedba1b8873645064" + ), + ) + + current_gkid = Gkid(321, 0, 12) + key = self.get_key( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + Gkid(321, 0, 1), + current_gkid=current_gkid, + ) + + # Expect to get an L2 seed key. + self.assertEqual(current_gkid, key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + self.assertEqual(Algorithm.SHA512, key.hash_algorithm) + self.assertIsNone(key.l1_key) + self.assertEqual( + bytes.fromhex( + "bbbd9376cd16c247ed40f5912d1908218c08f0915bae02fe02cbfb3753bde406" + "f9c553acd95143cf63906a0440e3cf237d2335ae4e4b9cd2d946a71351ebcb7b" + ), + key.l2_key, + ) + + def test_current_l0_idx_both_seed_keys(self): + """Request a key with the current L0 index, expecting to receive L1 and + L2 seed keys.""" + root_key_id = self.new_root_key( + use_start_time=ROOT_KEY_START_TIME, + hash_algorithm=Algorithm.SHA512, + guid=misc.GUID("09de0b38-c743-7abf-44ea-7a3c3e404314"), + data=bytes.fromhex( + "d5912d0eb3bd60e1371b1e525dd83be7fc5baf77018b0dba6bd948b7a98ebe5a" + "f37674332506a46c52c108a62f2a3e89251ad1bde6d539004679c0658853bb68" + ), + ) + + current_gkid = Gkid(123, 21, 0) + key = self.get_key( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + Gkid(123, 2, 1), + current_gkid=current_gkid, + ) + + # Expect to get both L1 and L2 seed keys. + self.assertEqual(current_gkid, key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + self.assertEqual(Algorithm.SHA512, key.hash_algorithm) + self.assertEqual( + bytes.fromhex( + "b1f7c5896e7dc791d9c0aaf8ca7dbab8c172a4f8b873db488a3c4cbd0f559b11" + "52ffba39d4aff2d9e8aada90b27a3c94a5af996f4b8f584a4f37ccab4d505d3d" + ), + key.l1_key, + ) + self.assertEqual( + bytes.fromhex( + "133c9bbd20d9227aeb38dfcd3be6bcbfc5983ba37202088ff5c8a70511214506" + "a69c195a8807cd844bcb955e9569c8e4d197759f28577cc126d15f16a7da4ee0" + ), + key.l2_key, + ) + + def test_previous_l0_idx(self): + """Request a key with a previous L0 index.""" + root_key_id = self.new_root_key( + use_start_time=ROOT_KEY_START_TIME, + hash_algorithm=Algorithm.SHA512, + guid=misc.GUID("27136e8f-e093-6fe3-e57f-1d915b102e1c"), + data=bytes.fromhex( + "b41118c60a19cafa5ecf858d1a2a2216527b2daedf386e9d599e42a46add6c7d" + "c93868619761c880ff3674a77c6e5fbf3434d130a9727bb2cd2a2557bdcfc752" + ), + ) + + key = self.get_key( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + Gkid(100, 20, 30), + current_gkid=Gkid(101, 2, 3), + ) + + # Expect to get an L1 seed key. + self.assertEqual(Gkid(100, 31, 31), key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + self.assertEqual(Algorithm.SHA512, key.hash_algorithm) + self.assertEqual( + bytes.fromhex( + "935cbdc06198eb28fa44b8d8278f51072c4613999236585041ede8e72d02fe95" + "e3454f046382cbc0a700779b79474dd7e080509d76302d2937407e96e3d3d022" + ), + key.l1_key, + ) + self.assertIsNone(key.l2_key) + + def test_sha1(self): + """Request a key derived with SHA1.""" + root_key_id = self.new_root_key( + use_start_time=ROOT_KEY_START_TIME, + hash_algorithm=Algorithm.SHA1, + guid=misc.GUID("970abad6-fe55-073a-caf1-b801d3f26bd3"), + data=bytes.fromhex( + "3bed03bf0fb7d4013149154f24ca2d59b98db6d588cb1f54eca083855e25eb28" + "d3562a01adc78c4b70e0b72a59515863e7732b853fba02dd7646e63108441211" + ), + ) + + current_gkid = Gkid(1, 2, 3) + key = self.get_key( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + Gkid(1, 1, 1), + current_gkid=current_gkid, + ) + + # Expect to get both L1 and L2 seed keys. + self.assertEqual(current_gkid, key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + self.assertEqual(Algorithm.SHA1, key.hash_algorithm) + self.assertEqual( + bytes.fromhex( + "576cb68f2e52eb739f817b488c3590d86f1c2c365f3fc9201d9c7fee7494853d" + "58746ee13e48f18aa6fa69f7157de3d07de34e13836792b7c088ffb6914a89c2" + ), + key.l1_key, + ) + self.assertEqual( + bytes.fromhex( + "3ffb825adaf116b6533207d568a30ed3d3f21c68840941c9456684f9afa11b05" + "6e0c59391b4d88c495d984c3d680029cc5c594630f34179119c1c5acaae5e90e" + ), + key.l2_key, + ) + + def test_sha256(self): + """Request a key derived with SHA256.""" + root_key_id = self.new_root_key( + use_start_time=ROOT_KEY_START_TIME, + hash_algorithm=Algorithm.SHA256, + guid=misc.GUID("45e26207-ed33-dcd5-925a-518a0deef69e"), + data=bytes.fromhex( + "28b5b6503d3c1d24814de781bb7bfce3ef69eed1ce4809372bee2c506270c5f0" + "b5c6df597472623f256c86daa0991e8a11a1705f21b2cfdc0bb9db4ba23246a2" + ), + ) + + current_gkid = Gkid(222, 22, 22) + key = self.get_key( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + Gkid(222, 11, 0), + current_gkid=current_gkid, + ) + + # Expect to get both L1 and L2 seed keys. + self.assertEqual(current_gkid, key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + self.assertEqual(Algorithm.SHA256, key.hash_algorithm) + self.assertEqual( + bytes.fromhex( + "57aced6e75f83f3af4f879b38b60f090b42e4bfa022fae3e6fd94280b469b0ec" + "15d8b853a870b5fbdf28708cce19273b74a573acbe0deda8ef515db4691e2dcb" + ), + key.l1_key, + ) + self.assertEqual( + bytes.fromhex( + "752a0879ae2424c0504c7493599f13e588e1bbdc252f83325ad5b1fb91c24c89" + "01d440f3ff9ffba59fcd65bb975732d9f383dd50b898174bb9393e383d25d540" + ), + key.l2_key, + ) + + def test_sha384(self): + """Request a key derived with SHA384.""" + root_key_id = self.new_root_key( + use_start_time=ROOT_KEY_START_TIME, + hash_algorithm=Algorithm.SHA384, + guid=misc.GUID("66e6d9f7-4924-f3fc-fe34-605634d42ebd"), + data=bytes.fromhex( + "23e5ba86cbd88f7b432ee66dbb03bf4eebf401cbfc3df735d4d728b503c87f84" + "3207c6f6153f190dfe85a86cb8d8b74df13b25305981be8d7e29c96ee54c9630" + ), + ) + + current_gkid = Gkid(287, 28, 27) + key = self.get_key( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + Gkid(287, 8, 7), + current_gkid=current_gkid, + ) + + # Expect to get both L1 and L2 seed keys. + self.assertEqual(current_gkid, key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + self.assertEqual(Algorithm.SHA384, key.hash_algorithm) + self.assertEqual( + bytes.fromhex( + "fabadd7a9a63df57d6832df7a735aebb6e181888b2eaf301a2e4ff9a70246d38" + "ab1d2416325bf3eb726a0267bab4bd950c7291f05ea5f17197ece56992af3eb8" + ), + key.l1_key, + ) + self.assertEqual( + bytes.fromhex( + "ec1c65634b5694818e1d341da9996db8f2a1ef6a2c776a7126a7ebd18b37a073" + "afdac44c41b167b14e4b872d485bbb6d7b70964215d0e84a2ff142a9d943f205" + ), + key.l2_key, + ) + + def test_derive_key_exact(self): + """Derive a key at an exact GKID.""" + root_key_id = self.new_root_key( + use_start_time=ROOT_KEY_START_TIME, + hash_algorithm=Algorithm.SHA512, + guid=misc.GUID("d95fb06f-5a9c-1829-e20d-27f3f2ecfbeb"), + data=bytes.fromhex( + "489f3531c537774d432d6b97e3bc1f43d2e8c6dc17eb0e4fd9a0870d2f1ebf92" + "e2496668a8b5bd11aea2d32d0aab716f48fe569f5c9b50ff3f9bf5deaea572fb" + ), + ) + + gkid = Gkid(333, 22, 11) + key = self.get_key_exact( + self.get_samdb(), + self.gmsa_sd, + root_key_id, + gkid, + current_gkid=self.current_gkid(), + ) + + self.assertEqual(gkid, key.gkid) + self.assertEqual(root_key_id, key.root_key_id) + self.assertEqual(Algorithm.SHA512, key.hash_algorithm) + self.assertEqual( + bytes.fromhex( + "d6ab3b14f4f4c8908aa3464011b39f10a8bfadb9974af90f7d9a9fede2fdc6e5" + "f68a628ec00f9994a3abd8a52ae9e2db4f68e83648311e9d7765f2535515b5e2" + ), + key.key, + ) + + +if __name__ == "__main__": + import unittest + + unittest.main() diff --git a/python/samba/tests/krb5/group_tests.py b/python/samba/tests/krb5/group_tests.py new file mode 100755 index 0000000..1214fa2 --- /dev/null +++ b/python/samba/tests/krb5/group_tests.py @@ -0,0 +1,1967 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd 2022 +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +import random +import re + +import ldb + +from samba import werror +from samba.dcerpc import netlogon, security +from samba.tests import DynamicTestCase, env_get_var_value +from samba.tests.krb5 import kcrypto +from samba.tests.krb5.kdc_base_test import GroupType, KDCBaseTest, Principal +from samba.tests.krb5.raw_testcase import RawKerberosTest +from samba.tests.krb5.rfc4120_constants import ( + KRB_TGS_REP, + NT_PRINCIPAL, +) + +SidType = RawKerberosTest.SidType + +global_asn1_print = False +global_hexdump = False + + +@DynamicTestCase +class GroupTests(KDCBaseTest): + # Placeholder objects that represent the user account undergoing testing. + user = object() + trust_user = object() + + # Constants for group SID attributes. + default_attrs = security.SE_GROUP_DEFAULT_FLAGS + resource_attrs = default_attrs | security.SE_GROUP_RESOURCE + + asserted_identity = security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY + + trust_domain = 'S-1-5-21-123-456-789' + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + @classmethod + def setUpDynamicTestCases(cls): + FILTER = env_get_var_value('FILTER', allow_missing=True) + SKIP_INVALID = env_get_var_value('SKIP_INVALID', allow_missing=True) + + for case in cls.cases: + invalid = case.pop('configuration_invalid', False) + if SKIP_INVALID and invalid: + # Some group setups are invalid on Windows, so we allow them to + # be skipped. + continue + name = case.pop('test') + name = re.sub(r'\W+', '_', name) + if FILTER and not re.search(FILTER, name): + continue + + cls.generate_dynamic_test('test_group', name, + dict(case)) + + def test_set_universal_primary_group(self): + samdb = self.get_samdb() + + # Create a universal group. + universal_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.UNIVERSAL.value) + + # Get the SID of the universal group. + universal_sid = self.get_objectSid(samdb, universal_dn) + + # Create a user account belonging to the group. + creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'member_of': ( + universal_dn, + ), + 'kerberos_enabled': False, + }, + use_cache=False) + + # Set the user's primary group. + self.set_primary_group(samdb, creds.get_dn(), universal_sid) + + def test_set_domain_local_primary_group(self): + samdb = self.get_samdb() + + # Create a domain-local group. + domain_local_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.DOMAIN_LOCAL.value) + + # Get the SID of the domain-local group. + domain_local_sid = self.get_objectSid(samdb, domain_local_dn) + + # Create a user account belonging to the group. + creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'member_of': ( + domain_local_dn, + ), + 'kerberos_enabled': False, + }, + use_cache=False) + + # Setting the user's primary group fails. + self.set_primary_group( + samdb, creds.get_dn(), domain_local_sid, + expected_error=ldb.ERR_UNWILLING_TO_PERFORM, + expected_werror=werror.WERR_MEMBER_NOT_IN_GROUP) + + def test_change_universal_primary_group_to_global(self): + samdb = self.get_samdb() + + # Create a universal group. + universal_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.UNIVERSAL.value) + + # Get the SID of the universal group. + universal_sid = self.get_objectSid(samdb, universal_dn) + + # Create a user account belonging to the group. + creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'member_of': ( + universal_dn, + ), + 'kerberos_enabled': False, + }, + use_cache=False) + + # Set the user's primary group. + self.set_primary_group(samdb, creds.get_dn(), universal_sid) + + # Change the group to a global group. + self.set_group_type(samdb, + ldb.Dn(samdb, universal_dn), + GroupType.GLOBAL) + + def test_change_universal_primary_group_to_domain_local(self): + samdb = self.get_samdb() + + # Create a universal group. + universal_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.UNIVERSAL.value) + + # Get the SID of the universal group. + universal_sid = self.get_objectSid(samdb, universal_dn) + + # Create a user account belonging to the group. + creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'member_of': ( + universal_dn, + ), + 'kerberos_enabled': False, + }, + use_cache=False) + + # Set the user's primary group. + self.set_primary_group(samdb, creds.get_dn(), universal_sid) + + # Change the group to a domain-local group. This works, even though the + # group is still the user's primary group. + self.set_group_type(samdb, + ldb.Dn(samdb, universal_dn), + GroupType.DOMAIN_LOCAL) + + # Check the groups in a SamInfo structure returned by SamLogon. + def test_samlogon_SamInfo(self): + samdb = self.get_samdb() + + # Create a universal and a domain-local group. + universal_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.UNIVERSAL.value) + domain_local_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.DOMAIN_LOCAL.value) + + # Create a user account belonging to both groups. + creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'member_of': ( + universal_dn, + domain_local_dn, + ), + 'kerberos_enabled': False, + }) + + # Get the SID and RID of the user account. + user_sid = creds.get_sid() + user_rid = int(user_sid.rsplit('-', 1)[1]) + + # Get the SID and RID of the universal group. + universal_sid = self.get_objectSid(samdb, universal_dn) + universal_rid = int(universal_sid.rsplit('-', 1)[1]) + + # We don't expect the EXTRA_SIDS flag to be set. + unexpected_flags = netlogon.NETLOGON_EXTRA_SIDS + + # Do a SamLogon call and check we get back the right structure. + interactive = netlogon.NetlogonInteractiveInformation + level = netlogon.NetlogonValidationSamInfo + validation = self._test_samlogon(creds=creds, + logon_type=interactive, + validation_level=level) + self.assertIsInstance(validation, netlogon.netr_SamInfo2) + + base = validation.base + + # Check some properties of the base structure. + self.assertEqual(user_rid, base.rid) + self.assertEqual(security.DOMAIN_RID_USERS, base.primary_gid) + self.assertEqual(samdb.get_domain_sid(), str(base.domain_sid)) + self.assertFalse(unexpected_flags & base.user_flags, + f'0x{unexpected_flags:x} unexpectedly set in ' + f'user_flags (0x{base.user_flags:x})') + + # Check we have two groups in the base. + self.assertEqual(2, base.groups.count) + + rids = base.groups.rids + + # The first group should be Domain Users. + self.assertEqual(security.DOMAIN_RID_USERS, rids[0].rid) + self.assertEqual(self.default_attrs, rids[0].attributes) + + # The second should be our universal group. + self.assertEqual(universal_rid, rids[1].rid) + self.assertEqual(self.default_attrs, rids[1].attributes) + + # The domain-local group is nowhere to be found. + + # Check the groups in a SamInfo2 structure returned by SamLogon. + def test_samlogon_SamInfo2(self): + samdb = self.get_samdb() + + # Create a universal and a domain-local group. + universal_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.UNIVERSAL.value) + domain_local_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.DOMAIN_LOCAL.value) + + # Create a user account belonging to both groups. + creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'member_of': ( + universal_dn, + domain_local_dn, + ), + 'kerberos_enabled': False, + }) + + # Get the SID and RID of the user account. + user_sid = creds.get_sid() + user_rid = int(user_sid.rsplit('-', 1)[1]) + + # Get the SID and RID of the universal group. + universal_sid = self.get_objectSid(samdb, universal_dn) + universal_rid = int(universal_sid.rsplit('-', 1)[1]) + + # Get the SID of the domain-local group. + domain_local_sid = self.get_objectSid(samdb, domain_local_dn) + + # We expect the EXTRA_SIDS flag to be set. + expected_flags = netlogon.NETLOGON_EXTRA_SIDS + + # Do a SamLogon call and check we get back the right structure. + interactive = netlogon.NetlogonInteractiveInformation + level = netlogon.NetlogonValidationSamInfo2 + validation = self._test_samlogon(creds=creds, + logon_type=interactive, + validation_level=level) + self.assertIsInstance(validation, netlogon.netr_SamInfo3) + + base = validation.base + + # Check some properties of the base structure. + self.assertEqual(user_rid, base.rid) + self.assertEqual(security.DOMAIN_RID_USERS, base.primary_gid) + self.assertEqual(samdb.get_domain_sid(), str(base.domain_sid)) + self.assertTrue(expected_flags & base.user_flags, + f'0x{expected_flags:x} unexpectedly reset in ' + f'user_flags (0x{base.user_flags:x})') + + # Check we have two groups in the base. + self.assertEqual(2, base.groups.count) + + rids = base.groups.rids + + # The first group should be Domain Users. + self.assertEqual(security.DOMAIN_RID_USERS, rids[0].rid) + self.assertEqual(self.default_attrs, rids[0].attributes) + + # The second should be our universal group. + self.assertEqual(universal_rid, rids[1].rid) + self.assertEqual(self.default_attrs, rids[1].attributes) + + # Check that we have one group in the SIDs array. + self.assertEqual(1, validation.sidcount) + + sids = validation.sids + + # That group should be our domain-local group. + self.assertEqual(domain_local_sid, str(sids[0].sid)) + self.assertEqual(self.resource_attrs, sids[0].attributes) + + # Check the groups in a SamInfo4 structure returned by SamLogon. + def test_samlogon_SamInfo4(self): + samdb = self.get_samdb() + + # Create a universal and a domain-local group. + universal_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.UNIVERSAL.value) + domain_local_dn = self.create_group(samdb, + self.get_new_username(), + gtype=GroupType.DOMAIN_LOCAL.value) + + # Create a user account belonging to both groups. + creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'member_of': ( + universal_dn, + domain_local_dn, + ), + 'kerberos_enabled': False, + }) + + # Get the SID and RID of the user account. + user_sid = creds.get_sid() + user_rid = int(user_sid.rsplit('-', 1)[1]) + + # Get the SID and RID of the universal group. + universal_sid = self.get_objectSid(samdb, universal_dn) + universal_rid = int(universal_sid.rsplit('-', 1)[1]) + + # Get the SID of the domain-local group. + domain_local_sid = self.get_objectSid(samdb, domain_local_dn) + + # We expect the EXTRA_SIDS flag to be set. + expected_flags = netlogon.NETLOGON_EXTRA_SIDS + + # Do a SamLogon call and check we get back the right structure. + interactive = netlogon.NetlogonInteractiveInformation + level = netlogon.NetlogonValidationSamInfo4 + validation = self._test_samlogon(creds=creds, + logon_type=interactive, + validation_level=level) + self.assertIsInstance(validation, netlogon.netr_SamInfo6) + + base = validation.base + + # Check some properties of the base structure. + self.assertEqual(user_rid, base.rid) + self.assertEqual(security.DOMAIN_RID_USERS, base.primary_gid) + self.assertEqual(samdb.get_domain_sid(), str(base.domain_sid)) + self.assertTrue(expected_flags & base.user_flags, + f'0x{expected_flags:x} unexpectedly reset in ' + f'user_flags (0x{base.user_flags:x})') + + # Check we have two groups in the base. + self.assertEqual(2, base.groups.count) + + rids = base.groups.rids + + # The first group should be Domain Users. + self.assertEqual(security.DOMAIN_RID_USERS, rids[0].rid) + self.assertEqual(self.default_attrs, rids[0].attributes) + + # The second should be our universal group. + self.assertEqual(universal_rid, rids[1].rid) + self.assertEqual(self.default_attrs, rids[1].attributes) + + # Check that we have one group in the SIDs array. + self.assertEqual(1, validation.sidcount) + + sids = validation.sids + + # That group should be our domain-local group. + self.assertEqual(domain_local_sid, str(sids[0].sid)) + self.assertEqual(self.resource_attrs, sids[0].attributes) + + # A list of test cases. + cases = [ + # AS-REQ tests. + { + 'test': 'universal; as-req to krbtgt', + 'groups': { + # A Universal group containing the user. + 'foo': (GroupType.UNIVERSAL, {user}), + }, + # Make an AS-REQ to the krbtgt with the user's account. + 'as:to_krbtgt': True, + 'as:expected': { + # Ignoring the user ID, or base RID, expect the PAC to contain + # precisely the following SIDS in any order: + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'universal; as-req to service', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {user}), + }, + # The same again, but this time perform the AS-REQ to a service. + 'as:to_krbtgt': False, + 'as:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'global; as-req to krbtgt', + 'groups': { + # The behaviour should be the same with a Global group. + 'foo': (GroupType.GLOBAL, {user}), + }, + 'as:to_krbtgt': True, + 'as:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'global; as-req to service', + 'groups': { + 'foo': (GroupType.GLOBAL, {user}), + }, + 'as:to_krbtgt': False, + 'as:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'domain-local; as-req to krbtgt', + 'groups': { + # A Domain-local group containing the user. + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + 'as:expected': { + # A TGT will not contain domain-local groups the user belongs + # to. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'domain-local; compression; as-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': False, + 'as:expected': { + # However, a service ticket will include domain-local + # groups. The account supports SID compression, so they are + # added as resource SIDs. + ('foo', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'domain-local; no compression; as-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': False, + # This time, the target account disclaims support for SID + # compression. + 'as:compression': False, + 'as:expected': { + # The SIDs in the PAC are the same, except the group SID is + # placed in Extra SIDs, not Resource SIDs. + ('foo', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested domain-local; as-req to krbtgt', + 'groups': { + # A Universal group containing a Domain-local group containing + # the user. + 'universal': (GroupType.UNIVERSAL, {'dom-local'}), + 'dom-local': (GroupType.DOMAIN_LOCAL, {user}), + }, + # It is not possible in Windows for a Universal group to contain a + # Domain-local group without exploiting bugs. This flag provides a + # convenient means by which these tests can be skipped. + 'configuration_invalid': True, + 'as:to_krbtgt': True, + 'as:expected': { + # While Windows would exclude the universal group from the PAC, + # expecting its inclusion is more sensible on the whole. + ('universal', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested domain-local; compression; as-req to service', + 'groups': { + 'universal': (GroupType.UNIVERSAL, {'dom-local'}), + 'dom-local': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'configuration_invalid': True, + 'as:to_krbtgt': False, + 'as:expected': { + # A service ticket is expected to include both SIDs. + ('universal', SidType.BASE_SID, default_attrs), + ('dom-local', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested domain-local; no compression; as-req to service', + 'groups': { + 'universal': (GroupType.UNIVERSAL, {'dom-local'}), + 'dom-local': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'configuration_invalid': True, + 'as:to_krbtgt': False, + 'as:compression': False, + 'as:expected': { + # As before, but disclaiming SID compression support, so the + # domain-local SID goes in Extra SIDs. + ('universal', SidType.BASE_SID, default_attrs), + ('dom-local', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested universal; as-req to krbtgt', + 'groups': { + # A similar scenario, except flipped around: a Domain-local + # group containing a Universal group containing the user. + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'as:expected': { + # Expect the Universal group's inclusion in the PAC. + ('universal', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested universal; compression; as-req to service', + 'groups': { + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': False, + 'as:expected': { + # Expect a service ticket to contain both SIDs. + ('universal', SidType.BASE_SID, default_attrs), + ('dom-local', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested universal; no compression; as-req to service', + 'groups': { + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': False, + 'as:compression': False, + 'as:expected': { + # As before, but disclaiming SID compression support, so the + # domain-local SID goes in Extra SIDs. + ('universal', SidType.BASE_SID, default_attrs), + ('dom-local', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + # TGS-REQ tests. + { + 'test': 'tgs-req to krbtgt', + 'groups': { + # A Universal group containing the user. + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'as:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + # Make a TGS-REQ to the krbtgt with the user's account. + 'tgs:to_krbtgt': True, + 'tgs:expected': { + # Expect the same results as with an AS-REQ. + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'tgs-req to service', + 'groups': { + # A Universal group containing the user. + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'as:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + # Make a TGS-REQ to a service with the user's account. + 'tgs:to_krbtgt': False, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'domain-local; tgs-req to krbtgt', + 'groups': { + # A Domain-local group containing the user. + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'as:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + # Expect the same results as with an AS-REQ. + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'domain-local; compression; tgs-req to service', + 'groups': { + # A Domain-local group containing the user. + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + 'as:expected': { + # The Domain-local group is not present in the PAC after an + # AS-REQ. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:expected': { + # Now it's added as a resource SID after the TGS-REQ. + ('foo', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'domain-local; no compression; tgs-req to service', + 'groups': { + # A Domain-local group containing the user. + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + # This time, the target account disclaims support for SID + # compression. + 'as:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + 'tgs:compression': False, + 'tgs:expected': { + # The SIDs in the PAC are the same, except the group SID is + # placed in Extra SIDs, not Resource SIDs. + ('foo', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'exclude asserted identity; tgs-req to krbtgt', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # Remove the Asserted Identity SID from the PAC. + ('foo', SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # It should not be re-added in the TGS-REQ. + ('foo', SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'exclude asserted identity; tgs-req to service', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + # Nor should it be re-added if the TGS-REQ is directed to a + # service. + 'tgs:to_krbtgt': False, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'exclude claims valid; tgs-req to krbtgt', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # Remove the Claims Valid SID from the PAC. + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + }, + 'tgs:expected': { + # It should not be re-added in the TGS-REQ. + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + }, + }, + { + 'test': 'exclude claims valid; tgs-req to service', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + # Nor should it be re-added if the TGS-REQ is directed to a + # service. + 'tgs:to_krbtgt': False, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + }, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + }, + }, + { + 'test': 'user group removal; tgs-req to krbtgt', + 'groups': { + # The user has been removed from the group... + 'foo': (GroupType.UNIVERSAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # ...but the user's PAC still contains the group SID. + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The group SID should not be removed when a TGS-REQ is + # performed. + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'user group removal; tgs-req to service', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {}), + }, + 'as:to_krbtgt': True, + # Likewise, but to a service. + 'tgs:to_krbtgt': False, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested group removal; tgs-req to krbtgt', + 'groups': { + # A Domain-local group contains a Universal group, of which the + # user is no longer a member... + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # ...but the user's PAC still contains the group SID. + ('universal', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The group SID should not be removed when a TGS-REQ is + # performed. + ('universal', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested group removal; compression; tgs-req to service', + 'groups': { + # A Domain-local group contains a Universal group, of which the + # user is no longer a member... + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + 'tgs:sids': { + # ...but the user's PAC still contains the group SID. + ('universal', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # Both SIDs should be present in the PAC when a TGS-REQ is + # performed. + ('universal', SidType.BASE_SID, default_attrs), + ('dom-local', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested group removal; no compression; tgs-req to service', + 'groups': { + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + # The same again, but with the server not supporting compression. + 'tgs:compression': False, + 'tgs:sids': { + ('universal', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The domain-local SID will go into Extra SIDs. + ('universal', SidType.BASE_SID, default_attrs), + ('dom-local', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'user group addition; tgs-req to krbtgt', + 'groups': { + # The user is a member of the group... + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # ...but the user's PAC still lacks the group SID. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The group SID should be omitted when a TGS-REQ is + # performed. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'user group addition; tgs-req to service', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + # Likewise, but to a service. + 'tgs:to_krbtgt': False, + 'tgs:sids': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested group addition; tgs-req to krbtgt', + 'groups': { + # A Domain-local group contains a Universal group, of which the + # user is now a member... + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # ...but the user's PAC still lacks the group SID. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The group SID should still be missing when a TGS-REQ is + # performed. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested group addition; compression; tgs-req to service', + 'groups': { + # A Domain-local group contains a Universal group, of which the + # user is now a member... + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + 'tgs:sids': { + # ...but the user's PAC still lacks the group SID. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # Both SIDs should be omitted from the PAC when a TGS-REQ is + # performed. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'nested group addition; no compression; tgs-req to service', + 'groups': { + 'dom-local': (GroupType.DOMAIN_LOCAL, {'universal'}), + 'universal': (GroupType.UNIVERSAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + # The same again, but with the server not supporting compression. + 'tgs:compression': False, + 'tgs:sids': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'resource sids given; tgs-req to krbtgt', + 'groups': { + # A couple of independent domain-local groups. + 'dom-local-0': (GroupType.DOMAIN_LOCAL, {}), + 'dom-local-1': (GroupType.DOMAIN_LOCAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # The TGT contains two resource SIDs for the domain-local + # groups. + ('dom-local-0', SidType.RESOURCE_SID, resource_attrs), + ('dom-local-1', SidType.RESOURCE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The resource SIDs remain after performing a TGS-REQ to the + # krbtgt. + ('dom-local-0', SidType.RESOURCE_SID, resource_attrs), + ('dom-local-1', SidType.RESOURCE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'resource sids wrongly given; tgs-req to krbtgt', + 'groups': { + 'dom-local-0': (GroupType.DOMAIN_LOCAL, {}), + 'dom-local-1': (GroupType.DOMAIN_LOCAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + # Though we have provided resource SIDs, we have reset the flag + # indicating that they are present. + 'tgs:reset_user_flags': netlogon.NETLOGON_RESOURCE_GROUPS, + 'tgs:sids': { + ('dom-local-0', SidType.RESOURCE_SID, resource_attrs), + ('dom-local-1', SidType.RESOURCE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + # The resource SIDs remain in the PAC. + ('dom-local-0', SidType.RESOURCE_SID, resource_attrs), + ('dom-local-1', SidType.RESOURCE_SID, default_attrs), + }, + }, + { + 'test': 'resource sids claimed given; tgs-req to krbtgt', + 'groups': { + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + # Though we claim to have provided resource SIDs, we have not + # actually done so. + 'tgs:set_user_flags': netlogon.NETLOGON_RESOURCE_GROUPS, + 'tgs:sids': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'resource sids given; compression; tgs-req to service', + 'groups': { + 'dom-local-0': (GroupType.DOMAIN_LOCAL, {}), + 'dom-local-1': (GroupType.DOMAIN_LOCAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + 'tgs:sids': { + ('dom-local-0', SidType.RESOURCE_SID, resource_attrs), + ('dom-local-1', SidType.RESOURCE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The resource SIDs are removed upon issuing a service ticket. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'resource sids given; no compression; tgs-req to service', + 'groups': { + 'dom-local-0': (GroupType.DOMAIN_LOCAL, {}), + 'dom-local-1': (GroupType.DOMAIN_LOCAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + # Compression is disabled on the service account. + 'tgs:compression': False, + 'tgs:sids': { + ('dom-local-0', SidType.RESOURCE_SID, resource_attrs), + ('dom-local-1', SidType.RESOURCE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The resource SIDs are again removed. + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + # Testing operability with older Samba versions. + { + 'test': 'domain-local; Samba 4.17; tgs-req to krbtgt', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # In Samba 4.17, domain-local groups are contained within the + # TGT, and do not have the SE_GROUP_RESOURCE bit set. + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + }, + 'tgs:expected': { + # After the TGS-REQ, the domain-local group remains in the PAC + # with its original attributes. + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + }, + }, + { + 'test': 'domain-local; Samba 4.17; compression; tgs-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + # The same scenario, but requesting a service ticket. + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + }, + 'tgs:expected': { + # The domain-local group remains in the PAC... + ('foo', SidType.BASE_SID, default_attrs), + # and another copy is added in Resource SIDs. This one has the + # SE_GROUP_RESOURCE bit set. + ('foo', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + }, + }, + { + 'test': 'domain-local; Samba 4.17; no compression; tgs-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + # In this case compression is disabled on the service. + 'tgs:compression': False, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + }, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + # Without compression, the extra SID appears in Extra SIDs. + ('foo', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + }, + }, + # Simulate a ticket coming in over a trust. + { + 'test': 'from trust; to krbtgt', + 'groups': { + # The user belongs to a couple of domain-local groups in our + # domain. + 'foo': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'bar': (GroupType.DOMAIN_LOCAL, {'foo'}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + # The user SID is from a different domain. + 'tgs:user_sid': trust_user, + 'tgs:sids': { + (trust_user, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + # This dummy resource SID comes from the trusted domain. + (f'{trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + # After performing a TGS-REQ to the krbtgt, the PAC remains + # unchanged. + (trust_user, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + (f'{trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + }, + { + 'test': 'from trust; compression; to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'bar': (GroupType.DOMAIN_LOCAL, {'foo'}), + }, + 'as:to_krbtgt': True, + # The same thing, but to a service. + 'tgs:to_krbtgt': False, + 'tgs:compression': True, + 'tgs:user_sid': trust_user, + 'tgs:sids': { + (trust_user, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (f'{trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (trust_user, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The resource SIDs are added to the PAC. + ('foo', SidType.RESOURCE_SID, resource_attrs), + ('bar', SidType.RESOURCE_SID, resource_attrs), + }, + }, + # Simulate a ticket coming in over a trust + { + 'test': 'from trust; no compression; to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {trust_user}), + 'bar': (GroupType.DOMAIN_LOCAL, {'foo'}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + # And again, but this time compression is disabled. + 'tgs:compression': False, + 'tgs:user_sid': trust_user, + 'tgs:sids': { + (trust_user, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (f'{trust_domain}-333', SidType.RESOURCE_SID, resource_attrs), + }, + 'tgs:expected': { + (trust_user, SidType.BASE_SID, default_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.PRIMARY_GID, None), + # The resource SIDs are added again, but this time to Extra + # SIDs. + ('foo', SidType.EXTRA_SID, resource_attrs), + ('bar', SidType.EXTRA_SID, resource_attrs), + }, + }, + # Test a group being the primary one for the user. + { + 'test': 'primary universal; as-req to krbtgt', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {user}), + }, + # Set this group as our primary group. + 'primary_group': 'foo', + 'as:to_krbtgt': True, + 'as:expected': { + # It appears in the PAC as normal. + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'primary universal; as-req to service', + 'groups': { + 'foo': (GroupType.UNIVERSAL, {user}), + }, + # Set this group as our primary group. + 'primary_group': 'foo', + # The request is made to a service. + 'as:to_krbtgt': False, + 'as:expected': { + # The group appears in the PAC as normal. + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + # Test domain-local primary groups. + { + 'test': 'primary domain-local; as-req to krbtgt', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + # Though Windows normally disallows setting a domain-local group as + # a primary group, Samba does not. + 'primary_group': 'foo', + 'as:to_krbtgt': True, + 'as:expected': { + # The domain-local group appears as our primary GID, but does + # not appear in the base SIDs. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'primary domain-local; compression; as-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'primary_group': 'foo', + # The same test, but the request is made to a service. + 'as:to_krbtgt': False, + 'as:expected': { + # The domain-local still only appears as our primary GID. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'primary domain-local; no compression; as-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'primary_group': 'foo', + 'as:to_krbtgt': False, + # This time, the target account disclaims support for SID + # compression. + 'as:compression': False, + 'as:expected': { + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'primary domain-local; tgs-req to krbtgt', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + # Though Windows normally disallows setting a domain-local group as + # a primary group, Samba does not. + 'primary_group': 'foo', + 'as:to_krbtgt': True, + 'as:expected': { + # The domain-local group appears as our primary GID, but does + # not appear in the base SIDs. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': True, + 'tgs:expected': { + # The domain-local group does not appear in the base SIDs. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'primary domain-local; compression; tgs-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + # Though Windows normally disallows setting a domain-local group as + # a primary group, Samba does not. + 'primary_group': 'foo', + 'as:to_krbtgt': True, + 'as:expected': { + # The domain-local group appears as our primary GID, but does + # not appear in the base SIDs. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + # The service is made to a service. + 'tgs:to_krbtgt': False, + 'tgs:expected': { + # The domain-local still only appears as our primary GID. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'primary domain-local; no compression; tgs-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + # Though Windows normally disallows setting a domain-local group as + # a primary group, Samba does not. + 'primary_group': 'foo', + 'as:to_krbtgt': True, + 'as:expected': { + # The domain-local group appears as our primary GID, but does + # not appear in the base SIDs. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:to_krbtgt': False, + # The service does not support compression. + 'tgs:compression': False, + 'tgs:expected': { + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + # Test the scenario where we belong to a now-domain-local group, and + # possess an old TGT issued when the group was still our primary one. + { + 'test': 'old primary domain-local; tgs-req to krbtgt', + 'groups': { + # A domain-local group to which we belong. + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # In the PAC, the group has the attributes of an ordinary + # group... + ('foo', SidType.BASE_SID, default_attrs), + # ...and remains our primary one. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The groups don't change. + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'old primary domain-local; compression; tgs-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + # The TGS request is made to a service. + 'tgs:to_krbtgt': False, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + # The group is added a second time to the PAC, now as a + # resource group. + ('foo', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'old primary domain-local; no compression; tgs-req to service', + 'groups': { + 'foo': (GroupType.DOMAIN_LOCAL, {user}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + # The target service doesn't support SID compression. + 'tgs:compression': False, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + # This time, the group is added to Extra SIDs. + ('foo', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + # Test the scenario where we possess an old TGT issued when a + # now-domain-local group was still our primary one. We no longer belong + # to that group, which itself belongs to another domain-local group. + { + 'test': 'old primary domain-local; transitive; tgs-req to krbtgt', + 'groups': { + 'bar': (GroupType.DOMAIN_LOCAL, {'foo'}), + 'foo': (GroupType.DOMAIN_LOCAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': True, + 'tgs:sids': { + # In the PAC, the group has the attributes of an ordinary + # group... + ('foo', SidType.BASE_SID, default_attrs), + # ...and remains our primary one. + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + # The groups don't change. + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'old primary domain-local; transitive; compression; tgs-req to service', + 'groups': { + 'bar': (GroupType.DOMAIN_LOCAL, {'foo'}), + 'foo': (GroupType.DOMAIN_LOCAL, {}), + }, + 'as:to_krbtgt': True, + # The TGS request is made to a service. + 'tgs:to_krbtgt': False, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + # The second resource group is added to the PAC as a resource + # group. + ('bar', SidType.RESOURCE_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + { + 'test': 'old primary domain-local; transitive; no compression; tgs-req to service', + 'groups': { + 'bar': (GroupType.DOMAIN_LOCAL, {'foo'}), + 'foo': (GroupType.DOMAIN_LOCAL, {}), + }, + 'as:to_krbtgt': True, + 'tgs:to_krbtgt': False, + # The target service doesn't support SID compression. + 'tgs:compression': False, + 'tgs:sids': { + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + 'tgs:expected': { + ('foo', SidType.BASE_SID, default_attrs), + ('foo', SidType.PRIMARY_GID, None), + # This time, the group is added to Extra SIDs. + ('bar', SidType.EXTRA_SID, resource_attrs), + (asserted_identity, SidType.EXTRA_SID, default_attrs), + (security.DOMAIN_RID_USERS, SidType.BASE_SID, default_attrs), + (security.SID_CLAIMS_VALID, SidType.EXTRA_SID, default_attrs), + }, + }, + ] + + # This is the main function to handle a single testcase. + def _test_group_with_args(self, case): + # The group arrangement for the test. + group_setup = case.pop('groups') + + # A group that should be the primary group for the user. + primary_group = case.pop('primary_group', None) + + # Whether the AS-REQ or TGS-REQ should be directed to the krbtgt. + as_to_krbtgt = case.pop('as:to_krbtgt') + tgs_to_krbtgt = case.pop('tgs:to_krbtgt', None) + + # Whether the target server of the AS-REQ or TGS-REQ should support + # resource SID compression. + as_compression = case.pop('as:compression', None) + tgs_compression = case.pop('tgs:compression', None) + + # Optional SIDs to replace those in the PAC prior to a TGS-REQ. + tgs_sids = case.pop('tgs:sids', None) + + # Optional user SID to replace that in the PAC prior to a TGS-REQ. + tgs_user_sid = case.pop('tgs:user_sid', None) + + # User flags that may be set or reset in the PAC prior to a TGS-REQ. + tgs_set_user_flags = case.pop('tgs:set_user_flags', None) + tgs_reset_user_flags = case.pop('tgs:reset_user_flags', None) + + # The SIDs we expect to see in the PAC after a AS-REQ or a TGS-REQ. + as_expected = case.pop('as:expected', None) + tgs_expected = case.pop('tgs:expected', None) + + # There should be no parameters remaining in the testcase. + self.assertFalse(case, 'unexpected parameters in testcase') + + if as_expected is None: + self.assertIsNotNone(tgs_expected, + 'no set of expected SIDs is provided') + + if as_to_krbtgt is None: + as_to_krbtgt = False + + if not as_to_krbtgt: + self.assertIsNone(tgs_expected, + "if we're performing a TGS-REQ, then AS-REQ " + "should be directed to the krbtgt") + + if tgs_to_krbtgt is None: + tgs_to_krbtgt = False + else: + self.assertIsNotNone(tgs_expected, + 'specified TGS request to krbtgt, but no ' + 'expected SIDs provided') + + if tgs_compression is not None: + self.assertIsNotNone(tgs_expected, + 'specified compression for TGS request, but ' + 'no expected SIDs provided') + + if tgs_user_sid is not None: + self.assertIsNotNone(tgs_sids, + 'specified TGS-REQ user SID, but no ' + 'accompanying SIDs provided') + + if tgs_set_user_flags is None: + tgs_set_user_flags = 0 + else: + self.assertIsNotNone(tgs_sids, + 'specified TGS-REQ set user flags, but no ' + 'accompanying SIDs provided') + + if tgs_reset_user_flags is None: + tgs_reset_user_flags = 0 + else: + self.assertIsNotNone(tgs_sids, + 'specified TGS-REQ reset user flags, but no ' + 'accompanying SIDs provided') + + samdb = self.get_samdb() + + domain_sid = samdb.get_domain_sid() + + # Create the user account. It needs to be freshly created rather than + # cached because we will probably add it to one or more groups. + user_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + user_dn = user_creds.get_dn() + user_sid = user_creds.get_sid() + user_name = user_creds.get_username() + salt = user_creds.get_salt() + + trust_user_rid = random.randint(2000, 0xfffffffe) + trust_user_sid = f'{self.trust_domain}-{trust_user_rid}' + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=user_name.split('/')) + + preauth_key = self.PasswordKey_from_creds(user_creds, + kcrypto.Enctype.AES256) + + ts_enc_padata = self.get_enc_timestamp_pa_data_from_key(preauth_key) + padata = [ts_enc_padata] + + target_creds, sname = self.get_target(as_to_krbtgt, + compression=as_compression) + decryption_key = self.TicketDecryptionKey_from_creds(target_creds) + + target_supported_etypes = target_creds.tgs_supported_enctypes + realm = target_creds.get_realm() + + # Initialise the group mapping with the user and trust principals. + user_principal = Principal(user_dn, user_sid) + trust_principal = Principal(None, trust_user_sid) + preexisting_groups = { + self.user: user_principal, + self.trust_user: trust_principal, + } + if primary_group is not None: + primary_groups = { + user_principal: primary_group, + } + else: + primary_groups = None + groups = self.setup_groups(samdb, + preexisting_groups, + group_setup, + primary_groups) + del group_setup + + if tgs_user_sid is None: + tgs_user_sid = user_sid + elif tgs_user_sid in groups: + tgs_user_sid = groups[tgs_user_sid].sid + + tgs_domain_sid, tgs_user_rid = tgs_user_sid.rsplit('-', 1) + + expected_groups = self.map_sids(as_expected, groups, + domain_sid) + tgs_sids_mapped = self.map_sids(tgs_sids, groups, + tgs_domain_sid) + tgs_expected_mapped = self.map_sids(tgs_expected, groups, + tgs_domain_sid) + + till = self.get_KerberosTime(offset=36000) + kdc_options = '0' + + etypes = self.get_default_enctypes(user_creds) + + # Perform an AS-REQ with the user account. + as_rep, kdc_exchange_dict = self._test_as_exchange( + creds=user_creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + expected_error_mode=0, + expected_crealm=realm, + expected_cname=cname, + expected_srealm=realm, + expected_sname=sname, + expected_salt=salt, + etypes=etypes, + padata=padata, + kdc_options=kdc_options, + expected_account_name=user_name, + expected_groups=expected_groups, + expected_sid=user_sid, + expected_domain_sid=domain_sid, + expected_supported_etypes=target_supported_etypes, + preauth_key=preauth_key, + ticket_decryption_key=decryption_key) + self.check_as_reply(as_rep) + + ticket = kdc_exchange_dict['rep_ticket_creds'] + + if tgs_expected is None: + # We're not performing a TGS-REQ, so we're done. + self.assertIsNone(tgs_sids, + 'provided SIDs to populate PAC for TGS-REQ, but ' + 'failed to specify expected SIDs') + return + + if tgs_sids is not None: + # Replace the SIDs in the PAC with the ones provided by the test. + ticket = self.ticket_with_sids(ticket, + tgs_sids_mapped, + tgs_domain_sid, + tgs_user_rid, + set_user_flags=tgs_set_user_flags, + reset_user_flags=tgs_reset_user_flags) + + target_creds, sname = self.get_target(tgs_to_krbtgt, + compression=tgs_compression) + decryption_key = self.TicketDecryptionKey_from_creds(target_creds) + + subkey = self.RandomKey(ticket.session_key.etype) + + requester_sid = None + if tgs_to_krbtgt: + requester_sid = user_sid + + expect_resource_groups_flag = None + if tgs_reset_user_flags & netlogon.NETLOGON_RESOURCE_GROUPS: + expect_resource_groups_flag = False + elif tgs_set_user_flags & netlogon.NETLOGON_RESOURCE_GROUPS: + expect_resource_groups_flag = True + + # Perform a TGS-REQ with the user account. + + kdc_exchange_dict = self.tgs_exchange_dict( + creds=user_creds, + expected_crealm=ticket.crealm, + expected_cname=cname, + expected_srealm=realm, + expected_sname=sname, + expected_account_name=user_name, + expected_groups=tgs_expected_mapped, + expected_sid=tgs_user_sid, + expected_requester_sid=requester_sid, + expected_domain_sid=tgs_domain_sid, + expected_supported_etypes=target_supported_etypes, + expect_resource_groups_flag=expect_resource_groups_flag, + ticket_decryption_key=decryption_key, + check_rep_fn=self.generic_check_kdc_rep, + check_kdc_private_fn=self.generic_check_kdc_private, + tgt=ticket, + authenticator_subkey=subkey, + kdc_options=kdc_options) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=realm, + sname=sname, + till_time=till, + etypes=etypes) + self.check_reply(rep, KRB_TGS_REP) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/kcrypto.py b/python/samba/tests/krb5/kcrypto.py new file mode 100755 index 0000000..c0a0990 --- /dev/null +++ b/python/samba/tests/krb5/kcrypto.py @@ -0,0 +1,969 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2013 by the Massachusetts Institute of Technology. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +# XXX current status: +# * Done and tested +# - AES encryption, checksum, string2key, prf +# - cf2 (needed for FAST) +# * Still to do: +# - DES enctypes and cksumtypes +# - RC4 exported enctype (if we need it for anything) +# - Unkeyed checksums +# - Special RC4, raw DES/DES3 operations for GSSAPI +# * Difficult or low priority: +# - Camellia not supported by PyCrypto +# - Cipher state only needed for kcmd suite +# - Nonstandard enctypes and cksumtypes like des-hmac-sha1 + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from math import gcd +from functools import reduce +from struct import pack, unpack +from binascii import crc32, b2a_hex +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hmac +from cryptography.hazmat.primitives.ciphers import algorithms as ciphers +from cryptography.hazmat.primitives.ciphers import modes +from cryptography.hazmat.primitives.ciphers.base import Cipher +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from samba.tests import TestCase +from samba.credentials import Credentials +from samba import generate_random_bytes as get_random_bytes +from samba.common import get_string, get_bytes + + +class Enctype(object): + DES_CRC = 1 + DES_MD4 = 2 + DES_MD5 = 3 + DES3 = 16 + AES128 = 17 + AES256 = 18 + RC4 = 23 + + +class Cksumtype(object): + CRC32 = 1 + MD4 = 2 + MD4_DES = 3 + MD5 = 7 + MD5_DES = 8 + SHA1_DES3 = 12 + SHA1 = 14 + SHA1_AES128 = 15 + SHA1_AES256 = 16 + HMAC_MD5 = -138 + + +class InvalidChecksum(ValueError): + pass + + +def _zeropad(s, padsize): + # Return s padded with 0 bytes to a multiple of padsize. + padlen = (padsize - (len(s) % padsize)) % padsize + return s + bytes(padlen) + + +def _xorbytes(b1, b2): + # xor two strings together and return the resulting string. + assert len(b1) == len(b2) + return bytes([x ^ y for x, y in zip(b1, b2)]) + + +def _mac_equal(mac1, mac2): + # Constant-time comparison function. (We can't use HMAC.verify + # since we use truncated macs.) + assert len(mac1) == len(mac2) + res = 0 + for x, y in zip(mac1, mac2): + res |= x ^ y + return res == 0 + + +def SIMPLE_HASH(string, algo_cls): + hash_ctx = hashes.Hash(algo_cls(), default_backend()) + hash_ctx.update(string) + return hash_ctx.finalize() + + +def HMAC_HASH(key, string, algo_cls): + hmac_ctx = hmac.HMAC(key, algo_cls(), default_backend()) + hmac_ctx.update(string) + return hmac_ctx.finalize() + + +def _nfold(str, nbytes): + # Convert str to a string of length nbytes using the RFC 3961 nfold + # operation. + + # Rotate the bytes in str to the right by nbits bits. + def rotate_right(str, nbits): + nbytes, remain = (nbits // 8) % len(str), nbits % 8 + return bytes([ + (str[i - nbytes] >> remain) + | (str[i - nbytes - 1] << (8 - remain) & 0xff) + for i in range(len(str))]) + + # Add equal-length strings together with end-around carry. + def add_ones_complement(str1, str2): + n = len(str1) + v = [a + b for a, b in zip(str1, str2)] + # Propagate carry bits to the left until there aren't any left. + while any(x & ~0xff for x in v): + v = [(v[i - n + 1] >> 8) + (v[i] & 0xff) for i in range(n)] + return bytes([x for x in v]) + + # Concatenate copies of str to produce the least common multiple + # of len(str) and nbytes, rotating each copy of str to the right + # by 13 bits times its list position. Decompose the concatenation + # into slices of length nbytes, and add them together as + # big-endian ones' complement integers. + slen = len(str) + lcm = nbytes * slen // gcd(nbytes, slen) + bigstr = b''.join((rotate_right(str, 13 * i) for i in range(lcm // slen))) + slices = (bigstr[p:p + nbytes] for p in range(0, lcm, nbytes)) + return reduce(add_ones_complement, slices) + + +def _is_weak_des_key(keybytes): + return keybytes in (b'\x01\x01\x01\x01\x01\x01\x01\x01', + b'\xFE\xFE\xFE\xFE\xFE\xFE\xFE\xFE', + b'\x1F\x1F\x1F\x1F\x0E\x0E\x0E\x0E', + b'\xE0\xE0\xE0\xE0\xF1\xF1\xF1\xF1', + b'\x01\xFE\x01\xFE\x01\xFE\x01\xFE', + b'\xFE\x01\xFE\x01\xFE\x01\xFE\x01', + b'\x1F\xE0\x1F\xE0\x0E\xF1\x0E\xF1', + b'\xE0\x1F\xE0\x1F\xF1\x0E\xF1\x0E', + b'\x01\xE0\x01\xE0\x01\xF1\x01\xF1', + b'\xE0\x01\xE0\x01\xF1\x01\xF1\x01', + b'\x1F\xFE\x1F\xFE\x0E\xFE\x0E\xFE', + b'\xFE\x1F\xFE\x1F\xFE\x0E\xFE\x0E', + b'\x01\x1F\x01\x1F\x01\x0E\x01\x0E', + b'\x1F\x01\x1F\x01\x0E\x01\x0E\x01', + b'\xE0\xFE\xE0\xFE\xF1\xFE\xF1\xFE', + b'\xFE\xE0\xFE\xE0\xFE\xF1\xFE\xF1') + + +class _EnctypeProfile(object): + # Base class for enctype profiles. Usable enctype classes must define: + # * enctype: enctype number + # * keysize: protocol size of key in bytes + # * seedsize: random_to_key input size in bytes + # * random_to_key (if the keyspace is not dense) + # * string_to_key + # * encrypt + # * decrypt + # * prf + + @classmethod + def random_to_key(cls, seed): + if len(seed) != cls.seedsize: + raise ValueError('Wrong seed length') + return Key(cls.enctype, seed) + + +class _SimplifiedEnctype(_EnctypeProfile): + # Base class for enctypes using the RFC 3961 simplified profile. + # Defines the encrypt, decrypt, and prf methods. Subclasses must + # define: + # * blocksize: Underlying cipher block size in bytes + # * padsize: Underlying cipher padding multiple (1 or blocksize) + # * macsize: Size of integrity MAC in bytes + # * hashmod: PyCrypto hash module for underlying hash function + # * basic_encrypt, basic_decrypt: Underlying CBC/CTS cipher + + @classmethod + def derive(cls, key, constant): + # RFC 3961 only says to n-fold the constant only if it is + # shorter than the cipher block size. But all Unix + # implementations n-fold constants if their length is larger + # than the block size as well, and n-folding when the length + # is equal to the block size is a no-op. + plaintext = _nfold(constant, cls.blocksize) + rndseed = b'' + while len(rndseed) < cls.seedsize: + ciphertext = cls.basic_encrypt(key, plaintext) + rndseed += ciphertext + plaintext = ciphertext + return cls.random_to_key(rndseed[0:cls.seedsize]) + + @classmethod + def encrypt(cls, key, keyusage, plaintext, confounder): + ki = cls.derive(key, pack('>iB', keyusage, 0x55)) + ke = cls.derive(key, pack('>iB', keyusage, 0xAA)) + if confounder is None: + confounder = get_random_bytes(cls.blocksize) + basic_plaintext = confounder + _zeropad(plaintext, cls.padsize) + hmac = HMAC_HASH(ki.contents, basic_plaintext, cls.hashalgo) + return cls.basic_encrypt(ke, basic_plaintext) + hmac[:cls.macsize] + + @classmethod + def decrypt(cls, key, keyusage, ciphertext): + ki = cls.derive(key, pack('>iB', keyusage, 0x55)) + ke = cls.derive(key, pack('>iB', keyusage, 0xAA)) + if len(ciphertext) < cls.blocksize + cls.macsize: + raise ValueError('ciphertext too short') + basic_ctext, mac = ciphertext[:-cls.macsize], ciphertext[-cls.macsize:] + if len(basic_ctext) % cls.padsize != 0: + raise ValueError('ciphertext does not meet padding requirement') + basic_plaintext = cls.basic_decrypt(ke, basic_ctext) + hmac = HMAC_HASH(ki.contents, basic_plaintext, cls.hashalgo) + expmac = hmac[:cls.macsize] + if not _mac_equal(mac, expmac): + raise InvalidChecksum('ciphertext integrity failure') + # Discard the confounder. + return basic_plaintext[cls.blocksize:] + + @classmethod + def prf(cls, key, string): + # Hash the input. RFC 3961 says to truncate to the padding + # size, but implementations truncate to the block size. + hashval = SIMPLE_HASH(string, cls.hashalgo) + truncated = hashval[:-(len(hashval) % cls.blocksize)] + # Encrypt the hash with a derived key. + kp = cls.derive(key, b'prf') + return cls.basic_encrypt(kp, truncated) + + +class _DES3CBC(_SimplifiedEnctype): + enctype = Enctype.DES3 + keysize = 24 + seedsize = 21 + blocksize = 8 + padsize = 8 + macsize = 20 + hashalgo = hashes.SHA1 + + @classmethod + def random_to_key(cls, seed): + # XXX Maybe reframe as _DESEnctype.random_to_key and use that + # way from DES3 random-to-key when DES is implemented, since + # MIT does this instead of the RFC 3961 random-to-key. + def expand(seed): + def parity(b): + # Return b with the low-order bit set to yield odd parity. + b &= ~1 + return b if bin(b & ~1).count('1') % 2 else b | 1 + assert len(seed) == 7 + firstbytes = bytes(parity(b & ~1) for b in seed) + lastbyte = parity(sum((seed[i] & 1) << i + 1 for i in range(7))) + keybytes = firstbytes + bytes([lastbyte]) + if _is_weak_des_key(keybytes): + keybytes = firstbytes + bytes([lastbyte ^ 0xF0]) + return keybytes + + if len(seed) != 21: + raise ValueError('Wrong seed length') + k1, k2, k3 = expand(seed[:7]), expand(seed[7:14]), expand(seed[14:]) + return Key(cls.enctype, k1 + k2 + k3) + + @classmethod + def string_to_key(cls, string, salt, params): + if params is not None and params != b'': + raise ValueError('Invalid DES3 string-to-key parameters') + k = cls.random_to_key(_nfold(string + salt, 21)) + return cls.derive(k, b'kerberos') + + @classmethod + def basic_encrypt(cls, key, plaintext): + assert len(plaintext) % 8 == 0 + algo = ciphers.TripleDES(key.contents) + cbc = modes.CBC(bytes(8)) + encryptor = Cipher(algo, cbc, default_backend()).encryptor() + ciphertext = encryptor.update(plaintext) + return ciphertext + + @classmethod + def basic_decrypt(cls, key, ciphertext): + assert len(ciphertext) % 8 == 0 + algo = ciphers.TripleDES(key.contents) + cbc = modes.CBC(bytes(8)) + decryptor = Cipher(algo, cbc, default_backend()).decryptor() + plaintext = decryptor.update(ciphertext) + return plaintext + + +class _AESEnctype(_SimplifiedEnctype): + # Base class for aes128-cts and aes256-cts. + blocksize = 16 + padsize = 1 + macsize = 12 + hashalgo = hashes.SHA1 + + @classmethod + def string_to_key(cls, string, salt, params): + (iterations,) = unpack('>L', params or b'\x00\x00\x10\x00') + pwbytes = get_bytes(string) + kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), + length=cls.seedsize, + salt=salt, + iterations=iterations, + backend=default_backend()) + seed = kdf.derive(pwbytes) + tkey = cls.random_to_key(seed) + return cls.derive(tkey, b'kerberos') + + @classmethod + def basic_encrypt(cls, key, plaintext): + assert len(plaintext) >= 16 + + algo = ciphers.AES(key.contents) + cbc = modes.CBC(bytes(16)) + aes_ctx = Cipher(algo, cbc, default_backend()) + + def aes_encrypt(plaintext): + encryptor = aes_ctx.encryptor() + ciphertext = encryptor.update(plaintext) + return ciphertext + + ctext = aes_encrypt(_zeropad(plaintext, 16)) + if len(plaintext) > 16: + # Swap the last two ciphertext blocks and truncate the + # final block to match the plaintext length. + lastlen = len(plaintext) % 16 or 16 + ctext = ctext[:-32] + ctext[-16:] + ctext[-32:-16][:lastlen] + return ctext + + @classmethod + def basic_decrypt(cls, key, ciphertext): + assert len(ciphertext) >= 16 + + algo = ciphers.AES(key.contents) + cbc = modes.CBC(bytes(16)) + aes_ctx = Cipher(algo, cbc, default_backend()) + + def aes_decrypt(ciphertext): + decryptor = aes_ctx.decryptor() + plaintext = decryptor.update(ciphertext) + return plaintext + + if len(ciphertext) == 16: + return aes_decrypt(ciphertext) + # Split the ciphertext into blocks. The last block may be partial. + cblocks = [ciphertext[p:p + 16] for p in range(0, len(ciphertext), 16)] + lastlen = len(cblocks[-1]) + # CBC-decrypt all but the last two blocks. + prev_cblock = bytes(16) + plaintext = b'' + for b in cblocks[:-2]: + plaintext += _xorbytes(aes_decrypt(b), prev_cblock) + prev_cblock = b + # Decrypt the second-to-last cipher block. The left side of + # the decrypted block will be the final block of plaintext + # xor'd with the final partial cipher block; the right side + # will be the omitted bytes of ciphertext from the final + # block. + b = aes_decrypt(cblocks[-2]) + lastplaintext = _xorbytes(b[:lastlen], cblocks[-1]) + omitted = b[lastlen:] + # Decrypt the final cipher block plus the omitted bytes to get + # the second-to-last plaintext block. + plaintext += _xorbytes(aes_decrypt(cblocks[-1] + omitted), prev_cblock) + return plaintext + lastplaintext + + +class _AES128CTS(_AESEnctype): + enctype = Enctype.AES128 + keysize = 16 + seedsize = 16 + + +class _AES256CTS(_AESEnctype): + enctype = Enctype.AES256 + keysize = 32 + seedsize = 32 + + +class _RC4(_EnctypeProfile): + enctype = Enctype.RC4 + keysize = 16 + seedsize = 16 + + @staticmethod + def usage_str(keyusage): + # Return a four-byte string for an RFC 3961 keyusage, using + # the RFC 4757 rules. Per the errata, do not map 9 to 8. + table = {3: 8, 23: 13} + msusage = table[keyusage] if keyusage in table else keyusage + return pack('<i', msusage) + + @classmethod + def string_to_key(cls, string, salt, params): + utf8string = get_string(string) + tmp = Credentials() + tmp.set_anonymous() + tmp.set_password(utf8string) + nthash = tmp.get_nt_hash() + return Key(cls.enctype, nthash) + + @classmethod + def encrypt(cls, key, keyusage, plaintext, confounder): + if confounder is None: + confounder = get_random_bytes(8) + ki = HMAC_HASH(key.contents, cls.usage_str(keyusage), hashes.MD5) + cksum = HMAC_HASH(ki, confounder + plaintext, hashes.MD5) + ke = HMAC_HASH(ki, cksum, hashes.MD5) + + encryptor = Cipher( + ciphers.ARC4(ke), None, default_backend()).encryptor() + ctext = encryptor.update(confounder + plaintext) + + return cksum + ctext + + @classmethod + def decrypt(cls, key, keyusage, ciphertext): + if len(ciphertext) < 24: + raise ValueError('ciphertext too short') + cksum, basic_ctext = ciphertext[:16], ciphertext[16:] + ki = HMAC_HASH(key.contents, cls.usage_str(keyusage), hashes.MD5) + ke = HMAC_HASH(ki, cksum, hashes.MD5) + + decryptor = Cipher( + ciphers.ARC4(ke), None, default_backend()).decryptor() + basic_plaintext = decryptor.update(basic_ctext) + + exp_cksum = HMAC_HASH(ki, basic_plaintext, hashes.MD5) + ok = _mac_equal(cksum, exp_cksum) + if not ok and keyusage == 9: + # Try again with usage 8, due to RFC 4757 errata. + ki = HMAC_HASH(key.contents, pack('<i', 8), hashes.MD5) + exp_cksum = HMAC_HASH(ki, basic_plaintext, hashes.MD5) + ok = _mac_equal(cksum, exp_cksum) + if not ok: + raise InvalidChecksum('ciphertext integrity failure') + # Discard the confounder. + return basic_plaintext[8:] + + @classmethod + def prf(cls, key, string): + return HMAC_HASH(key.contents, string, hashes.SHA1) + + +class _ChecksumProfile(object): + # Base class for checksum profiles. Usable checksum classes must + # define: + # * checksum + # * verify (if verification is not just checksum-and-compare) + # * checksum_len + @classmethod + def verify(cls, key, keyusage, text, cksum): + expected = cls.checksum(key, keyusage, text) + if not _mac_equal(cksum, expected): + raise InvalidChecksum('checksum verification failure') + + +class _SimplifiedChecksum(_ChecksumProfile): + # Base class for checksums using the RFC 3961 simplified profile. + # Defines the checksum and verify methods. Subclasses must + # define: + # * macsize: Size of checksum in bytes + # * enc: Profile of associated enctype + + @classmethod + def checksum(cls, key, keyusage, text): + kc = cls.enc.derive(key, pack('>iB', keyusage, 0x99)) + hmac = HMAC_HASH(kc.contents, text, cls.enc.hashalgo) + return hmac[:cls.macsize] + + @classmethod + def verify(cls, key, keyusage, text, cksum): + if key.enctype != cls.enc.enctype: + raise ValueError('Wrong key type for checksum') + super(_SimplifiedChecksum, cls).verify(key, keyusage, text, cksum) + + @classmethod + def checksum_len(cls): + return cls.macsize + + +class _SHA1AES128(_SimplifiedChecksum): + macsize = 12 + enc = _AES128CTS + + +class _SHA1AES256(_SimplifiedChecksum): + macsize = 12 + enc = _AES256CTS + + +class _SHA1DES3(_SimplifiedChecksum): + macsize = 20 + enc = _DES3CBC + + +class _HMACMD5(_ChecksumProfile): + @classmethod + def checksum(cls, key, keyusage, text): + ksign = HMAC_HASH(key.contents, b'signaturekey\0', hashes.MD5) + md5hash = SIMPLE_HASH(_RC4.usage_str(keyusage) + text, hashes.MD5) + return HMAC_HASH(ksign, md5hash, hashes.MD5) + + @classmethod + def verify(cls, key, keyusage, text, cksum): + if key.enctype != Enctype.RC4: + raise ValueError('Wrong key type for checksum') + super(_HMACMD5, cls).verify(key, keyusage, text, cksum) + + @classmethod + def checksum_len(cls): + return hashes.MD5.digest_size + + +class _MD5(_ChecksumProfile): + @classmethod + def checksum(cls, key, keyusage, text): + # This is unkeyed! + return SIMPLE_HASH(text, hashes.MD5) + + @classmethod + def checksum_len(cls): + return hashes.MD5.digest_size + + +class _SHA1(_ChecksumProfile): + @classmethod + def checksum(cls, key, keyusage, text): + # This is unkeyed! + return SIMPLE_HASH(text, hashes.SHA1) + + @classmethod + def checksum_len(cls): + return hashes.SHA1.digest_size + + +class _CRC32(_ChecksumProfile): + @classmethod + def checksum(cls, key, keyusage, text): + # This is unkeyed! + cksum = (~crc32(text, 0xffffffff)) & 0xffffffff + return pack('<I', cksum) + + @classmethod + def checksum_len(cls): + return 4 + + +_enctype_table = { + Enctype.DES3: _DES3CBC, + Enctype.AES128: _AES128CTS, + Enctype.AES256: _AES256CTS, + Enctype.RC4: _RC4 +} + + +_checksum_table = { + Cksumtype.SHA1_DES3: _SHA1DES3, + Cksumtype.SHA1_AES128: _SHA1AES128, + Cksumtype.SHA1_AES256: _SHA1AES256, + Cksumtype.HMAC_MD5: _HMACMD5, + Cksumtype.MD5: _MD5, + Cksumtype.SHA1: _SHA1, + Cksumtype.CRC32: _CRC32, +} + + +def _get_enctype_profile(enctype): + if enctype not in _enctype_table: + raise ValueError('Invalid enctype %d' % enctype) + return _enctype_table[enctype] + + +def _get_checksum_profile(cksumtype): + if cksumtype not in _checksum_table: + raise ValueError('Invalid cksumtype %d' % cksumtype) + return _checksum_table[cksumtype] + + +class Key(object): + def __init__(self, enctype, contents): + e = _get_enctype_profile(enctype) + if len(contents) != e.keysize: + raise ValueError('Wrong key length') + self.enctype = enctype + self.contents = contents + + def __str__(self): + return "enctype=%d contents=%s" % (self.enctype, + b2a_hex(self.contents).decode('ascii')) + +def seedsize(enctype): + e = _get_enctype_profile(enctype) + return e.seedsize + + +def random_to_key(enctype, seed): + e = _get_enctype_profile(enctype) + if len(seed) != e.seedsize: + raise ValueError('Wrong crypto seed length') + return e.random_to_key(seed) + + +def string_to_key(enctype, string, salt, params=None): + e = _get_enctype_profile(enctype) + return e.string_to_key(string, salt, params) + + +def encrypt(key, keyusage, plaintext, confounder=None): + e = _get_enctype_profile(key.enctype) + return e.encrypt(key, keyusage, plaintext, confounder) + + +def decrypt(key, keyusage, ciphertext): + # Throw InvalidChecksum on checksum failure. Throw ValueError on + # invalid key enctype or malformed ciphertext. + e = _get_enctype_profile(key.enctype) + return e.decrypt(key, keyusage, ciphertext) + + +def prf(key, string): + e = _get_enctype_profile(key.enctype) + return e.prf(key, string) + + +def make_checksum(cksumtype, key, keyusage, text): + c = _get_checksum_profile(cksumtype) + return c.checksum(key, keyusage, text) + + +def verify_checksum(cksumtype, key, keyusage, text, cksum): + # Throw InvalidChecksum exception on checksum failure. Throw + # ValueError on invalid cksumtype, invalid key enctype, or + # malformed checksum. + c = _get_checksum_profile(cksumtype) + c.verify(key, keyusage, text, cksum) + + +def checksum_len(cksumtype): + c = _get_checksum_profile(cksumtype) + return c.checksum_len() + + +def prfplus(key, pepper, ln): + # Produce ln bytes of output using the RFC 6113 PRF+ function. + out = b'' + count = 1 + while len(out) < ln: + out += prf(key, bytes([count]) + pepper) + count += 1 + return out[:ln] + + +def cf2(key1, key2, pepper1, pepper2, enctype=None): + # Combine two keys and two pepper strings to produce a result key + # of type enctype, using the RFC 6113 KRB-FX-CF2 function. + if enctype is None: + enctype = key1.enctype + e = _get_enctype_profile(enctype) + return e.random_to_key(_xorbytes(prfplus(key1, pepper1, e.seedsize), + prfplus(key2, pepper2, e.seedsize))) + + +def h(hexstr): + return bytes.fromhex(hexstr) + + +class KcrytoTest(TestCase): + """kcrypto Test case.""" + + def test_aes128_crypr(self): + # AES128 encrypt and decrypt + kb = h('9062430C8CDA3388922E6D6A509F5B7A') + conf = h('94B491F481485B9A0678CD3C4EA386AD') + keyusage = 2 + plain = b'9 bytesss' + ctxt = h('68FB9679601F45C78857B2BF820FD6E53ECA8D42FD4B1D7024A09205ABB7' + 'CD2EC26C355D2F') + k = Key(Enctype.AES128, kb) + self.assertEqual(encrypt(k, keyusage, plain, conf), ctxt) + self.assertEqual(decrypt(k, keyusage, ctxt), plain) + + def test_aes256_crypt(self): + # AES256 encrypt and decrypt + kb = h('F1C795E9248A09338D82C3F8D5B567040B0110736845041347235B14042313' + '98') + conf = h('E45CA518B42E266AD98E165E706FFB60') + keyusage = 4 + plain = b'30 bytes bytes bytes bytes byt' + ctxt = h('D1137A4D634CFECE924DBC3BF6790648BD5CFF7DE0E7B99460211D0DAEF3' + 'D79A295C688858F3B34B9CBD6EEBAE81DAF6B734D4D498B6714F1C1D') + k = Key(Enctype.AES256, kb) + self.assertEqual(encrypt(k, keyusage, plain, conf), ctxt) + self.assertEqual(decrypt(k, keyusage, ctxt), plain) + + def test_aes128_checksum(self): + # AES128 checksum + kb = h('9062430C8CDA3388922E6D6A509F5B7A') + keyusage = 3 + plain = b'eight nine ten eleven twelve thirteen' + cksum = h('01A4B088D45628F6946614E3') + k = Key(Enctype.AES128, kb) + verify_checksum(Cksumtype.SHA1_AES128, k, keyusage, plain, cksum) + + def test_aes256_checksum(self): + # AES256 checksum + kb = h('B1AE4CD8462AFF1677053CC9279AAC30B796FB81CE21474DD3DDBC' + 'FEA4EC76D7') + keyusage = 4 + plain = b'fourteen' + cksum = h('E08739E3279E2903EC8E3836') + k = Key(Enctype.AES256, kb) + verify_checksum(Cksumtype.SHA1_AES256, k, keyusage, plain, cksum) + + def test_aes128_string_to_key(self): + # AES128 string-to-key + string = b'password' + salt = b'ATHENA.MIT.EDUraeburn' + params = h('00000002') + kb = h('C651BF29E2300AC27FA469D693BDDA13') + k = string_to_key(Enctype.AES128, string, salt, params) + self.assertEqual(k.contents, kb) + + def test_aes256_string_to_key(self): + # AES256 string-to-key + string = b'X' * 64 + salt = b'pass phrase equals block size' + params = h('000004B0') + kb = h('89ADEE3608DB8BC71F1BFBFE459486B05618B70CBAE22092534E56' + 'C553BA4B34') + k = string_to_key(Enctype.AES256, string, salt, params) + self.assertEqual(k.contents, kb) + + def test_aes128_prf(self): + # AES128 prf + kb = h('77B39A37A868920F2A51F9DD150C5717') + k = string_to_key(Enctype.AES128, b'key1', b'key1') + self.assertEqual(prf(k, b'\x01\x61'), kb) + + def test_aes256_prf(self): + # AES256 prf + kb = h('0D674DD0F9A6806525A4D92E828BD15A') + k = string_to_key(Enctype.AES256, b'key2', b'key2') + self.assertEqual(prf(k, b'\x02\x62'), kb) + + def test_aes128_cf2(self): + # AES128 cf2 + kb = h('97DF97E4B798B29EB31ED7280287A92A') + k1 = string_to_key(Enctype.AES128, b'key1', b'key1') + k2 = string_to_key(Enctype.AES128, b'key2', b'key2') + k = cf2(k1, k2, b'a', b'b') + self.assertEqual(k.contents, kb) + + def test_aes256_cf2(self): + # AES256 cf2 + kb = h('4D6CA4E629785C1F01BAF55E2E548566B9617AE3A96868C337CB93B5' + 'E72B1C7B') + k1 = string_to_key(Enctype.AES256, b'key1', b'key1') + k2 = string_to_key(Enctype.AES256, b'key2', b'key2') + k = cf2(k1, k2, b'a', b'b') + self.assertEqual(k.contents, kb) + + def test_des3_crypt(self): + # DES3 encrypt and decrypt + kb = h('0DD52094E0F41CECCB5BE510A764B35176E3981332F1E598') + conf = h('94690A17B2DA3C9B') + keyusage = 3 + plain = b'13 bytes byte' + ctxt = h('839A17081ECBAFBCDC91B88C6955DD3C4514023CF177B77BF0D0177A16F7' + '05E849CB7781D76A316B193F8D30') + k = Key(Enctype.DES3, kb) + self.assertEqual(encrypt(k, keyusage, plain, conf), ctxt) + self.assertEqual(decrypt(k, keyusage, ctxt), _zeropad(plain, 8)) + + def test_des3_string_to_key(self): + # DES3 string-to-key + string = b'password' + salt = b'ATHENA.MIT.EDUraeburn' + kb = h('850BB51358548CD05E86768C313E3BFEF7511937DCF72C3E') + k = string_to_key(Enctype.DES3, string, salt) + self.assertEqual(k.contents, kb) + + def test_des3_checksum(self): + # DES3 checksum + kb = h('7A25DF8992296DCEDA0E135BC4046E2375B3C14C98FBC162') + keyusage = 2 + plain = b'six seven' + cksum = h('0EEFC9C3E049AABC1BA5C401677D9AB699082BB4') + k = Key(Enctype.DES3, kb) + verify_checksum(Cksumtype.SHA1_DES3, k, keyusage, plain, cksum) + + def test_des3_cf2(self): + # DES3 cf2 + kb = h('E58F9EB643862C13AD38E529313462A7F73E62834FE54A01') + k1 = string_to_key(Enctype.DES3, b'key1', b'key1') + k2 = string_to_key(Enctype.DES3, b'key2', b'key2') + k = cf2(k1, k2, b'a', b'b') + self.assertEqual(k.contents, kb) + + def test_rc4_crypt(self): + # RC4 encrypt and decrypt + kb = h('68F263DB3FCE15D031C9EAB02D67107A') + conf = h('37245E73A45FBF72') + keyusage = 4 + plain = b'30 bytes bytes bytes bytes byt' + ctxt = h('95F9047C3AD75891C2E9B04B16566DC8B6EB9CE4231AFB2542EF87A7B5A0' + 'F260A99F0460508DE0CECC632D07C354124E46C5D2234EB8') + k = Key(Enctype.RC4, kb) + self.assertEqual(encrypt(k, keyusage, plain, conf), ctxt) + self.assertEqual(decrypt(k, keyusage, ctxt), plain) + + def test_rc4_string_to_key(self): + # RC4 string-to-key + string = b'foo' + kb = h('AC8E657F83DF82BEEA5D43BDAF7800CC') + k = string_to_key(Enctype.RC4, string, None) + self.assertEqual(k.contents, kb) + + def test_rc4_checksum(self): + # RC4 checksum + kb = h('F7D3A155AF5E238A0B7A871A96BA2AB2') + keyusage = 6 + plain = b'seventeen eighteen nineteen twenty' + cksum = h('EB38CC97E2230F59DA4117DC5859D7EC') + k = Key(Enctype.RC4, kb) + verify_checksum(Cksumtype.HMAC_MD5, k, keyusage, plain, cksum) + + def test_rc4_cf2(self): + # RC4 cf2 + kb = h('24D7F6B6BAE4E5C00D2082C5EBAB3672') + k1 = string_to_key(Enctype.RC4, b'key1', b'key1') + k2 = string_to_key(Enctype.RC4, b'key2', b'key2') + k = cf2(k1, k2, b'a', b'b') + self.assertEqual(k.contents, kb) + + def _test_md5_unkeyed_checksum(self, etype, usage): + # MD5 unkeyed checksum + pw = b'pwd' + salt = b'bytes' + key = string_to_key(etype, pw, salt) + plain = b'seventeen eighteen nineteen twenty' + cksum = h('9d9588cdef3a8cefc9d2c208d978f60c') + verify_checksum(Cksumtype.MD5, key, usage, plain, cksum) + + def test_md5_unkeyed_checksum_des3_usage_40(self): + return self._test_md5_unkeyed_checksum(Enctype.DES3, 40) + + def test_md5_unkeyed_checksum_des3_usage_50(self): + return self._test_md5_unkeyed_checksum(Enctype.DES3, 50) + + def test_md5_unkeyed_checksum_rc4_usage_40(self): + return self._test_md5_unkeyed_checksum(Enctype.RC4, 40) + + def test_md5_unkeyed_checksum_rc4_usage_50(self): + return self._test_md5_unkeyed_checksum(Enctype.RC4, 50) + + def test_md5_unkeyed_checksum_aes128_usage_40(self): + return self._test_md5_unkeyed_checksum(Enctype.AES128, 40) + + def test_md5_unkeyed_checksum_aes128_usage_50(self): + return self._test_md5_unkeyed_checksum(Enctype.AES128, 50) + + def test_md5_unkeyed_checksum_aes256_usage_40(self): + return self._test_md5_unkeyed_checksum(Enctype.AES256, 40) + + def test_md5_unkeyed_checksum_aes256_usage_50(self): + return self._test_md5_unkeyed_checksum(Enctype.AES256, 50) + + def _test_sha1_unkeyed_checksum(self, etype, usage): + # SHA1 unkeyed checksum + pw = b'password' + salt = b'salt' + key = string_to_key(etype, pw, salt) + plain = b'twenty nineteen eighteen seventeen' + cksum = h('381c870d8875d1913555de19af5c885fd27b7da9') + verify_checksum(Cksumtype.SHA1, key, usage, plain, cksum) + + def test_sha1_unkeyed_checksum_des3_usage_40(self): + return self._test_sha1_unkeyed_checksum(Enctype.DES3, 40) + + def test_sha1_unkeyed_checksum_des3_usage_50(self): + return self._test_sha1_unkeyed_checksum(Enctype.DES3, 50) + + def test_sha1_unkeyed_checksum_rc4_usage_40(self): + return self._test_sha1_unkeyed_checksum(Enctype.RC4, 40) + + def test_sha1_unkeyed_checksum_rc4_usage_50(self): + return self._test_sha1_unkeyed_checksum(Enctype.RC4, 50) + + def test_sha1_unkeyed_checksum_aes128_usage_40(self): + return self._test_sha1_unkeyed_checksum(Enctype.AES128, 40) + + def test_sha1_unkeyed_checksum_aes128_usage_50(self): + return self._test_sha1_unkeyed_checksum(Enctype.AES128, 50) + + def test_sha1_unkeyed_checksum_aes256_usage_40(self): + return self._test_sha1_unkeyed_checksum(Enctype.AES256, 40) + + def test_sha1_unkeyed_checksum_aes256_usage_50(self): + return self._test_sha1_unkeyed_checksum(Enctype.AES256, 50) + + def _test_crc32_unkeyed_checksum(self, etype, usage): + # CRC32 unkeyed checksum + pw = b'password' + salt = b'salt' + key = string_to_key(etype, pw, salt) + plain = b'africa america asia australia europe' + cksum = h('ce595a53') + verify_checksum(Cksumtype.CRC32, key, usage, plain, cksum) + + def test_crc32_unkeyed_checksum_des3_usage_40(self): + return self._test_crc32_unkeyed_checksum(Enctype.DES3, 40) + + def test_crc32_unkeyed_checksum_des3_usage_50(self): + return self._test_crc32_unkeyed_checksum(Enctype.DES3, 50) + + def test_crc32_unkeyed_checksum_rc4_usage_40(self): + return self._test_crc32_unkeyed_checksum(Enctype.RC4, 40) + + def test_crc32_unkeyed_checksum_rc4_usage_50(self): + return self._test_crc32_unkeyed_checksum(Enctype.RC4, 50) + + def test_crc32_unkeyed_checksum_aes128_usage_40(self): + return self._test_crc32_unkeyed_checksum(Enctype.AES128, 40) + + def test_crc32_unkeyed_checksum_aes128_usage_50(self): + return self._test_crc32_unkeyed_checksum(Enctype.AES128, 50) + + def test_crc32_unkeyed_checksum_aes256_usage_40(self): + return self._test_crc32_unkeyed_checksum(Enctype.AES256, 40) + + def test_crc32_unkeyed_checksum_aes256_usage_50(self): + return self._test_crc32_unkeyed_checksum(Enctype.AES256, 50) + + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/kdc_base_test.py b/python/samba/tests/krb5/kdc_base_test.py new file mode 100644 index 0000000..373c73e --- /dev/null +++ b/python/samba/tests/krb5/kdc_base_test.py @@ -0,0 +1,3755 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2020-2021 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import binascii +import collections +from datetime import datetime, timezone +from enum import Enum +from functools import partial +import numbers +import secrets +import tempfile + +from collections import namedtuple +import ldb +from ldb import SCOPE_BASE +from samba import ( + NTSTATUSError, + arcfour_encrypt, + common, + generate_random_password, + ntstatus, +) +from samba.auth import system_session +from samba.credentials import ( + Credentials, + DONT_USE_KERBEROS, + MUST_USE_KERBEROS, + SPECIFIED, +) +from samba.crypto import des_crypt_blob_16, md4_hash_blob +from samba.dcerpc import ( + claims, + drsblobs, + drsuapi, + krb5ccache, + krb5pac, + lsa, + misc, + netlogon, + ntlmssp, + samr, + security, +) +from samba.drs_utils import drs_Replicate, drsuapi_connect +from samba.dsdb import ( + DSDB_SYNTAX_BINARY_DN, + DS_DOMAIN_FUNCTION_2000, + DS_DOMAIN_FUNCTION_2008, + DS_GUID_COMPUTERS_CONTAINER, + DS_GUID_DOMAIN_CONTROLLERS_CONTAINER, + DS_GUID_MANAGED_SERVICE_ACCOUNTS_CONTAINER, + DS_GUID_USERS_CONTAINER, + GTYPE_SECURITY_DOMAIN_LOCAL_GROUP, + GTYPE_SECURITY_GLOBAL_GROUP, + GTYPE_SECURITY_UNIVERSAL_GROUP, + UF_NORMAL_ACCOUNT, + UF_NOT_DELEGATED, + UF_NO_AUTH_DATA_REQUIRED, + UF_PARTIAL_SECRETS_ACCOUNT, + UF_SERVER_TRUST_ACCOUNT, + UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, + UF_WORKSTATION_TRUST_ACCOUNT, +) +from samba.dcerpc.misc import ( + SEC_CHAN_BDC, + SEC_CHAN_NULL, + SEC_CHAN_WKSTA, +) +from samba.join import DCJoinContext +from samba.ndr import ndr_pack, ndr_unpack +from samba import net +from samba.netcmd.domain.models import AuthenticationPolicy, AuthenticationSilo +from samba.samdb import SamDB, dsdb_Dn + +rc4_bit = security.KERB_ENCTYPE_RC4_HMAC_MD5 +aes256_sk_bit = security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK + +from samba.tests import TestCaseInTempDir, delete_force +import samba.tests.krb5.kcrypto as kcrypto +from samba.tests.krb5.raw_testcase import ( + KerberosCredentials, + KerberosTicketCreds, + RawKerberosTest, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +from samba.tests.krb5.rfc4120_constants import ( + AD_IF_RELEVANT, + AD_WIN2K_PAC, + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_PREAUTH_REQUIRED, + KDC_ERR_TGT_REVOKED, + KRB_AS_REP, + KRB_ERROR, + KRB_TGS_REP, + KU_AS_REP_ENC_PART, + KU_ENC_CHALLENGE_CLIENT, + KU_PA_ENC_TIMESTAMP, + KU_TICKET, + NT_PRINCIPAL, + NT_SRV_INST, + PADATA_ENCRYPTED_CHALLENGE, + PADATA_ENC_TIMESTAMP, + PADATA_ETYPE_INFO2, +) + +global_asn1_print = False +global_hexdump = False + + +class GroupType(Enum): + GLOBAL = GTYPE_SECURITY_GLOBAL_GROUP + DOMAIN_LOCAL = GTYPE_SECURITY_DOMAIN_LOCAL_GROUP + UNIVERSAL = GTYPE_SECURITY_UNIVERSAL_GROUP + + +# This simple class encapsulates the DN and SID of a Principal. +class Principal: + __slots__ = ['dn', 'sid'] + + def __init__(self, dn, sid): + if dn is not None and not isinstance(dn, ldb.Dn): + raise AssertionError(f'expected {dn} to be an ldb.Dn') + + self.dn = dn + self.sid = sid + + +class KDCBaseTest(TestCaseInTempDir, RawKerberosTest): + """ Base class for KDC tests. + """ + + class AccountType(Enum): + USER = object() + COMPUTER = object() + SERVER = object() + RODC = object() + MANAGED_SERVICE = object() + GROUP_MANAGED_SERVICE = object() + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._lp = None + + cls._ldb = None + cls._rodc_ldb = None + + cls._drsuapi_connection = None + + cls._functional_level = None + + # An identifier to ensure created accounts have unique names. Windows + # caches accounts based on usernames, so account names being different + # across test runs avoids previous test runs affecting the results. + cls.account_base = f'{secrets.token_hex(4)}_' + cls.account_id = 0 + + # A list containing DNs of accounts created as part of testing. + cls.accounts = [] + + cls.account_cache = {} + cls.policy_cache = {} + cls.tkt_cache = {} + + cls._rodc_ctx = None + + cls.ldb_cleanups = [] + + cls._claim_types_dn = None + cls._authn_policy_config_dn = None + cls._authn_policies_dn = None + cls._authn_silos_dn = None + + def get_claim_types_dn(self): + samdb = self.get_samdb() + + if self._claim_types_dn is None: + claim_config_dn = samdb.get_config_basedn() + + claim_config_dn.add_child('CN=Claims Configuration,CN=Services') + details = { + 'dn': claim_config_dn, + 'objectClass': 'container', + } + try: + samdb.add(details) + except ldb.LdbError as err: + num, _ = err.args + if num != ldb.ERR_ENTRY_ALREADY_EXISTS: + raise + else: + self.accounts.append(str(claim_config_dn)) + + claim_types_dn = claim_config_dn + claim_types_dn.add_child('CN=Claim Types') + details = { + 'dn': claim_types_dn, + 'objectClass': 'msDS-ClaimTypes', + } + try: + samdb.add(details) + except ldb.LdbError as err: + num, _ = err.args + if num != ldb.ERR_ENTRY_ALREADY_EXISTS: + raise + else: + self.accounts.append(str(claim_types_dn)) + + type(self)._claim_types_dn = claim_types_dn + + # Return a copy of the DN. + return ldb.Dn(samdb, str(self._claim_types_dn)) + + def get_authn_policy_config_dn(self): + samdb = self.get_samdb() + + if self._authn_policy_config_dn is None: + authn_policy_config_dn = samdb.get_config_basedn() + + authn_policy_config_dn.add_child( + 'CN=AuthN Policy Configuration,CN=Services') + details = { + 'dn': authn_policy_config_dn, + 'objectClass': 'container', + 'description': ('Contains configuration for authentication ' + 'policy'), + } + try: + samdb.add(details) + except ldb.LdbError as err: + num, _ = err.args + if num != ldb.ERR_ENTRY_ALREADY_EXISTS: + raise + else: + self.accounts.append(str(authn_policy_config_dn)) + + type(self)._authn_policy_config_dn = authn_policy_config_dn + + # Return a copy of the DN. + return ldb.Dn(samdb, str(self._authn_policy_config_dn)) + + def get_authn_policies_dn(self): + samdb = self.get_samdb() + + if self._authn_policies_dn is None: + authn_policies_dn = self.get_authn_policy_config_dn() + authn_policies_dn.add_child('CN=AuthN Policies') + details = { + 'dn': authn_policies_dn, + 'objectClass': 'msDS-AuthNPolicies', + 'description': 'Contains authentication policy objects', + } + try: + samdb.add(details) + except ldb.LdbError as err: + num, _ = err.args + if num != ldb.ERR_ENTRY_ALREADY_EXISTS: + raise + else: + self.accounts.append(str(authn_policies_dn)) + + type(self)._authn_policies_dn = authn_policies_dn + + # Return a copy of the DN. + return ldb.Dn(samdb, str(self._authn_policies_dn)) + + def get_authn_silos_dn(self): + samdb = self.get_samdb() + + if self._authn_silos_dn is None: + authn_silos_dn = self.get_authn_policy_config_dn() + authn_silos_dn.add_child('CN=AuthN Silos') + details = { + 'dn': authn_silos_dn, + 'objectClass': 'msDS-AuthNPolicySilos', + 'description': 'Contains authentication policy silo objects', + } + try: + samdb.add(details) + except ldb.LdbError as err: + num, _ = err.args + if num != ldb.ERR_ENTRY_ALREADY_EXISTS: + raise + else: + self.accounts.append(str(authn_silos_dn)) + + type(self)._authn_silos_dn = authn_silos_dn + + # Return a copy of the DN. + return ldb.Dn(samdb, str(self._authn_silos_dn)) + + @staticmethod + def freeze(m): + return frozenset((k, v) for k, v in m.items()) + + def tearDown(self): + # Run any cleanups that may modify accounts prior to deleting those + # accounts. + self.doCleanups() + + # Clean up any accounts created for single tests. + if self._ldb is not None: + for dn in reversed(self.test_accounts): + delete_force(self._ldb, dn) + + super().tearDown() + + @classmethod + def tearDownClass(cls): + # Clean up any accounts created by create_account. This is + # done in tearDownClass() rather than tearDown(), so that + # accounts need only be created once for permutation tests. + if cls._ldb is not None: + for cleanup in reversed(cls.ldb_cleanups): + try: + cls._ldb.modify(cleanup) + except ldb.LdbError: + pass + + for dn in reversed(cls.accounts): + delete_force(cls._ldb, dn) + + if cls._rodc_ctx is not None: + cls._rodc_ctx.cleanup_old_join(force=True) + + super().tearDownClass() + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + # A list containing DNs of accounts that should be removed when the + # current test finishes. + self.test_accounts = [] + + def get_lp(self): + if self._lp is None: + type(self)._lp = self.get_loadparm() + + return self._lp + + def get_samdb(self): + if self._ldb is None: + creds = self.get_admin_creds() + lp = self.get_lp() + + session = system_session() + type(self)._ldb = SamDB(url="ldap://%s" % self.dc_host, + session_info=session, + credentials=creds, + lp=lp) + + return self._ldb + + def get_rodc_samdb(self): + if self._rodc_ldb is None: + creds = self.get_admin_creds() + lp = self.get_lp() + + session = system_session() + type(self)._rodc_ldb = SamDB(url="ldap://%s" % self.host, + session_info=session, + credentials=creds, + lp=lp, + am_rodc=True) + + return self._rodc_ldb + + def get_drsuapi_connection(self): + if self._drsuapi_connection is None: + admin_creds = self.get_admin_creds() + samdb = self.get_samdb() + dns_hostname = samdb.host_dns_name() + type(self)._drsuapi_connection = drsuapi_connect(dns_hostname, + self.get_lp(), + admin_creds, + ip=self.dc_host) + + return self._drsuapi_connection + + def get_server_dn(self, samdb): + server = samdb.get_serverName() + + res = samdb.search(base=server, + scope=ldb.SCOPE_BASE, + attrs=['serverReference']) + dn = ldb.Dn(samdb, res[0]['serverReference'][0].decode('utf8')) + + return dn + + def get_mock_rodc_ctx(self): + if self._rodc_ctx is None: + admin_creds = self.get_admin_creds() + lp = self.get_lp() + + rodc_name = self.get_new_username() + site_name = 'Default-First-Site-Name' + + rodc_ctx = DCJoinContext(server=self.dc_host, + creds=admin_creds, + lp=lp, + site=site_name, + netbios_name=rodc_name, + targetdir=None, + domain=None) + self.create_rodc(rodc_ctx) + + type(self)._rodc_ctx = rodc_ctx + + return self._rodc_ctx + + def get_domain_functional_level(self, ldb=None): + if self._functional_level is None: + if ldb is None: + ldb = self.get_samdb() + + res = ldb.search(base='', + scope=SCOPE_BASE, + attrs=['domainFunctionality']) + try: + functional_level = int(res[0]['domainFunctionality'][0]) + except KeyError: + functional_level = DS_DOMAIN_FUNCTION_2000 + + type(self)._functional_level = functional_level + + return self._functional_level + + def get_default_enctypes(self, creds): + self.assertIsNotNone(creds, 'expected client creds to be passed in') + + functional_level = self.get_domain_functional_level() + + default_enctypes = [] + + if functional_level >= DS_DOMAIN_FUNCTION_2008: + # AES is only supported at functional level 2008 or higher + default_enctypes.append(kcrypto.Enctype.AES256) + default_enctypes.append(kcrypto.Enctype.AES128) + + if self.expect_nt_hash or creds.get_workstation(): + default_enctypes.append(kcrypto.Enctype.RC4) + + return default_enctypes + + def create_group(self, samdb, name, ou=None, gtype=None): + if ou is None: + ou = samdb.get_wellknown_dn(samdb.get_default_basedn(), + DS_GUID_USERS_CONTAINER) + + dn = f'CN={name},{ou}' + + # Remove the group if it exists; this will happen if a previous test + # run failed. + delete_force(samdb, dn) + + # Save the group name so it can be deleted in tearDownClass. + self.accounts.append(dn) + + details = { + 'dn': dn, + 'objectClass': 'group' + } + if gtype is not None: + details['groupType'] = common.normalise_int32(gtype) + samdb.add(details) + + return dn + + def get_dn_from_attribute(self, attribute): + return self.get_from_attribute(attribute).dn + + def get_dn_from_class(self, attribute): + return self.get_from_class(attribute).dn + + def get_schema_id_guid_from_attribute(self, attribute): + guid = self.get_from_attribute(attribute).get('schemaIDGUID', idx=0) + return misc.GUID(guid) + + def get_from_attribute(self, attribute): + return self.get_from_schema(attribute, 'attributeSchema') + + def get_from_class(self, attribute): + return self.get_from_schema(attribute, 'classSchema') + + def get_from_schema(self, name, object_class): + samdb = self.get_samdb() + schema_dn = samdb.get_schema_basedn() + + res = samdb.search(base=schema_dn, + scope=ldb.SCOPE_ONELEVEL, + attrs=['schemaIDGUID'], + expression=(f'(&(objectClass={object_class})' + f'(lDAPDisplayName={name}))')) + self.assertEqual(1, len(res), + f'could not locate {name} in {object_class}') + + return res[0] + + def create_authn_silo(self, *, + members=None, + user_policy=None, + computer_policy=None, + service_policy=None, + enforced=None): + samdb = self.get_samdb() + + silo_id = self.get_new_username() + + authn_silo_dn = self.get_authn_silos_dn() + authn_silo_dn.add_child(f'CN={silo_id}') + + details = { + 'dn': authn_silo_dn, + 'objectClass': 'msDS-AuthNPolicySilo', + } + + if enforced is True: + enforced = 'TRUE' + elif enforced is False: + enforced = 'FALSE' + + if members is not None: + details['msDS-AuthNPolicySiloMembers'] = members + if user_policy is not None: + details['msDS-UserAuthNPolicy'] = str(user_policy.dn) + if computer_policy is not None: + details['msDS-ComputerAuthNPolicy'] = str(computer_policy.dn) + if service_policy is not None: + details['msDS-ServiceAuthNPolicy'] = str(service_policy.dn) + if enforced is not None: + details['msDS-AuthNPolicySiloEnforced'] = enforced + + # Save the silo DN so it can be deleted in tearDownClass(). + self.accounts.append(str(authn_silo_dn)) + + # Remove the silo if it exists; this will happen if a previous test run + # failed. + delete_force(samdb, authn_silo_dn) + + samdb.add(details) + + return AuthenticationSilo.get(samdb, dn=authn_silo_dn) + + def create_authn_silo_claim_id(self): + claim_id = 'ad://ext/AuthenticationSilo' + + for_classes = [ + 'msDS-GroupManagedServiceAccount', + 'user', + 'msDS-ManagedServiceAccount', + 'computer', + ] + + self.create_claim(claim_id, + enabled=True, + single_valued=True, + value_space_restricted=False, + source_type='Constructed', + for_classes=for_classes, + value_type=claims.CLAIM_TYPE_STRING, + # It's OK if the claim type already exists. + force=False) + + return claim_id + + def create_authn_policy(self, *, + use_cache=True, + **kwargs): + + if use_cache: + cache_key = self.freeze(kwargs) + + authn_policy = self.policy_cache.get(cache_key) + if authn_policy is not None: + return authn_policy + + authn_policy = self.create_authn_policy_opts(**kwargs) + if use_cache: + self.policy_cache[cache_key] = authn_policy + + return authn_policy + + def create_authn_policy_opts(self, *, + enforced=None, + strong_ntlm_policy=None, + user_allowed_from=None, + user_allowed_ntlm=None, + user_allowed_to=None, + user_tgt_lifetime=None, + computer_allowed_to=None, + computer_tgt_lifetime=None, + service_allowed_from=None, + service_allowed_ntlm=None, + service_allowed_to=None, + service_tgt_lifetime=None): + samdb = self.get_samdb() + + policy_id = self.get_new_username() + + policy_dn = self.get_authn_policies_dn() + policy_dn.add_child(f'CN={policy_id}') + + details = { + 'dn': policy_dn, + 'objectClass': 'msDS-AuthNPolicy', + } + + _domain_sid = None + + def sd_from_sddl(sddl): + nonlocal _domain_sid + if _domain_sid is None: + _domain_sid = security.dom_sid(samdb.get_domain_sid()) + + return ndr_pack(security.descriptor.from_sddl(sddl, _domain_sid)) + + if enforced is True: + enforced = 'TRUE' + elif enforced is False: + enforced = 'FALSE' + + if user_allowed_ntlm is True: + user_allowed_ntlm = 'TRUE' + elif user_allowed_ntlm is False: + user_allowed_ntlm = 'FALSE' + + if service_allowed_ntlm is True: + service_allowed_ntlm = 'TRUE' + elif service_allowed_ntlm is False: + service_allowed_ntlm = 'FALSE' + + if enforced is not None: + details['msDS-AuthNPolicyEnforced'] = enforced + if strong_ntlm_policy is not None: + details['msDS-StrongNTLMPolicy'] = strong_ntlm_policy + + if user_allowed_from is not None: + details['msDS-UserAllowedToAuthenticateFrom'] = sd_from_sddl( + user_allowed_from) + if user_allowed_ntlm is not None: + details['msDS-UserAllowedNTLMNetworkAuthentication'] = ( + user_allowed_ntlm) + if user_allowed_to is not None: + details['msDS-UserAllowedToAuthenticateTo'] = sd_from_sddl( + user_allowed_to) + if user_tgt_lifetime is not None: + if isinstance(user_tgt_lifetime, numbers.Number): + user_tgt_lifetime = str(int(user_tgt_lifetime * 10_000_000)) + details['msDS-UserTGTLifetime'] = user_tgt_lifetime + + if computer_allowed_to is not None: + details['msDS-ComputerAllowedToAuthenticateTo'] = sd_from_sddl( + computer_allowed_to) + if computer_tgt_lifetime is not None: + if isinstance(computer_tgt_lifetime, numbers.Number): + computer_tgt_lifetime = str( + int(computer_tgt_lifetime * 10_000_000)) + details['msDS-ComputerTGTLifetime'] = computer_tgt_lifetime + + if service_allowed_from is not None: + details['msDS-ServiceAllowedToAuthenticateFrom'] = sd_from_sddl( + service_allowed_from) + if service_allowed_ntlm is not None: + details['msDS-ServiceAllowedNTLMNetworkAuthentication'] = ( + service_allowed_ntlm) + if service_allowed_to is not None: + details['msDS-ServiceAllowedToAuthenticateTo'] = sd_from_sddl( + service_allowed_to) + if service_tgt_lifetime is not None: + if isinstance(service_tgt_lifetime, numbers.Number): + service_tgt_lifetime = str( + int(service_tgt_lifetime * 10_000_000)) + details['msDS-ServiceTGTLifetime'] = service_tgt_lifetime + + # Save the policy DN so it can be deleted in tearDownClass(). + self.accounts.append(str(policy_dn)) + + # Remove the policy if it exists; this will happen if a previous test + # run failed. + delete_force(samdb, policy_dn) + + samdb.add(details) + + return AuthenticationPolicy.get(samdb, dn=policy_dn) + + def create_claim(self, + claim_id, + enabled=None, + attribute=None, + single_valued=None, + value_space_restricted=None, + source=None, + source_type=None, + for_classes=None, + value_type=None, + force=True): + samdb = self.get_samdb() + + claim_dn = self.get_claim_types_dn() + claim_dn.add_child(f'CN={claim_id}') + + details = { + 'dn': claim_dn, + 'objectClass': 'msDS-ClaimType', + } + + if enabled is True: + enabled = 'TRUE' + elif enabled is False: + enabled = 'FALSE' + + if attribute is not None: + attribute = str(self.get_dn_from_attribute(attribute)) + + if single_valued is True: + single_valued = 'TRUE' + elif single_valued is False: + single_valued = 'FALSE' + + if value_space_restricted is True: + value_space_restricted = 'TRUE' + elif value_space_restricted is False: + value_space_restricted = 'FALSE' + + if for_classes is not None: + for_classes = [str(self.get_dn_from_class(name)) + for name in for_classes] + + if isinstance(value_type, int): + value_type = str(value_type) + + if enabled is not None: + details['Enabled'] = enabled + if attribute is not None: + details['msDS-ClaimAttributeSource'] = attribute + if single_valued is not None: + details['msDS-ClaimIsSingleValued'] = single_valued + if value_space_restricted is not None: + details['msDS-ClaimIsValueSpaceRestricted'] = ( + value_space_restricted) + if source is not None: + details['msDS-ClaimSource'] = source + if source_type is not None: + details['msDS-ClaimSourceType'] = source_type + if for_classes is not None: + details['msDS-ClaimTypeAppliesToClass'] = for_classes + if value_type is not None: + details['msDS-ClaimValueType'] = value_type + + if force: + # Remove the claim if it exists; this will happen if a previous + # test run failed + delete_force(samdb, claim_dn) + + try: + samdb.add(details) + except ldb.LdbError as err: + num, estr = err.args + if num != ldb.ERR_ENTRY_ALREADY_EXISTS: + raise + self.assertFalse(force, 'should not fail with force=True') + else: + # Save the claim DN so it can be deleted in tearDownClass() + self.accounts.append(str(claim_dn)) + + def create_account(self, samdb, name, account_type=AccountType.USER, + spn=None, upn=None, additional_details=None, + ou=None, account_control=0, add_dollar=None, + expired_password=False, force_nt4_hash=False, + preserve=True): + """Create an account for testing. + The dn of the created account is added to self.accounts, + which is used by tearDownClass to clean up the created accounts. + """ + if add_dollar is None and account_type is not self.AccountType.USER: + add_dollar = True + + if ou is None: + if account_type is self.AccountType.COMPUTER: + guid = DS_GUID_COMPUTERS_CONTAINER + elif account_type is self.AccountType.MANAGED_SERVICE or ( + account_type is self.AccountType.GROUP_MANAGED_SERVICE): + guid = DS_GUID_MANAGED_SERVICE_ACCOUNTS_CONTAINER + elif account_type is self.AccountType.SERVER: + guid = DS_GUID_DOMAIN_CONTROLLERS_CONTAINER + else: + guid = DS_GUID_USERS_CONTAINER + + ou = samdb.get_wellknown_dn(samdb.get_default_basedn(), guid) + + dn = "CN=%s,%s" % (name, ou) + + # remove the account if it exists, this will happen if a previous test + # run failed + delete_force(samdb, dn) + account_name = name + if add_dollar: + account_name += '$' + secure_schannel_type = SEC_CHAN_NULL + if account_type is self.AccountType.USER: + object_class = "user" + account_control |= UF_NORMAL_ACCOUNT + elif account_type is self.AccountType.MANAGED_SERVICE: + object_class = "msDS-ManagedServiceAccount" + account_control |= UF_WORKSTATION_TRUST_ACCOUNT + secure_schannel_type = SEC_CHAN_WKSTA + elif account_type is self.AccountType.GROUP_MANAGED_SERVICE: + object_class = "msDS-GroupManagedServiceAccount" + account_control |= UF_WORKSTATION_TRUST_ACCOUNT + secure_schannel_type = SEC_CHAN_WKSTA + else: + object_class = "computer" + if account_type is self.AccountType.COMPUTER: + account_control |= UF_WORKSTATION_TRUST_ACCOUNT + secure_schannel_type = SEC_CHAN_WKSTA + elif account_type is self.AccountType.SERVER: + account_control |= UF_SERVER_TRUST_ACCOUNT + secure_schannel_type = SEC_CHAN_BDC + else: + self.fail() + + details = { + "dn": dn, + "objectClass": object_class, + "sAMAccountName": account_name, + "userAccountControl": str(account_control), + } + + if account_type is self.AccountType.GROUP_MANAGED_SERVICE: + password = None + else: + password = generate_random_password(32, 32) + utf16pw = ('"%s"' % password).encode('utf-16-le') + + details['unicodePwd'] = utf16pw + + if upn is not None: + upn = upn.format(account=account_name) + if spn is not None: + if isinstance(spn, str): + spn = spn.format(account=account_name) + else: + spn = tuple(s.format(account=account_name) for s in spn) + details["servicePrincipalName"] = spn + if upn is not None: + details["userPrincipalName"] = upn + if expired_password: + details["pwdLastSet"] = "0" + if additional_details is not None: + details.update(additional_details) + if preserve: + # Mark this account for deletion in tearDownClass() after all the + # tests in this class finish. + self.accounts.append(dn) + else: + # Mark this account for deletion in tearDown() after the current + # test finishes. Because the time complexity of deleting an account + # in Samba scales with the number of accounts, it is faster to + # delete accounts as soon as possible than to keep them around + # until all the tests are finished. + self.test_accounts.append(dn) + samdb.add(details) + + expected_kvno = 1 + + if force_nt4_hash: + admin_creds = self.get_admin_creds() + lp = self.get_lp() + net_ctx = net.Net(admin_creds, lp, server=self.dc_host) + domain = samdb.domain_netbios_name().upper() + + password = generate_random_password(32, 32) + utf16pw = ('"%s"' % password).encode('utf-16-le') + + try: + net_ctx.set_password(newpassword=password, + account_name=account_name, + domain_name=domain, + force_samr_18=True) + expected_kvno += 1 + except Exception as e: + self.fail(e) + + creds = KerberosCredentials() + creds.guess(self.get_lp()) + creds.set_realm(samdb.domain_dns_name().upper()) + creds.set_domain(samdb.domain_netbios_name().upper()) + if password is not None: + creds.set_password(password) + creds.set_username(account_name) + if account_type is self.AccountType.USER: + creds.set_workstation('') + else: + creds.set_workstation(name) + creds.set_secure_channel_type(secure_schannel_type) + creds.set_dn(ldb.Dn(samdb, dn)) + creds.set_upn(upn) + creds.set_spn(spn) + creds.set_type(account_type) + + self.creds_set_enctypes(creds) + + res = samdb.search(base=dn, + scope=ldb.SCOPE_BASE, + attrs=['msDS-KeyVersionNumber', + 'objectSid']) + + kvno = res[0].get('msDS-KeyVersionNumber', idx=0) + if kvno is not None: + self.assertEqual(int(kvno), expected_kvno) + creds.set_kvno(expected_kvno) + + sid = res[0].get('objectSid', idx=0) + sid = samdb.schema_format_value('objectSID', sid) + sid = sid.decode('utf-8') + creds.set_sid(sid) + + return (creds, dn) + + def get_security_descriptor(self, dn): + samdb = self.get_samdb() + + sid = self.get_objectSid(samdb, dn) + + owner_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + + ace = security.ace() + ace.access_mask = security.SEC_ADS_CONTROL_ACCESS + + ace.trustee = security.dom_sid(sid) + + dacl = security.acl() + dacl.revision = security.SECURITY_ACL_REVISION_ADS + dacl.aces = [ace] + dacl.num_aces = 1 + + security_desc = security.descriptor() + security_desc.type |= security.SEC_DESC_DACL_PRESENT + security_desc.owner_sid = owner_sid + security_desc.dacl = dacl + + return ndr_pack(security_desc) + + def create_rodc(self, ctx): + ctx.nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn] + ctx.full_nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn] + ctx.krbtgt_dn = f'CN=krbtgt_{ctx.myname},CN=Users,{ctx.base_dn}' + + ctx.never_reveal_sid = [f'<SID={ctx.domsid}-{security.DOMAIN_RID_RODC_DENY}>', + f'<SID={security.SID_BUILTIN_ADMINISTRATORS}>', + f'<SID={security.SID_BUILTIN_SERVER_OPERATORS}>', + f'<SID={security.SID_BUILTIN_BACKUP_OPERATORS}>', + f'<SID={security.SID_BUILTIN_ACCOUNT_OPERATORS}>'] + ctx.reveal_sid = f'<SID={ctx.domsid}-{security.DOMAIN_RID_RODC_ALLOW}>' + + mysid = ctx.get_mysid() + admin_dn = f'<SID={mysid}>' + ctx.managedby = admin_dn + + ctx.userAccountControl = (UF_WORKSTATION_TRUST_ACCOUNT | + UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION | + UF_PARTIAL_SECRETS_ACCOUNT) + + ctx.connection_dn = f'CN=RODC Connection (FRS),{ctx.ntds_dn}' + ctx.secure_channel_type = misc.SEC_CHAN_RODC + ctx.RODC = True + ctx.replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_GET_ANC | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED | + drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING) + ctx.domain_replica_flags = ctx.replica_flags | drsuapi.DRSUAPI_DRS_CRITICAL_ONLY + + ctx.build_nc_lists() + + ctx.cleanup_old_join() + + try: + ctx.join_add_objects() + except Exception: + # cleanup the failed join (checking we still have a live LDB + # connection to the remote DC first) + ctx.refresh_ldb_connection() + ctx.cleanup_old_join() + raise + + def replicate_account_to_rodc(self, dn): + samdb = self.get_samdb() + rodc_samdb = self.get_rodc_samdb() + + repl_val = f'{samdb.get_dsServiceName()}:{dn}:SECRETS_ONLY' + + msg = ldb.Message() + msg.dn = ldb.Dn(rodc_samdb, '') + msg['replicateSingleObject'] = ldb.MessageElement( + repl_val, + ldb.FLAG_MOD_REPLACE, + 'replicateSingleObject') + + try: + # Try replication using the replicateSingleObject rootDSE + # operation. + rodc_samdb.modify(msg) + except ldb.LdbError as err: + enum, estr = err.args + self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM) + self.assertIn('rootdse_modify: unknown attribute to change!', + estr) + + # If that method wasn't supported, we may be in the rodc:local test + # environment, where we can try replicating to the local database. + + lp = self.get_lp() + + rodc_creds = Credentials() + rodc_creds.guess(lp) + rodc_creds.set_machine_account(lp) + + local_samdb = SamDB(url=None, session_info=system_session(), + credentials=rodc_creds, lp=lp) + + destination_dsa_guid = misc.GUID(local_samdb.get_ntds_GUID()) + + repl = drs_Replicate(f'ncacn_ip_tcp:{self.dc_host}[seal]', + lp, rodc_creds, + local_samdb, destination_dsa_guid) + + source_dsa_invocation_id = misc.GUID(samdb.invocation_id) + + repl.replicate(dn, + source_dsa_invocation_id, + destination_dsa_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + rodc=True) + + def reveal_account_to_mock_rodc(self, dn): + samdb = self.get_samdb() + rodc_ctx = self.get_mock_rodc_ctx() + + self.get_secrets( + dn, + destination_dsa_guid=rodc_ctx.ntds_guid, + source_dsa_invocation_id=misc.GUID(samdb.invocation_id)) + + def check_revealed(self, dn, rodc_dn, revealed=True): + samdb = self.get_samdb() + + res = samdb.search(base=rodc_dn, + scope=ldb.SCOPE_BASE, + attrs=['msDS-RevealedUsers']) + + revealed_users = res[0].get('msDS-RevealedUsers') + if revealed_users is None: + self.assertFalse(revealed) + return + + revealed_dns = set(str(dsdb_Dn(samdb, str(user), + syntax_oid=DSDB_SYNTAX_BINARY_DN).dn) + for user in revealed_users) + + if revealed: + self.assertIn(str(dn), revealed_dns) + else: + self.assertNotIn(str(dn), revealed_dns) + + def get_secrets(self, dn, + destination_dsa_guid, + source_dsa_invocation_id): + bind, handle, _ = self.get_drsuapi_connection() + + req = drsuapi.DsGetNCChangesRequest8() + + req.destination_dsa_guid = destination_dsa_guid + req.source_dsa_invocation_id = source_dsa_invocation_id + + naming_context = drsuapi.DsReplicaObjectIdentifier() + naming_context.dn = dn + + req.naming_context = naming_context + + hwm = drsuapi.DsReplicaHighWaterMark() + hwm.tmp_highest_usn = 0 + hwm.reserved_usn = 0 + hwm.highest_usn = 0 + + req.highwatermark = hwm + req.uptodateness_vector = None + + req.replica_flags = 0 + + req.max_object_count = 1 + req.max_ndr_size = 402116 + req.extended_op = drsuapi.DRSUAPI_EXOP_REPL_SECRET + + attids = [drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_unicodePwd, + drsuapi.DRSUAPI_ATTID_ntPwdHistory] + + partial_attribute_set = drsuapi.DsPartialAttributeSet() + partial_attribute_set.version = 1 + partial_attribute_set.attids = attids + partial_attribute_set.num_attids = len(attids) + + req.partial_attribute_set = partial_attribute_set + + req.partial_attribute_set_ex = None + req.mapping_ctr.num_mappings = 0 + req.mapping_ctr.mappings = None + + _, ctr = bind.DsGetNCChanges(handle, 8, req) + + self.assertEqual(1, ctr.object_count) + + identifier = ctr.first_object.object.identifier + attributes = ctr.first_object.object.attribute_ctr.attributes + + self.assertEqual(dn, identifier.dn) + + return bind, identifier, attributes + + def get_keys(self, creds, expected_etypes=None): + admin_creds = self.get_admin_creds() + samdb = self.get_samdb() + + dn = creds.get_dn() + + bind, identifier, attributes = self.get_secrets( + str(dn), + destination_dsa_guid=misc.GUID(samdb.get_ntds_GUID()), + source_dsa_invocation_id=misc.GUID()) + + rid = identifier.sid.split()[1] + + net_ctx = net.Net(admin_creds) + + keys = {} + + for attr in attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_supplementalCredentials: + net_ctx.replicate_decrypt(bind, attr, rid) + if attr.value_ctr.num_values == 0: + continue + attr_val = attr.value_ctr.values[0].blob + + spl = ndr_unpack(drsblobs.supplementalCredentialsBlob, + attr_val) + for pkg in spl.sub.packages: + if pkg.name == 'Primary:Kerberos-Newer-Keys': + krb5_new_keys_raw = binascii.a2b_hex(pkg.data) + krb5_new_keys = ndr_unpack( + drsblobs.package_PrimaryKerberosBlob, + krb5_new_keys_raw) + for key in krb5_new_keys.ctr.keys: + keytype = key.keytype + if keytype in (kcrypto.Enctype.AES256, + kcrypto.Enctype.AES128): + keys[keytype] = key.value.hex() + elif attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd: + net_ctx.replicate_decrypt(bind, attr, rid) + if attr.value_ctr.num_values > 0: + pwd = attr.value_ctr.values[0].blob + keys[kcrypto.Enctype.RC4] = pwd.hex() + + if expected_etypes is None: + expected_etypes = self.get_default_enctypes(creds) + + self.assertCountEqual(expected_etypes, keys) + + return keys + + def creds_set_keys(self, creds, keys): + if keys is not None: + for enctype, key in keys.items(): + creds.set_forced_key(enctype, key) + + def creds_set_enctypes(self, creds, + extra_bits=None, + remove_bits=None): + samdb = self.get_samdb() + + res = samdb.search(creds.get_dn(), + scope=ldb.SCOPE_BASE, + attrs=['msDS-SupportedEncryptionTypes']) + supported_enctypes = res[0].get('msDS-SupportedEncryptionTypes', idx=0) + + if supported_enctypes is None: + supported_enctypes = self.default_etypes + if supported_enctypes is None: + lp = self.get_lp() + supported_enctypes = lp.get('kdc default domain supported enctypes') + if supported_enctypes == 0: + supported_enctypes = rc4_bit | aes256_sk_bit + supported_enctypes = int(supported_enctypes) + + if extra_bits is not None: + # We need to add in implicit or implied encryption types. + supported_enctypes |= extra_bits + if remove_bits is not None: + # We also need to remove certain bits, such as the non-encryption + # type bit aes256-sk. + supported_enctypes &= ~remove_bits + + creds.set_as_supported_enctypes(supported_enctypes) + creds.set_tgs_supported_enctypes(supported_enctypes) + creds.set_ap_supported_enctypes(supported_enctypes) + + def creds_set_default_enctypes(self, creds, + fast_support=False, + claims_support=False, + compound_id_support=False): + default_enctypes = self.get_default_enctypes(creds) + supported_enctypes = KerberosCredentials.etypes_to_bits( + default_enctypes) + + if fast_support: + supported_enctypes |= security.KERB_ENCTYPE_FAST_SUPPORTED + if claims_support: + supported_enctypes |= security.KERB_ENCTYPE_CLAIMS_SUPPORTED + if compound_id_support: + supported_enctypes |= ( + security.KERB_ENCTYPE_COMPOUND_IDENTITY_SUPPORTED) + + creds.set_as_supported_enctypes(supported_enctypes) + creds.set_tgs_supported_enctypes(supported_enctypes) + creds.set_ap_supported_enctypes(supported_enctypes) + + def add_to_group(self, account_dn, group_dn, group_attr, expect_attr=True, + new_group_type=None): + samdb = self.get_samdb() + + try: + res = samdb.search(base=group_dn, + scope=ldb.SCOPE_BASE, + attrs=[group_attr]) + except ldb.LdbError as err: + num, _ = err.args + if num != ldb.ERR_NO_SUCH_OBJECT: + raise + + self.fail(err) + + orig_msg = res[0] + members = orig_msg.get(group_attr) + if expect_attr: + self.assertIsNotNone(members) + elif members is None: + members = () + else: + members = map(lambda s: s.decode('utf-8'), members) + + # Use a set so we can handle the same group being added twice. + members = set(members) + + self.assertNotIsInstance(account_dn, ldb.Dn, + 'ldb.MessageElement does not support ldb.Dn') + self.assertNotIsInstance(account_dn, bytes) + + if isinstance(account_dn, str): + members.add(account_dn) + else: + members.update(account_dn) + + msg = ldb.Message() + msg.dn = group_dn + if new_group_type is not None: + msg['0'] = ldb.MessageElement( + common.normalise_int32(new_group_type), + ldb.FLAG_MOD_REPLACE, + 'groupType') + msg['1'] = ldb.MessageElement(list(members), + ldb.FLAG_MOD_REPLACE, + group_attr) + cleanup = samdb.msg_diff(msg, orig_msg) + self.ldb_cleanups.append(cleanup) + samdb.modify(msg) + + return cleanup + + def remove_from_group(self, account_dn, group_dn): + samdb = self.get_samdb() + + res = samdb.search(base=group_dn, + scope=ldb.SCOPE_BASE, + attrs=['member']) + orig_msg = res[0] + self.assertIn('member', orig_msg) + members = list(orig_msg['member']) + + account_dn = str(account_dn).encode('utf-8') + self.assertIn(account_dn, members) + members.remove(account_dn) + + msg = ldb.Message() + msg.dn = group_dn + msg['member'] = ldb.MessageElement(members, + ldb.FLAG_MOD_REPLACE, + 'member') + + cleanup = samdb.msg_diff(msg, orig_msg) + self.ldb_cleanups.append(cleanup) + samdb.modify(msg) + + return cleanup + + # Create a new group and return a Principal object representing it. + def create_group_principal(self, samdb, group_type): + name = self.get_new_username() + dn = self.create_group(samdb, name, gtype=group_type.value) + sid = self.get_objectSid(samdb, dn) + + return Principal(ldb.Dn(samdb, dn), sid) + + def set_group_type(self, samdb, dn, gtype): + group_type = common.normalise_int32(gtype.value) + msg = ldb.Message(dn) + msg['groupType'] = ldb.MessageElement(group_type, + ldb.FLAG_MOD_REPLACE, + 'groupType') + samdb.modify(msg) + + def set_primary_group(self, samdb, dn, primary_sid, + expected_error=None, + expected_werror=None): + # Get the RID to be set as our primary group. + primary_rid = primary_sid.rsplit('-', 1)[1] + + # Find out our current primary group. + res = samdb.search(dn, + scope=ldb.SCOPE_BASE, + attrs=['primaryGroupId']) + orig_msg = res[0] + + # Prepare to modify the attribute. + msg = ldb.Message(dn) + msg['primaryGroupId'] = ldb.MessageElement(str(primary_rid), + ldb.FLAG_MOD_REPLACE, + 'primaryGroupId') + + # We'll remove the primaryGroupId attribute after the test, to avoid + # problems in the teardown if the user outlives the group. + remove_msg = samdb.msg_diff(msg, orig_msg) + self.addCleanup(samdb.modify, remove_msg) + + # Set primaryGroupId. + if expected_error is None: + self.assertIsNone(expected_werror) + + samdb.modify(msg) + else: + self.assertIsNotNone(expected_werror) + + with self.assertRaises( + ldb.LdbError, + msg='expected setting primary group to fail' + ) as err: + samdb.modify(msg) + + error, estr = err.exception.args + self.assertEqual(expected_error, error) + self.assertIn(f'{expected_werror:08X}', estr) + + # Create an arrangement of groups based on a configuration specified in a + # test case. 'user_principal' is a principal representing the user account; + # 'trust_principal', a principal representing the account of a user from + # another domain. + def setup_groups(self, + samdb, + preexisting_groups, + group_setup, + primary_groups): + groups = dict(preexisting_groups) + + primary_group_types = {} + + # Create each group and add it to the group mapping. + if group_setup is not None: + for group_id, (group_type, _) in group_setup.items(): + self.assertNotIn(group_id, preexisting_groups, + "don't specify placeholders") + self.assertNotIn(group_id, groups, + 'group ID specified more than once') + + if primary_groups is not None and ( + group_id in primary_groups.values()): + # Windows disallows setting a domain-local group as a + # primary group, unless we create it as Universal first and + # change it back to Domain-Local later. + primary_group_types[group_id] = group_type + group_type = GroupType.UNIVERSAL + + groups[group_id] = self.create_group_principal(samdb, + group_type) + + if group_setup is not None: + # Map a group ID to that group's DN, and generate an + # understandable error message if the mapping fails. + def group_id_to_dn(group_id): + try: + group = groups[group_id] + except KeyError: + self.fail(f"included group member '{group_id}', but it is " + f"not specified in {groups.keys()}") + else: + if group.dn is not None: + return str(group.dn) + + return f'<SID={group.sid}>' + + # Populate each group's members. + for group_id, (_, members) in group_setup.items(): + # Get the group's DN and the mapped DNs of its members. + dn = groups[group_id].dn + principal_members = map(group_id_to_dn, members) + + # Add the members to the group. + self.add_to_group(principal_members, dn, 'member', + expect_attr=False) + + # Set primary groups. + if primary_groups is not None: + for user, primary_group in primary_groups.items(): + primary_sid = groups[primary_group].sid + self.set_primary_group(samdb, user.dn, primary_sid) + + # Change the primary groups to their actual group types. + for primary_group, primary_group_type in primary_group_types.items(): + self.set_group_type(samdb, + groups[primary_group].dn, + primary_group_type) + + # Return the mapping from group IDs to principals. + return groups + + def map_to_sid(self, val, mapping, domain_sid): + if isinstance(val, int): + # If it's an integer, we assume it's a RID, and prefix the domain + # SID. + self.assertIsNotNone(domain_sid) + return f'{domain_sid}-{val}' + + if mapping is not None and val in mapping: + # Or if we have a mapping for it, apply that. + return mapping[val].sid + + # Otherwise leave it unmodified. + return val + + def map_to_dn(self, val, mapping, domain_sid): + sid = self.map_to_sid(val, mapping, domain_sid) + return ldb.Dn(self.get_samdb(), f'<SID={sid}>') + + # Return SIDs from principal placeholders based on a supplied mapping. + def map_sids(self, sids, mapping, domain_sid): + if sids is None: + return None + + mapped_sids = set() + + for entry in sids: + if isinstance(entry, frozenset): + mapped_sids.add(frozenset(self.map_sids(entry, + mapping, + domain_sid))) + else: + val, sid_type, attrs = entry + sid = self.map_to_sid(val, mapping, domain_sid) + + # There's no point expecting the 'Claims Valid' SID to be + # present if we don't support claims. Filter it out to give the + # tests a chance of passing. + if not self.kdc_claims_support and ( + sid == security.SID_CLAIMS_VALID): + continue + + mapped_sids.add((sid, sid_type, attrs)) + + return mapped_sids + + def issued_by_rodc(self, ticket): + rodc_krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + rodc_krbtgt_key = self.TicketDecryptionKey_from_creds( + rodc_krbtgt_creds) + + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: rodc_krbtgt_key, + } + + return self.modified_ticket( + ticket, + new_ticket_key=rodc_krbtgt_key, + checksum_keys=checksum_keys) + + def signed_by_rodc(self, ticket): + rodc_krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + rodc_krbtgt_key = self.TicketDecryptionKey_from_creds( + rodc_krbtgt_creds) + + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: rodc_krbtgt_key, + } + + return self.modified_ticket(ticket, + checksum_keys=checksum_keys) + + # Get a ticket with the SIDs in the PAC replaced with ones we specify. This + # is useful for creating arbitrary tickets that can be used to perform a + # TGS-REQ. + def ticket_with_sids(self, + ticket, + new_sids, + domain_sid, + user_rid, + set_user_flags=0, + reset_user_flags=0, + from_rodc=False): + if from_rodc: + krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + else: + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: krbtgt_key + } + + modify_pac_fn = partial(self.set_pac_sids, + new_sids=new_sids, + domain_sid=domain_sid, + user_rid=user_rid, + set_user_flags=set_user_flags, + reset_user_flags=reset_user_flags) + + return self.modified_ticket(ticket, + new_ticket_key=krbtgt_key, + modify_pac_fn=modify_pac_fn, + checksum_keys=checksum_keys) + + # Replace the SIDs in a PAC with 'new_sids'. + def set_pac_sids(self, + pac, + *, + new_sids, + domain_sid=None, + user_rid=None, + set_user_flags=0, + reset_user_flags=0): + if domain_sid is None: + domain_sid = self.get_samdb().get_domain_sid() + + base_sids = [] + extra_sids = [] + resource_sids = [] + + resource_domain = None + + primary_gid = None + + # Filter our SIDs into three arrays depending on their ultimate + # location in the PAC. + for sid, sid_type, attrs in new_sids: + if sid_type is self.SidType.BASE_SID: + if isinstance(sid, int): + domain, rid = domain_sid, sid + else: + domain, rid = sid.rsplit('-', 1) + self.assertEqual(domain_sid, domain, + f'base SID {sid} must be in our domain') + + base_sid = samr.RidWithAttribute() + base_sid.rid = int(rid) + base_sid.attributes = attrs + + base_sids.append(base_sid) + elif sid_type is self.SidType.EXTRA_SID: + extra_sid = netlogon.netr_SidAttr() + extra_sid.sid = security.dom_sid(sid) + extra_sid.attributes = attrs + + extra_sids.append(extra_sid) + elif sid_type is self.SidType.RESOURCE_SID: + if isinstance(sid, int): + domain, rid = domain_sid, sid + else: + domain, rid = sid.rsplit('-', 1) + if resource_domain is None: + resource_domain = domain + else: + self.assertEqual(resource_domain, domain, + 'resource SIDs must share the same ' + 'domain') + + resource_sid = samr.RidWithAttribute() + resource_sid.rid = int(rid) + resource_sid.attributes = attrs + + resource_sids.append(resource_sid) + elif sid_type is self.SidType.PRIMARY_GID: + self.assertIsNone(primary_gid, + f'must not specify a second primary GID ' + f'{sid}') + self.assertIsNone(attrs, 'cannot specify primary GID attrs') + + if isinstance(sid, int): + domain, primary_gid = domain_sid, sid + else: + domain, primary_gid = sid.rsplit('-', 1) + self.assertEqual(domain_sid, domain, + f'primary GID {sid} must be in our domain') + else: + self.fail(f'invalid SID type {sid_type}') + + found_logon_info = False + + pac_buffers = pac.buffers + for pac_buffer in pac_buffers: + # Find the LOGON_INFO PAC buffer. + if pac_buffer.type == krb5pac.PAC_TYPE_LOGON_INFO: + logon_info = pac_buffer.info.info + + # Add Extra SIDs and set the EXTRA_SIDS flag as needed. + logon_info.info3.sidcount = len(extra_sids) + if extra_sids: + logon_info.info3.sids = extra_sids + logon_info.info3.base.user_flags |= ( + netlogon.NETLOGON_EXTRA_SIDS) + else: + logon_info.info3.sids = None + logon_info.info3.base.user_flags &= ~( + netlogon.NETLOGON_EXTRA_SIDS) + + # Add Base SIDs. + logon_info.info3.base.groups.count = len(base_sids) + if base_sids: + logon_info.info3.base.groups.rids = base_sids + else: + logon_info.info3.base.groups.rids = None + + logon_info.info3.base.domain_sid = security.dom_sid(domain_sid) + if user_rid is not None: + logon_info.info3.base.rid = int(user_rid) + + if primary_gid is not None: + logon_info.info3.base.primary_gid = int(primary_gid) + + # Add Resource SIDs and set the RESOURCE_GROUPS flag as needed. + logon_info.resource_groups.groups.count = len(resource_sids) + if resource_sids: + resource_domain = security.dom_sid(resource_domain) + logon_info.resource_groups.domain_sid = resource_domain + logon_info.resource_groups.groups.rids = resource_sids + logon_info.info3.base.user_flags |= ( + netlogon.NETLOGON_RESOURCE_GROUPS) + else: + logon_info.resource_groups.domain_sid = None + logon_info.resource_groups.groups.rids = None + logon_info.info3.base.user_flags &= ~( + netlogon.NETLOGON_RESOURCE_GROUPS) + + logon_info.info3.base.user_flags |= set_user_flags + logon_info.info3.base.user_flags &= ~reset_user_flags + + found_logon_info = True + + # Also replace the user's SID in the UPN DNS buffer. + elif pac_buffer.type == krb5pac.PAC_TYPE_UPN_DNS_INFO: + upn_dns_info_ex = pac_buffer.info.ex + + if user_rid is not None: + upn_dns_info_ex.objectsid = security.dom_sid( + f'{domain_sid}-{user_rid}') + + # But don't replace the user's SID in the Requester SID buffer, or + # we'll get a SID mismatch. + + self.assertTrue(found_logon_info, 'no LOGON_INFO PAC buffer') + + pac.buffers = pac_buffers + + return pac + + # Replace the device SIDs in a PAC with 'new_sids'. + def set_pac_device_sids(self, + pac, + *, + new_sids, + domain_sid=None, + user_rid): + if domain_sid is None: + domain_sid = self.get_samdb().get_domain_sid() + + base_sids = [] + extra_sids = [] + resource_sids = [] + + primary_gid = None + + # Filter our SIDs into three arrays depending on their ultimate + # location in the PAC. + for entry in new_sids: + if isinstance(entry, frozenset): + resource_domain = None + domain_sids = [] + + for sid, sid_type, attrs in entry: + self.assertIs(sid_type, self.SidType.RESOURCE_SID, + 'only resource SIDs may be specified in this way') + + if isinstance(sid, int): + domain, rid = domain_sid, sid + else: + domain, rid = sid.rsplit('-', 1) + if resource_domain is None: + resource_domain = domain + else: + self.assertEqual(resource_domain, domain, + 'resource SIDs must share the same ' + 'domain') + + resource_sid = samr.RidWithAttribute() + resource_sid.rid = int(rid) + resource_sid.attributes = attrs + + domain_sids.append(resource_sid) + + membership = krb5pac.PAC_DOMAIN_GROUP_MEMBERSHIP() + if resource_domain is not None: + membership.domain_sid = security.dom_sid(resource_domain) + membership.groups.rids = domain_sids + membership.groups.count = len(domain_sids) + + resource_sids.append(membership) + else: + sid, sid_type, attrs = entry + if sid_type is self.SidType.BASE_SID: + if isinstance(sid, int): + domain, rid = domain_sid, sid + else: + domain, rid = sid.rsplit('-', 1) + self.assertEqual(domain_sid, domain, + f'base SID {sid} must be in our domain') + + base_sid = samr.RidWithAttribute() + base_sid.rid = int(rid) + base_sid.attributes = attrs + + base_sids.append(base_sid) + elif sid_type is self.SidType.EXTRA_SID: + extra_sid = netlogon.netr_SidAttr() + extra_sid.sid = security.dom_sid(sid) + extra_sid.attributes = attrs + + extra_sids.append(extra_sid) + elif sid_type is self.SidType.RESOURCE_SID: + self.fail('specify resource groups in frozenset(s)') + elif sid_type is self.SidType.PRIMARY_GID: + self.assertIsNone(primary_gid, + f'must not specify a second primary GID ' + f'{sid}') + self.assertIsNone(attrs, 'cannot specify primary GID attrs') + + if isinstance(sid, int): + domain, primary_gid = domain_sid, sid + else: + domain, primary_gid = sid.rsplit('-', 1) + self.assertEqual(domain_sid, domain, + f'primary GID {sid} must be in our domain') + else: + self.fail(f'invalid SID type {sid_type}') + + pac_buffers = pac.buffers + for pac_buffer in pac_buffers: + # Find the DEVICE_INFO PAC buffer. + if pac_buffer.type == krb5pac.PAC_TYPE_DEVICE_INFO: + logon_info = pac_buffer.info.info + break + else: + logon_info = krb5pac.PAC_DEVICE_INFO() + + logon_info_ctr = krb5pac.PAC_DEVICE_INFO_CTR() + logon_info_ctr.info = logon_info + + pac_buffer = krb5pac.PAC_BUFFER() + pac_buffer.type = krb5pac.PAC_TYPE_DEVICE_INFO + pac_buffer.info = logon_info_ctr + + pac_buffers.append(pac_buffer) + + logon_info.domain_sid = security.dom_sid(domain_sid) + logon_info.rid = int(user_rid) + + self.assertIsNotNone(primary_gid, 'please specify the primary GID') + logon_info.primary_gid = int(primary_gid) + + # Add Base SIDs. + if base_sids: + logon_info.groups.rids = base_sids + else: + logon_info.groups.rids = None + logon_info.groups.count = len(base_sids) + + # Add Extra SIDs. + if extra_sids: + logon_info.sids = extra_sids + else: + logon_info.sids = None + logon_info.sid_count = len(extra_sids) + + # Add Resource SIDs. + if resource_sids: + logon_info.domain_groups = resource_sids + else: + logon_info.domain_groups = None + logon_info.domain_group_count = len(resource_sids) + + pac.buffers = pac_buffers + pac.num_buffers = len(pac_buffers) + + return pac + + def set_pac_claims(self, pac, *, client_claims=None, device_claims=None, claim_ids=None): + if claim_ids is None: + claim_ids = {} + + if client_claims is not None: + self.assertIsNone(device_claims, + 'don’t specify both client and device claims') + pac_claims = client_claims + pac_buffer_type = krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO + else: + self.assertIsNotNone(device_claims, + 'please specify client or device claims') + pac_claims = device_claims + pac_buffer_type = krb5pac.PAC_TYPE_DEVICE_CLAIMS_INFO + + claim_value_types = { + claims.CLAIM_TYPE_INT64: claims.CLAIM_INT64, + claims.CLAIM_TYPE_UINT64: claims.CLAIM_UINT64, + claims.CLAIM_TYPE_STRING: claims.CLAIM_STRING, + claims.CLAIM_TYPE_BOOLEAN: claims.CLAIM_UINT64, + } + + claims_arrays = [] + + for pac_claim_array in pac_claims: + pac_claim_source_type, pac_claim_entries = ( + pac_claim_array) + + claim_entries = [] + + for pac_claim_entry in pac_claim_entries: + pac_claim_id, pac_claim_type, pac_claim_values = ( + pac_claim_entry) + + claim_values_type = claim_value_types.get( + pac_claim_type, claims.CLAIM_STRING) + + claim_values_enum = claim_values_type() + claim_values_enum.values = pac_claim_values + claim_values_enum.value_count = len( + pac_claim_values) + + claim_entry = claims.CLAIM_ENTRY() + try: + claim_entry.id = pac_claim_id.format_map( + claim_ids) + except KeyError as err: + raise RuntimeError( + f'unknown claim name(s) ' + f'in ‘{pac_claim_id}’' + ) from err + claim_entry.type = pac_claim_type + claim_entry.values = claim_values_enum + + claim_entries.append(claim_entry) + + claims_array = claims.CLAIMS_ARRAY() + claims_array.claims_source_type = pac_claim_source_type + claims_array.claim_entries = claim_entries + claims_array.claims_count = len(claim_entries) + + claims_arrays.append(claims_array) + + claims_set = claims.CLAIMS_SET() + claims_set.claims_arrays = claims_arrays + claims_set.claims_array_count = len(claims_arrays) + + claims_ctr = claims.CLAIMS_SET_CTR() + claims_ctr.claims = claims_set + + claims_ndr = claims.CLAIMS_SET_NDR() + claims_ndr.claims = claims_ctr + + metadata = claims.CLAIMS_SET_METADATA() + metadata.claims_set = claims_ndr + metadata.compression_format = ( + claims.CLAIMS_COMPRESSION_FORMAT_XPRESS_HUFF) + + metadata_ctr = claims.CLAIMS_SET_METADATA_CTR() + metadata_ctr.metadata = metadata + + metadata_ndr = claims.CLAIMS_SET_METADATA_NDR() + metadata_ndr.claims = metadata_ctr + + pac_buffers = pac.buffers + for pac_buffer in pac_buffers: + if pac_buffer.type == pac_buffer_type: + break + else: + pac_buffer = krb5pac.PAC_BUFFER() + pac_buffer.type = pac_buffer_type + pac_buffer.info = krb5pac.DATA_BLOB_REM() + + pac_buffers.append(pac_buffer) + + pac_buffer.info.remaining = ndr_pack(metadata_ndr) + + pac.buffers = pac_buffers + pac.num_buffers = len(pac_buffers) + + return pac + + def add_extra_pac_buffers(self, pac, *, buffers=None): + if buffers is None: + buffers = [] + + pac_buffers = pac.buffers + for pac_buffer_type in buffers: + info = krb5pac.DATA_BLOB_REM() + # Having an empty PAC buffer will trigger an assertion failure in + # the MIT KDC’s k5_pac_locate_buffer(), so we need at least one + # byte. + info.remaining = b'0' + + pac_buffer = krb5pac.PAC_BUFFER() + pac_buffer.type = pac_buffer_type + pac_buffer.info = info + + pac_buffers.append(pac_buffer) + + pac.buffers = pac_buffers + pac.num_buffers = len(pac_buffers) + + return pac + + def get_cached_creds(self, *, + account_type, + opts=None, + use_cache=True): + if opts is None: + opts = {} + + opts_default = { + 'name_prefix': None, + 'name_suffix': None, + 'add_dollar': None, + 'upn': None, + 'spn': None, + 'additional_details': None, + 'allowed_replication': False, + 'allowed_replication_mock': False, + 'denied_replication': False, + 'denied_replication_mock': False, + 'revealed_to_rodc': False, + 'revealed_to_mock_rodc': False, + 'no_auth_data_required': False, + 'expired_password': False, + 'supported_enctypes': None, + 'not_delegated': False, + 'delegation_to_spn': None, + 'delegation_from_dn': None, + 'trusted_to_auth_for_delegation': False, + 'fast_support': False, + 'claims_support': False, + 'compound_id_support': False, + 'sid_compression_support': True, + 'member_of': None, + 'kerberos_enabled': True, + 'secure_channel_type': None, + 'id': None, + 'force_nt4_hash': False, + 'assigned_policy': None, + 'assigned_silo': None, + 'logon_hours': None, + } + + account_opts = { + 'account_type': account_type, + **opts_default, + **opts + } + + if use_cache: + cache_key = tuple(sorted(account_opts.items())) + creds = self.account_cache.get(cache_key) + if creds is not None: + return creds + + creds = self.create_account_opts(use_cache, **account_opts) + if use_cache: + self.account_cache[cache_key] = creds + + return creds + + def create_account_opts(self, use_cache, *, + account_type, + name_prefix, + name_suffix, + add_dollar, + upn, + spn, + additional_details, + allowed_replication, + allowed_replication_mock, + denied_replication, + denied_replication_mock, + revealed_to_rodc, + revealed_to_mock_rodc, + no_auth_data_required, + expired_password, + supported_enctypes, + not_delegated, + delegation_to_spn, + delegation_from_dn, + trusted_to_auth_for_delegation, + fast_support, + claims_support, + compound_id_support, + sid_compression_support, + member_of, + kerberos_enabled, + secure_channel_type, + id, + force_nt4_hash, + assigned_policy, + assigned_silo, + logon_hours): + if account_type is self.AccountType.USER: + self.assertIsNone(delegation_to_spn) + self.assertIsNone(delegation_from_dn) + self.assertFalse(trusted_to_auth_for_delegation) + else: + self.assertFalse(not_delegated) + + samdb = self.get_samdb() + + user_name = self.get_new_username() + if name_prefix is not None: + user_name = name_prefix + user_name + if name_suffix is not None: + user_name += name_suffix + + user_account_control = 0 + if trusted_to_auth_for_delegation: + user_account_control |= UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION + if not_delegated: + user_account_control |= UF_NOT_DELEGATED + if no_auth_data_required: + user_account_control |= UF_NO_AUTH_DATA_REQUIRED + + if additional_details: + details = {k: v for k, v in additional_details} + else: + details = {} + + enctypes = supported_enctypes + if fast_support: + enctypes = enctypes or 0 + enctypes |= security.KERB_ENCTYPE_FAST_SUPPORTED + if claims_support: + enctypes = enctypes or 0 + enctypes |= security.KERB_ENCTYPE_CLAIMS_SUPPORTED + if compound_id_support: + enctypes = enctypes or 0 + enctypes |= security.KERB_ENCTYPE_COMPOUND_IDENTITY_SUPPORTED + if sid_compression_support is False: + enctypes = enctypes or 0 + enctypes |= security.KERB_ENCTYPE_RESOURCE_SID_COMPRESSION_DISABLED + + if enctypes is not None: + details['msDS-SupportedEncryptionTypes'] = str(enctypes) + + if delegation_to_spn: + details['msDS-AllowedToDelegateTo'] = delegation_to_spn + + if delegation_from_dn: + if isinstance(delegation_from_dn, str): + delegation_from_dn = self.get_security_descriptor( + delegation_from_dn) + details['msDS-AllowedToActOnBehalfOfOtherIdentity'] = ( + delegation_from_dn) + + if spn is None and account_type is not self.AccountType.USER: + spn = 'host/' + user_name + + if assigned_policy is not None: + details['msDS-AssignedAuthNPolicy'] = assigned_policy + + if assigned_silo is not None: + details['msDS-AssignedAuthNPolicySilo'] = assigned_silo + + if logon_hours is not None: + details['logonHours'] = logon_hours + + creds, dn = self.create_account(samdb, user_name, + account_type=account_type, + upn=upn, + spn=spn, + additional_details=details, + account_control=user_account_control, + add_dollar=add_dollar, + force_nt4_hash=force_nt4_hash, + expired_password=expired_password, + preserve=use_cache) + + expected_etypes = None + if force_nt4_hash: + expected_etypes = {kcrypto.Enctype.RC4} + keys = self.get_keys(creds, expected_etypes=expected_etypes) + self.creds_set_keys(creds, keys) + + # Handle secret replication to the RODC. + + if allowed_replication or revealed_to_rodc: + rodc_samdb = self.get_rodc_samdb() + rodc_dn = self.get_server_dn(rodc_samdb) + + # Allow replicating this account's secrets if requested, or allow + # it only temporarily if we're about to replicate them. + allowed_cleanup = self.add_to_group( + dn, rodc_dn, + 'msDS-RevealOnDemandGroup') + + if revealed_to_rodc: + # Replicate this account's secrets to the RODC. + self.replicate_account_to_rodc(dn) + + if not allowed_replication: + # If we don't want replicating secrets to be allowed for this + # account, disable it again. + samdb.modify(allowed_cleanup) + + self.check_revealed(dn, + rodc_dn, + revealed=revealed_to_rodc) + + if denied_replication: + rodc_samdb = self.get_rodc_samdb() + rodc_dn = self.get_server_dn(rodc_samdb) + + # Deny replicating this account's secrets to the RODC. + self.add_to_group(dn, rodc_dn, 'msDS-NeverRevealGroup') + + # Handle secret replication to the mock RODC. + + if allowed_replication_mock or revealed_to_mock_rodc: + # Allow replicating this account's secrets if requested, or allow + # it only temporarily if we want to add the account to the mock + # RODC's msDS-RevealedUsers. + rodc_ctx = self.get_mock_rodc_ctx() + mock_rodc_dn = ldb.Dn(samdb, rodc_ctx.acct_dn) + + allowed_mock_cleanup = self.add_to_group( + dn, mock_rodc_dn, + 'msDS-RevealOnDemandGroup') + + if revealed_to_mock_rodc: + # Request replicating this account's secrets to the mock RODC, + # which updates msDS-RevealedUsers. + self.reveal_account_to_mock_rodc(dn) + + if not allowed_replication_mock: + # If we don't want replicating secrets to be allowed for this + # account, disable it again. + samdb.modify(allowed_mock_cleanup) + + self.check_revealed(dn, + mock_rodc_dn, + revealed=revealed_to_mock_rodc) + + if denied_replication_mock: + # Deny replicating this account's secrets to the mock RODC. + rodc_ctx = self.get_mock_rodc_ctx() + mock_rodc_dn = ldb.Dn(samdb, rodc_ctx.acct_dn) + + self.add_to_group(dn, mock_rodc_dn, 'msDS-NeverRevealGroup') + + if member_of is not None: + for group_dn in member_of: + self.add_to_group(dn, ldb.Dn(samdb, group_dn), 'member', + expect_attr=False) + + if kerberos_enabled: + creds.set_kerberos_state(MUST_USE_KERBEROS) + else: + creds.set_kerberos_state(DONT_USE_KERBEROS) + + if secure_channel_type is not None: + creds.set_secure_channel_type(secure_channel_type) + + return creds + + def get_new_username(self): + user_name = self.account_base + str(self.account_id) + type(self).account_id += 1 + + return user_name + + def get_client_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + def create_client_account(): + return self.get_cached_creds(account_type=self.AccountType.USER) + + c = self._get_krb5_creds(prefix='CLIENT', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys, + fallback_creds_fn=create_client_account) + return c + + def get_mach_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + def create_mach_account(): + return self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'fast_support': True, + 'claims_support': True, + 'compound_id_support': True, + 'supported_enctypes': ( + security.KERB_ENCTYPE_RC4_HMAC_MD5 | + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK + ), + }) + + c = self._get_krb5_creds(prefix='MAC', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys, + fallback_creds_fn=create_mach_account) + return c + + def get_service_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + def create_service_account(): + return self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'trusted_to_auth_for_delegation': True, + 'fast_support': True, + 'claims_support': True, + 'compound_id_support': True, + 'supported_enctypes': ( + security.KERB_ENCTYPE_RC4_HMAC_MD5 | + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK + ), + }) + + c = self._get_krb5_creds(prefix='SERVICE', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys, + fallback_creds_fn=create_service_account) + return c + + def get_rodc_krbtgt_creds(self, + require_keys=True, + require_strongest_key=False): + if require_strongest_key: + self.assertTrue(require_keys) + + def download_rodc_krbtgt_creds(): + samdb = self.get_samdb() + rodc_samdb = self.get_rodc_samdb() + + rodc_dn = self.get_server_dn(rodc_samdb) + + res = samdb.search(rodc_dn, + scope=ldb.SCOPE_BASE, + attrs=['msDS-KrbTgtLink']) + krbtgt_dn = res[0]['msDS-KrbTgtLink'][0] + + res = samdb.search(krbtgt_dn, + scope=ldb.SCOPE_BASE, + attrs=['sAMAccountName', + 'msDS-KeyVersionNumber', + 'msDS-SecondaryKrbTgtNumber']) + krbtgt_dn = res[0].dn + username = str(res[0]['sAMAccountName']) + + creds = KerberosCredentials() + creds.set_domain(self.env_get_var('DOMAIN', 'RODC_KRBTGT')) + creds.set_realm(self.env_get_var('REALM', 'RODC_KRBTGT')) + creds.set_username(username) + + kvno = int(res[0]['msDS-KeyVersionNumber'][0]) + krbtgt_number = int(res[0]['msDS-SecondaryKrbTgtNumber'][0]) + + rodc_kvno = krbtgt_number << 16 | kvno + creds.set_kvno(rodc_kvno) + creds.set_dn(krbtgt_dn) + + keys = self.get_keys(creds) + self.creds_set_keys(creds, keys) + + # The RODC krbtgt account should support the default enctypes, + # although it might not have the msDS-SupportedEncryptionTypes + # attribute. + self.creds_set_default_enctypes( + creds, + fast_support=self.kdc_fast_support, + claims_support=self.kdc_claims_support, + compound_id_support=self.kdc_compound_id_support) + + return creds + + c = self._get_krb5_creds(prefix='RODC_KRBTGT', + allow_missing_password=True, + allow_missing_keys=not require_keys, + require_strongest_key=require_strongest_key, + fallback_creds_fn=download_rodc_krbtgt_creds) + return c + + def get_mock_rodc_krbtgt_creds(self, + require_keys=True, + require_strongest_key=False): + if require_strongest_key: + self.assertTrue(require_keys) + + def create_rodc_krbtgt_account(): + samdb = self.get_samdb() + + rodc_ctx = self.get_mock_rodc_ctx() + + krbtgt_dn = rodc_ctx.new_krbtgt_dn + + res = samdb.search(base=ldb.Dn(samdb, krbtgt_dn), + scope=ldb.SCOPE_BASE, + attrs=['msDS-KeyVersionNumber', + 'msDS-SecondaryKrbTgtNumber']) + dn = res[0].dn + username = str(rodc_ctx.krbtgt_name) + + creds = KerberosCredentials() + creds.set_domain(self.env_get_var('DOMAIN', 'RODC_KRBTGT')) + creds.set_realm(self.env_get_var('REALM', 'RODC_KRBTGT')) + creds.set_username(username) + + kvno = int(res[0]['msDS-KeyVersionNumber'][0]) + krbtgt_number = int(res[0]['msDS-SecondaryKrbTgtNumber'][0]) + + rodc_kvno = krbtgt_number << 16 | kvno + creds.set_kvno(rodc_kvno) + creds.set_dn(dn) + + keys = self.get_keys(creds) + self.creds_set_keys(creds, keys) + + if self.get_domain_functional_level() >= DS_DOMAIN_FUNCTION_2008: + extra_bits = (security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96 | + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96) + else: + extra_bits = 0 + remove_bits = (security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK | + security.KERB_ENCTYPE_RC4_HMAC_MD5) + self.creds_set_enctypes(creds, + extra_bits=extra_bits, + remove_bits=remove_bits) + + return creds + + c = self._get_krb5_creds(prefix='MOCK_RODC_KRBTGT', + allow_missing_password=True, + allow_missing_keys=not require_keys, + require_strongest_key=require_strongest_key, + fallback_creds_fn=create_rodc_krbtgt_account) + return c + + def get_krbtgt_creds(self, + require_keys=True, + require_strongest_key=False): + if require_strongest_key: + self.assertTrue(require_keys) + + def download_krbtgt_creds(): + samdb = self.get_samdb() + + krbtgt_rid = security.DOMAIN_RID_KRBTGT + krbtgt_sid = '%s-%d' % (samdb.get_domain_sid(), krbtgt_rid) + + res = samdb.search(base='<SID=%s>' % krbtgt_sid, + scope=ldb.SCOPE_BASE, + attrs=['sAMAccountName', + 'msDS-KeyVersionNumber']) + dn = res[0].dn + username = str(res[0]['sAMAccountName']) + + creds = KerberosCredentials() + creds.set_domain(self.env_get_var('DOMAIN', 'KRBTGT')) + creds.set_realm(self.env_get_var('REALM', 'KRBTGT')) + creds.set_username(username) + + kvno = int(res[0]['msDS-KeyVersionNumber'][0]) + creds.set_kvno(kvno) + creds.set_dn(dn) + + keys = self.get_keys(creds) + self.creds_set_keys(creds, keys) + + # The krbtgt account should support the default enctypes, although + # it might not (on Samba) have the msDS-SupportedEncryptionTypes + # attribute. + self.creds_set_default_enctypes( + creds, + fast_support=self.kdc_fast_support, + claims_support=self.kdc_claims_support, + compound_id_support=self.kdc_compound_id_support) + + return creds + + c = self._get_krb5_creds(prefix='KRBTGT', + default_username='krbtgt', + allow_missing_password=True, + allow_missing_keys=not require_keys, + require_strongest_key=require_strongest_key, + fallback_creds_fn=download_krbtgt_creds) + return c + + def get_dc_creds(self, + require_keys=True, + require_strongest_key=False): + if require_strongest_key: + self.assertTrue(require_keys) + + def download_dc_creds(): + samdb = self.get_samdb() + + dc_rid = 1000 + dc_sid = '%s-%d' % (samdb.get_domain_sid(), dc_rid) + + res = samdb.search(base='<SID=%s>' % dc_sid, + scope=ldb.SCOPE_BASE, + attrs=['sAMAccountName', + 'msDS-KeyVersionNumber']) + dn = res[0].dn + username = str(res[0]['sAMAccountName']) + + creds = KerberosCredentials() + creds.set_domain(self.env_get_var('DOMAIN', 'DC')) + creds.set_realm(self.env_get_var('REALM', 'DC')) + creds.set_username(username) + + kvno = int(res[0]['msDS-KeyVersionNumber'][0]) + creds.set_kvno(kvno) + creds.set_workstation(username[:-1]) + creds.set_dn(dn) + + keys = self.get_keys(creds) + self.creds_set_keys(creds, keys) + + if self.get_domain_functional_level() >= DS_DOMAIN_FUNCTION_2008: + extra_bits = (security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96 | + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96) + else: + extra_bits = 0 + remove_bits = security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK + self.creds_set_enctypes(creds, + extra_bits=extra_bits, + remove_bits=remove_bits) + + return creds + + c = self._get_krb5_creds(prefix='DC', + allow_missing_password=True, + allow_missing_keys=not require_keys, + require_strongest_key=require_strongest_key, + fallback_creds_fn=download_dc_creds) + return c + + def get_server_creds(self, + require_keys=True, + require_strongest_key=False): + if require_strongest_key: + self.assertTrue(require_keys) + + def download_server_creds(): + samdb = self.get_samdb() + + res = samdb.search(base=samdb.get_default_basedn(), + expression=(f'(|(sAMAccountName={self.host}*)' + f'(dNSHostName={self.host}))'), + scope=ldb.SCOPE_SUBTREE, + attrs=['sAMAccountName', + 'msDS-KeyVersionNumber']) + self.assertEqual(1, len(res)) + dn = res[0].dn + username = str(res[0]['sAMAccountName']) + + creds = KerberosCredentials() + creds.set_domain(self.env_get_var('DOMAIN', 'SERVER')) + creds.set_realm(self.env_get_var('REALM', 'SERVER')) + creds.set_username(username) + + kvno = int(res[0]['msDS-KeyVersionNumber'][0]) + creds.set_kvno(kvno) + creds.set_dn(dn) + + keys = self.get_keys(creds) + self.creds_set_keys(creds, keys) + + if self.get_domain_functional_level() >= DS_DOMAIN_FUNCTION_2008: + extra_bits = (security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96 | + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96) + else: + extra_bits = 0 + remove_bits = security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK + self.creds_set_enctypes(creds, + extra_bits=extra_bits, + remove_bits=remove_bits) + + return creds + + c = self._get_krb5_creds(prefix='SERVER', + allow_missing_password=True, + allow_missing_keys=not require_keys, + require_strongest_key=require_strongest_key, + fallback_creds_fn=download_server_creds) + return c + + # Get the credentials and server principal name of either the krbtgt, or a + # specially created account, with resource SID compression either supported + # or unsupported. + def get_target(self, + to_krbtgt, *, + compound_id=None, + compression=None, + extra_enctypes=0): + if to_krbtgt: + self.assertIsNone(compound_id, + "it's no good specifying compound id support " + "for the krbtgt") + self.assertIsNone(compression, + "it's no good specifying compression support " + "for the krbtgt") + self.assertFalse(extra_enctypes, + "it's no good specifying extra enctypes " + "for the krbtgt") + creds = self.get_krbtgt_creds() + sname = self.get_krbtgt_sname() + else: + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'supported_enctypes': + security.KERB_ENCTYPE_RC4_HMAC_MD5 | + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96 | + extra_enctypes, + 'compound_id_support': compound_id, + 'sid_compression_support': compression, + }) + target_name = creds.get_username() + + if target_name[-1] == '$': + target_name = target_name[:-1] + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=['host', target_name]) + + return creds, sname + + def as_req(self, cname, sname, realm, etypes, padata=None, kdc_options=0): + """Send a Kerberos AS_REQ, returns the undecoded response + """ + + till = self.get_KerberosTime(offset=36000) + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + return rep + + def get_as_rep_key(self, creds, rep): + """Extract the session key from an AS-REP + """ + rep_padata = self.der_decode( + rep['e-data'], + asn1Spec=krb5_asn1.METHOD_DATA()) + + for pa in rep_padata: + if pa['padata-type'] == PADATA_ETYPE_INFO2: + padata_value = pa['padata-value'] + break + else: + self.fail('expected to find ETYPE-INFO2') + + etype_info2 = self.der_decode( + padata_value, asn1Spec=krb5_asn1.ETYPE_INFO2()) + + key = self.PasswordKey_from_etype_info2(creds, etype_info2[0], + creds.get_kvno()) + return key + + def get_enc_timestamp_pa_data(self, creds, rep, skew=0): + """generate the pa_data data element for an AS-REQ + """ + + key = self.get_as_rep_key(creds, rep) + + return self.get_enc_timestamp_pa_data_from_key(key, skew=skew) + + def get_enc_timestamp_pa_data_from_key(self, key, skew=0): + (patime, pausec) = self.get_KerberosTimeWithUsec(offset=skew) + padata = self.PA_ENC_TS_ENC_create(patime, pausec) + padata = self.der_encode(padata, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + padata = self.EncryptedData_create(key, KU_PA_ENC_TIMESTAMP, padata) + padata = self.der_encode(padata, asn1Spec=krb5_asn1.EncryptedData()) + + padata = self.PA_DATA_create(PADATA_ENC_TIMESTAMP, padata) + + return padata + + def get_challenge_pa_data(self, client_challenge_key, skew=0): + patime, pausec = self.get_KerberosTimeWithUsec(offset=skew) + padata = self.PA_ENC_TS_ENC_create(patime, pausec) + padata = self.der_encode(padata, + asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + padata = self.EncryptedData_create(client_challenge_key, + KU_ENC_CHALLENGE_CLIENT, + padata) + padata = self.der_encode(padata, + asn1Spec=krb5_asn1.EncryptedData()) + + padata = self.PA_DATA_create(PADATA_ENCRYPTED_CHALLENGE, + padata) + + return padata + + def get_as_rep_enc_data(self, key, rep): + """ Decrypt and Decode the encrypted data in an AS-REP + """ + enc_part = key.decrypt(KU_AS_REP_ENC_PART, rep['enc-part']['cipher']) + # MIT KDC encodes both EncASRepPart and EncTGSRepPart with + # application tag 26 + try: + enc_part = self.der_decode( + enc_part, asn1Spec=krb5_asn1.EncASRepPart()) + except Exception: + enc_part = self.der_decode( + enc_part, asn1Spec=krb5_asn1.EncTGSRepPart()) + + return enc_part + + def check_pre_authentication(self, rep): + """ Check that the kdc response was pre-authentication required + """ + self.check_error_rep(rep, KDC_ERR_PREAUTH_REQUIRED) + + def check_as_reply(self, rep): + """ Check that the kdc response is an AS-REP and that the + values for: + msg-type + pvno + tkt-pvno + kvno + match the expected values + """ + self.check_reply(rep, msg_type=KRB_AS_REP) + + def check_tgs_reply(self, rep): + """ Check that the kdc response is an TGS-REP and that the + values for: + msg-type + pvno + tkt-pvno + kvno + match the expected values + """ + self.check_reply(rep, msg_type=KRB_TGS_REP) + + def check_reply(self, rep, msg_type): + + # Should have a reply, and it should an TGS-REP message. + self.assertIsNotNone(rep) + self.assertEqual(rep['msg-type'], msg_type, "rep = {%s}" % rep) + + # Protocol version number should be 5 + pvno = int(rep['pvno']) + self.assertEqual(5, pvno, "rep = {%s}" % rep) + + # The ticket version number should be 5 + tkt_vno = int(rep['ticket']['tkt-vno']) + self.assertEqual(5, tkt_vno, "rep = {%s}" % rep) + + # Check that the kvno is not an RODC kvno + # MIT kerberos does not provide the kvno, so we treat it as optional. + # This is tested in compatability_test.py + if 'kvno' in rep['enc-part']: + kvno = int(rep['enc-part']['kvno']) + # If the high order bits are set this is an RODC kvno. + self.assertEqual(0, kvno & 0xFFFF0000, "rep = {%s}" % rep) + + def check_error_rep(self, rep, expected): + """ Check that the reply is an error message, with the expected + error-code specified. + """ + self.assertIsNotNone(rep) + self.assertEqual(rep['msg-type'], KRB_ERROR, "rep = {%s}" % rep) + if isinstance(expected, collections.abc.Container): + self.assertIn(rep['error-code'], expected, "rep = {%s}" % rep) + else: + self.assertEqual(rep['error-code'], expected, "rep = {%s}" % rep) + + def tgs_req(self, cname, sname, realm, ticket, key, etypes, + expected_error_mode=0, padata=None, kdc_options=0, + to_rodc=False, creds=None, service_creds=None, expect_pac=True, + expect_edata=None, expected_flags=None, unexpected_flags=None): + """Send a TGS-REQ, returns the response and the decrypted and + decoded enc-part + """ + + subkey = self.RandomKey(key.etype) + + (ctime, cusec) = self.get_KerberosTimeWithUsec() + + tgt = KerberosTicketCreds(ticket, + key, + crealm=realm, + cname=cname) + + if service_creds is not None: + decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + expected_supported_etypes = service_creds.tgs_supported_enctypes + else: + decryption_key = None + expected_supported_etypes = None + + if not expected_error_mode: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + else: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + + def generate_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + + return padata, req_body + + kdc_exchange_dict = self.tgs_exchange_dict( + creds=creds, + expected_crealm=realm, + expected_cname=cname, + expected_srealm=realm, + expected_sname=sname, + expected_error_mode=expected_error_mode, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + expected_supported_etypes=expected_supported_etypes, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + ticket_decryption_key=decryption_key, + generate_padata_fn=generate_padata if padata is not None else None, + tgt=tgt, + authenticator_subkey=subkey, + kdc_options=str(kdc_options), + expect_edata=expect_edata, + expect_pac=expect_pac, + to_rodc=to_rodc) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=realm, + sname=sname, + etypes=etypes) + + if expected_error_mode: + enc_part = None + else: + ticket_creds = kdc_exchange_dict['rep_ticket_creds'] + enc_part = ticket_creds.encpart_private + + return rep, enc_part + + def get_service_ticket(self, tgt, target_creds, service='host', + sname=None, + target_name=None, till=None, rc4_support=True, + to_rodc=False, kdc_options=None, + expected_flags=None, unexpected_flags=None, + expected_groups=None, + unexpected_groups=None, + expect_client_claims=None, + expect_device_claims=None, + expected_client_claims=None, + unexpected_client_claims=None, + expected_device_claims=None, + unexpected_device_claims=None, + pac_request=True, expect_pac=True, + expect_requester_sid=None, + expect_pac_attrs=None, + expect_pac_attrs_pac_request=None, + fresh=False): + user_name = tgt.cname['name-string'][0] + ticket_sname = tgt.sname + if target_name is None: + target_name = target_creds.get_username()[:-1] + else: + self.assertIsNone(sname, 'supplied both target name and sname') + cache_key = (user_name, target_name, service, to_rodc, kdc_options, + pac_request, str(expected_flags), str(unexpected_flags), + till, rc4_support, + str(ticket_sname), + str(sname), + str(expected_groups), + str(unexpected_groups), + expect_client_claims, expect_device_claims, + str(expected_client_claims), + str(unexpected_client_claims), + str(expected_device_claims), + str(unexpected_device_claims), + expect_pac, + expect_requester_sid, + expect_pac_attrs, + expect_pac_attrs_pac_request) + + if not fresh: + ticket = self.tkt_cache.get(cache_key) + + if ticket is not None: + return ticket + + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + if kdc_options is None: + kdc_options = '0' + kdc_options = str(krb5_asn1.KDCOptions(kdc_options)) + + if sname is None: + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[service, target_name]) + + srealm = target_creds.get_realm() + + authenticator_subkey = self.RandomKey(kcrypto.Enctype.AES256) + + decryption_key = self.TicketDecryptionKey_from_creds(target_creds) + + kdc_exchange_dict = self.tgs_exchange_dict( + expected_crealm=tgt.crealm, + expected_cname=tgt.cname, + expected_srealm=srealm, + expected_sname=sname, + expected_supported_etypes=target_creds.tgs_supported_enctypes, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + expected_groups=expected_groups, + unexpected_groups=unexpected_groups, + expect_client_claims=expect_client_claims, + expect_device_claims=expect_device_claims, + expected_client_claims=expected_client_claims, + unexpected_client_claims=unexpected_client_claims, + expected_device_claims=expected_device_claims, + unexpected_device_claims=unexpected_device_claims, + ticket_decryption_key=decryption_key, + check_rep_fn=self.generic_check_kdc_rep, + check_kdc_private_fn=self.generic_check_kdc_private, + tgt=tgt, + authenticator_subkey=authenticator_subkey, + kdc_options=kdc_options, + pac_request=pac_request, + expect_pac=expect_pac, + expect_requester_sid=expect_requester_sid, + expect_pac_attrs=expect_pac_attrs, + expect_pac_attrs_pac_request=expect_pac_attrs_pac_request, + rc4_support=rc4_support, + to_rodc=to_rodc) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=srealm, + sname=sname, + till_time=till, + etypes=etype) + self.check_tgs_reply(rep) + + service_ticket_creds = kdc_exchange_dict['rep_ticket_creds'] + + if to_rodc: + krbtgt_creds = self.get_rodc_krbtgt_creds() + else: + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + is_tgs_princ = self.is_tgs_principal(sname) + expect_ticket_checksum = (self.tkt_sig_support + and not is_tgs_princ) + expect_full_checksum = (self.full_sig_support + and not is_tgs_princ) + self.verify_ticket(service_ticket_creds, krbtgt_key, + service_ticket=True, expect_pac=expect_pac, + expect_ticket_checksum=expect_ticket_checksum, + expect_full_checksum=expect_full_checksum) + + self.tkt_cache[cache_key] = service_ticket_creds + + return service_ticket_creds + + def get_tgt(self, creds, to_rodc=False, kdc_options=None, + client_account=None, client_name_type=NT_PRINCIPAL, + target_creds=None, ticket_etype=None, + expected_flags=None, unexpected_flags=None, + expected_account_name=None, expected_upn_name=None, + expected_cname=None, + expected_sid=None, + sname=None, realm=None, + expected_groups=None, + unexpected_groups=None, + pac_request=True, expect_pac=True, + expect_pac_attrs=None, expect_pac_attrs_pac_request=None, + pac_options=None, + expect_requester_sid=None, + rc4_support=True, + expect_edata=None, + expect_client_claims=None, expect_device_claims=None, + expected_client_claims=None, unexpected_client_claims=None, + expected_device_claims=None, unexpected_device_claims=None, + fresh=False): + if client_account is not None: + user_name = client_account + else: + user_name = creds.get_username() + + cache_key = (user_name, to_rodc, kdc_options, pac_request, pac_options, + client_name_type, + ticket_etype, + str(expected_flags), str(unexpected_flags), + expected_account_name, expected_upn_name, expected_sid, + str(sname), str(realm), + str(expected_groups), + str(unexpected_groups), + str(expected_cname), + rc4_support, + expect_pac, expect_pac_attrs, + expect_pac_attrs_pac_request, expect_requester_sid, + expect_client_claims, expect_device_claims, + str(expected_client_claims), + str(unexpected_client_claims), + str(expected_device_claims), + str(unexpected_device_claims)) + + if not fresh: + tgt = self.tkt_cache.get(cache_key) + + if tgt is not None: + return tgt + + if realm is None: + realm = creds.get_realm() + + salt = creds.get_salt() + + etype = self.get_default_enctypes(creds) + cname = self.PrincipalName_create(name_type=client_name_type, + names=user_name.split('/')) + if sname is None: + sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=['krbtgt', realm]) + expected_sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=['krbtgt', realm.upper()]) + else: + expected_sname = sname + + if expected_cname is None: + expected_cname = cname + + till = self.get_KerberosTime(offset=36000) + + if target_creds is not None: + krbtgt_creds = target_creds + elif to_rodc: + krbtgt_creds = self.get_rodc_krbtgt_creds() + else: + krbtgt_creds = self.get_krbtgt_creds() + ticket_decryption_key = ( + self.TicketDecryptionKey_from_creds(krbtgt_creds, + etype=ticket_etype)) + + expected_etypes = krbtgt_creds.tgs_supported_enctypes + + if kdc_options is None: + kdc_options = ('forwardable,' + 'renewable,' + 'canonicalize,' + 'renewable-ok') + kdc_options = krb5_asn1.KDCOptions(kdc_options) + + if pac_options is None: + pac_options = '1' # supports claims + + rep, kdc_exchange_dict = self._test_as_exchange( + creds=creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + expected_error_mode=KDC_ERR_PREAUTH_REQUIRED, + expected_crealm=realm, + expected_cname=expected_cname, + expected_srealm=realm, + expected_sname=sname, + expected_account_name=expected_account_name, + expected_upn_name=expected_upn_name, + expected_sid=expected_sid, + expected_groups=expected_groups, + unexpected_groups=unexpected_groups, + expected_salt=salt, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + 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, + expect_pac=expect_pac, + expect_pac_attrs=expect_pac_attrs, + expect_pac_attrs_pac_request=expect_pac_attrs_pac_request, + expect_requester_sid=expect_requester_sid, + rc4_support=rc4_support, + expect_client_claims=expect_client_claims, + expect_device_claims=expect_device_claims, + expected_client_claims=expected_client_claims, + unexpected_client_claims=unexpected_client_claims, + expected_device_claims=expected_device_claims, + unexpected_device_claims=unexpected_device_claims, + to_rodc=to_rodc) + self.check_pre_authentication(rep) + + etype_info2 = kdc_exchange_dict['preauth_etype_info2'] + + preauth_key = self.PasswordKey_from_etype_info2(creds, + etype_info2[0], + creds.get_kvno()) + + ts_enc_padata = self.get_enc_timestamp_pa_data_from_key(preauth_key) + + padata = [ts_enc_padata] + + expected_realm = realm.upper() + + rep, kdc_exchange_dict = self._test_as_exchange( + creds=creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + expected_error_mode=0, + expected_crealm=expected_realm, + expected_cname=expected_cname, + expected_srealm=expected_realm, + expected_sname=expected_sname, + expected_account_name=expected_account_name, + expected_upn_name=expected_upn_name, + expected_sid=expected_sid, + expected_groups=expected_groups, + unexpected_groups=unexpected_groups, + expected_salt=salt, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + 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, + expect_pac=expect_pac, + expect_pac_attrs=expect_pac_attrs, + expect_pac_attrs_pac_request=expect_pac_attrs_pac_request, + expect_requester_sid=expect_requester_sid, + rc4_support=rc4_support, + expect_client_claims=expect_client_claims, + expect_device_claims=expect_device_claims, + expected_client_claims=expected_client_claims, + unexpected_client_claims=unexpected_client_claims, + expected_device_claims=expected_device_claims, + unexpected_device_claims=unexpected_device_claims, + to_rodc=to_rodc) + self.check_as_reply(rep) + + ticket_creds = kdc_exchange_dict['rep_ticket_creds'] + + self.tkt_cache[cache_key] = ticket_creds + + return ticket_creds + + def _make_tgs_request(self, client_creds, service_creds, tgt, + client_account=None, + client_name_type=NT_PRINCIPAL, + kdc_options=None, + pac_request=None, expect_pac=True, + expect_error=False, + expected_cname=None, + expected_account_name=None, + expected_upn_name=None, + expected_sid=None): + if client_account is None: + client_account = client_creds.get_username() + cname = self.PrincipalName_create(name_type=client_name_type, + names=client_account.split('/')) + + service_account = service_creds.get_username() + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[service_account]) + + realm = service_creds.get_realm() + + expected_crealm = realm + if expected_cname is None: + expected_cname = cname + expected_srealm = realm + expected_sname = sname + + expected_supported_etypes = service_creds.tgs_supported_enctypes + + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + if kdc_options is None: + kdc_options = 'canonicalize' + kdc_options = str(krb5_asn1.KDCOptions(kdc_options)) + + target_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + + authenticator_subkey = self.RandomKey(kcrypto.Enctype.AES256) + + if expect_error: + expected_error_mode = expect_error + if expected_error_mode is True: + expected_error_mode = KDC_ERR_TGT_REVOKED + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + expected_error_mode = 0 + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + kdc_exchange_dict = self.tgs_exchange_dict( + expected_crealm=expected_crealm, + expected_cname=expected_cname, + expected_srealm=expected_srealm, + expected_sname=expected_sname, + expected_account_name=expected_account_name, + expected_upn_name=expected_upn_name, + expected_sid=expected_sid, + expected_supported_etypes=expected_supported_etypes, + ticket_decryption_key=target_decryption_key, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error_mode, + tgt=tgt, + authenticator_subkey=authenticator_subkey, + kdc_options=kdc_options, + pac_request=pac_request, + expect_pac=expect_pac, + expect_edata=False) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=cname, + realm=realm, + sname=sname, + etypes=etypes) + if expect_error: + self.check_error_rep(rep, expected_error_mode) + + return None + else: + self.check_reply(rep, KRB_TGS_REP) + + return kdc_exchange_dict['rep_ticket_creds'] + + # Named tuple to contain values of interest when the PAC is decoded. + PacData = namedtuple( + "PacData", + "account_name account_sid logon_name upn domain_name") + + def get_pac_data(self, authorization_data): + """Decode the PAC element contained in the authorization-data element + """ + account_name = None + user_sid = None + logon_name = None + upn = None + domain_name = None + + # The PAC data will be wrapped in an AD_IF_RELEVANT element + ad_if_relevant_elements = ( + x for x in authorization_data if x['ad-type'] == AD_IF_RELEVANT) + for dt in ad_if_relevant_elements: + buf = self.der_decode( + dt['ad-data'], asn1Spec=krb5_asn1.AD_IF_RELEVANT()) + # The PAC data is further wrapped in a AD_WIN2K_PAC element + for ad in (x for x in buf if x['ad-type'] == AD_WIN2K_PAC): + pb = ndr_unpack(krb5pac.PAC_DATA, ad['ad-data']) + for pac in pb.buffers: + if pac.type == krb5pac.PAC_TYPE_LOGON_INFO: + account_name = ( + pac.info.info.info3.base.account_name) + user_sid = ( + str(pac.info.info.info3.base.domain_sid) + + "-" + str(pac.info.info.info3.base.rid)) + elif pac.type == krb5pac.PAC_TYPE_LOGON_NAME: + logon_name = pac.info.account_name + elif pac.type == krb5pac.PAC_TYPE_UPN_DNS_INFO: + upn = pac.info.upn_name + domain_name = pac.info.dns_domain_name + + return self.PacData( + account_name, + user_sid, + logon_name, + upn, + domain_name) + + def decode_service_ticket(self, creds, ticket): + """Decrypt and decode a service ticket + """ + + enc_part = ticket['enc-part'] + + key = self.TicketDecryptionKey_from_creds(creds, + enc_part['etype']) + + if key.kvno is not None: + self.assertElementKVNO(enc_part, 'kvno', key.kvno) + + enc_part = key.decrypt(KU_TICKET, enc_part['cipher']) + enc_ticket_part = self.der_decode( + enc_part, asn1Spec=krb5_asn1.EncTicketPart()) + return enc_ticket_part + + def modify_ticket_flag(self, enc_part, flag, value): + self.assertIsInstance(value, bool) + + flag = krb5_asn1.TicketFlags(flag) + pos = len(tuple(flag)) - 1 + + flags = enc_part['flags'] + self.assertLessEqual(pos, len(flags)) + + new_flags = flags[:pos] + str(int(value)) + flags[pos + 1:] + enc_part['flags'] = new_flags + + return enc_part + + def get_objectSid(self, samdb, dn): + """ Get the objectSID for a DN + Note: performs an Ldb query. + """ + res = samdb.search(dn, scope=SCOPE_BASE, attrs=["objectSID"]) + self.assertTrue(len(res) == 1, "did not get objectSid for %s" % dn) + sid = samdb.schema_format_value("objectSID", res[0]["objectSID"][0]) + return sid.decode('utf8') + + def add_attribute(self, samdb, dn_str, name, value): + if isinstance(value, list): + values = value + else: + values = [value] + flag = ldb.FLAG_MOD_ADD + + dn = ldb.Dn(samdb, dn_str) + msg = ldb.Message(dn) + msg[name] = ldb.MessageElement(values, flag, name) + samdb.modify(msg) + + def modify_attribute(self, samdb, dn_str, name, value): + if isinstance(value, list): + values = value + else: + values = [value] + flag = ldb.FLAG_MOD_REPLACE + + dn = ldb.Dn(samdb, dn_str) + msg = ldb.Message(dn) + msg[name] = ldb.MessageElement(values, flag, name) + samdb.modify(msg) + + def remove_attribute(self, samdb, dn_str, name): + flag = ldb.FLAG_MOD_DELETE + + dn = ldb.Dn(samdb, dn_str) + msg = ldb.Message(dn) + msg[name] = ldb.MessageElement([], flag, name) + samdb.modify(msg) + + def create_ccache(self, cname, ticket, enc_part): + """ Lay out a version 4 on-disk credentials cache, to be read using the + FILE: protocol. + """ + + field = krb5ccache.DELTATIME_TAG() + field.kdc_sec_offset = 0 + field.kdc_usec_offset = 0 + + v4tag = krb5ccache.V4TAG() + v4tag.tag = 1 + v4tag.field = field + + v4tags = krb5ccache.V4TAGS() + v4tags.tag = v4tag + v4tags.further_tags = b'' + + optional_header = krb5ccache.V4HEADER() + optional_header.v4tags = v4tags + + cname_string = cname['name-string'] + + cprincipal = krb5ccache.PRINCIPAL() + cprincipal.name_type = cname['name-type'] + cprincipal.component_count = len(cname_string) + cprincipal.realm = ticket['realm'] + cprincipal.components = cname_string + + sname = ticket['sname'] + sname_string = sname['name-string'] + + sprincipal = krb5ccache.PRINCIPAL() + sprincipal.name_type = sname['name-type'] + sprincipal.component_count = len(sname_string) + sprincipal.realm = ticket['realm'] + sprincipal.components = sname_string + + key = self.EncryptionKey_import(enc_part['key']) + + key_data = key.export_obj() + keyblock = krb5ccache.KEYBLOCK() + keyblock.enctype = key_data['keytype'] + keyblock.data = key_data['keyvalue'] + + addresses = krb5ccache.ADDRESSES() + addresses.count = 0 + addresses.data = [] + + authdata = krb5ccache.AUTHDATA() + authdata.count = 0 + authdata.data = [] + + # Re-encode the ticket, since it was decoded by another layer. + ticket_data = self.der_encode(ticket, asn1Spec=krb5_asn1.Ticket()) + + authtime = enc_part['authtime'] + starttime = enc_part.get('starttime', authtime) + endtime = enc_part['endtime'] + + cred = krb5ccache.CREDENTIAL() + cred.client = cprincipal + cred.server = sprincipal + cred.keyblock = keyblock + cred.authtime = self.get_EpochFromKerberosTime(authtime) + cred.starttime = self.get_EpochFromKerberosTime(starttime) + cred.endtime = self.get_EpochFromKerberosTime(endtime) + + # Account for clock skew of up to five minutes. + self.assertLess(cred.authtime - 5 * 60, + datetime.now(timezone.utc).timestamp(), + "Ticket not yet valid - clocks may be out of sync.") + self.assertLess(cred.starttime - 5 * 60, + datetime.now(timezone.utc).timestamp(), + "Ticket not yet valid - clocks may be out of sync.") + self.assertGreater(cred.endtime - 60 * 60, + datetime.now(timezone.utc).timestamp(), + "Ticket already expired/about to expire - " + "clocks may be out of sync.") + + cred.renew_till = cred.endtime + cred.is_skey = 0 + cred.ticket_flags = int(enc_part['flags'], 2) + cred.addresses = addresses + cred.authdata = authdata + cred.ticket = ticket_data + cred.second_ticket = b'' + + ccache = krb5ccache.CCACHE() + ccache.pvno = 5 + ccache.version = 4 + ccache.optional_header = optional_header + ccache.principal = cprincipal + ccache.cred = cred + + # Serialise the credentials cache structure. + result = ndr_pack(ccache) + + # Create a temporary file and write the credentials. + cachefile = tempfile.NamedTemporaryFile(dir=self.tempdir, delete=False) + cachefile.write(result) + cachefile.close() + + return cachefile + + def create_ccache_with_ticket(self, user_credentials, ticket, pac=True): + # Place the ticket into a newly created credentials cache file. + + user_name = user_credentials.get_username() + realm = user_credentials.get_realm() + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[user_name]) + + if not pac: + ticket = self.modified_ticket(ticket, exclude_pac=True) + + # Write the ticket into a credentials cache file that can be ingested + # by the main credentials code. + cachefile = self.create_ccache(cname, ticket.ticket, + ticket.encpart_private) + + # Create a credentials object to reference the credentials cache. + creds = Credentials() + creds.set_kerberos_state(MUST_USE_KERBEROS) + creds.set_username(user_name, SPECIFIED) + creds.set_realm(realm) + creds.set_named_ccache(cachefile.name, SPECIFIED, self.get_lp()) + + # Return the credentials along with the cache file. + return (creds, cachefile) + + def create_ccache_with_user(self, user_credentials, mach_credentials, + service="host", target_name=None, pac=True): + # Obtain a service ticket authorising the user and place it into a + # newly created credentials cache file. + + tgt = self.get_tgt(user_credentials) + + ticket = self.get_service_ticket(tgt, mach_credentials, + service=service, + target_name=target_name) + + return self.create_ccache_with_ticket(user_credentials, ticket, + pac=pac) + + # Test credentials by connecting to the DC through LDAP. + def _connect(self, creds, simple_bind, expect_error=None): + samdb = self.get_samdb() + dn = creds.get_dn() + + if simple_bind: + url = f'ldaps://{samdb.host_dns_name()}' + creds.set_bind_dn(str(dn)) + else: + url = f'ldap://{samdb.host_dns_name()}' + creds.set_bind_dn(None) + try: + ldap = SamDB(url=url, + credentials=creds, + lp=self.get_lp()) + except ldb.LdbError as err: + self.assertIsNotNone(expect_error, 'got unexpected error') + num, estr = err.args + if num != ldb.ERR_INVALID_CREDENTIALS: + raise + + self.assertIn(expect_error, estr) + + return + else: + self.assertIsNone(expect_error, 'expected to get an error') + + res = ldap.search('', + scope=ldb.SCOPE_BASE, + attrs=['tokenGroups']) + self.assertEqual(1, len(res)) + + sid = creds.get_sid() + + token_groups = res[0].get('tokenGroups', idx=0) + token_sid = ndr_unpack(security.dom_sid, token_groups) + + self.assertEqual(sid, str(token_sid)) + + # Test the two SAMR password change methods implemented in Samba. If the + # user is protected, we should get an ACCOUNT_RESTRICTION error indicating + # that the password change is not allowed. + def _test_samr_change_password(self, creds, expect_error, + connect_error=None): + samdb = self.get_samdb() + server_name = samdb.host_dns_name() + try: + conn = samr.samr(f'ncacn_np:{server_name}[seal,smb2]', + self.get_lp(), + creds) + except NTSTATUSError as err: + self.assertIsNotNone(connect_error, + 'connection unexpectedly failed') + self.assertIsNone(expect_error, 'don’t specify both errors') + + num, _ = err.args + self.assertEqual(num, connect_error) + + return + else: + self.assertIsNone(connect_error, 'expected connection to fail') + + # Get the NT hash. + nt_hash = creds.get_nt_hash() + + # Generate a new UTF-16 password. + new_password_str = generate_random_password(32, 32) + new_password = new_password_str.encode('utf-16le') + + # Generate the MD4 hash of the password. + new_password_md4 = md4_hash_blob(new_password) + + # Prefix the password with padding so it is 512 bytes long. + new_password_len = len(new_password) + remaining_len = 512 - new_password_len + new_password = bytes(remaining_len) + new_password + + # Append the 32-bit length of the password. + new_password += int.to_bytes(new_password_len, + length=4, + byteorder='little') + + # Create a key from the MD4 hash of the new password. + key = new_password_md4[:14] + + # Encrypt the old NT hash with DES to obtain the verifier. + verifier = des_crypt_blob_16(nt_hash, key) + + server = lsa.String() + server.string = server_name + + account = lsa.String() + account.string = creds.get_username() + + nt_verifier = samr.Password() + nt_verifier.hash = list(verifier) + + nt_password = samr.CryptPassword() + nt_password.data = list(arcfour_encrypt(nt_hash, new_password)) + + if not self.expect_nt_hash: + expect_error = ntstatus.NT_STATUS_NTLM_BLOCKED + + try: + conn.ChangePasswordUser2(server=server, + account=account, + nt_password=nt_password, + nt_verifier=nt_verifier, + lm_change=False, + lm_password=None, + lm_verifier=None) + except NTSTATUSError as err: + num, _ = err.args + self.assertIsNotNone(expect_error, + f'unexpectedly failed with {num:08X}') + self.assertEqual(num, expect_error) + else: + self.assertIsNone(expect_error, 'expected to fail') + + creds.set_password(new_password_str) + + # Get the NT hash. + nt_hash = creds.get_nt_hash() + + # Generate a new UTF-16 password. + new_password = generate_random_password(32, 32) + new_password = new_password.encode('utf-16le') + + # Generate the MD4 hash of the password. + new_password_md4 = md4_hash_blob(new_password) + + # Prefix the password with padding so it is 512 bytes long. + new_password_len = len(new_password) + remaining_len = 512 - new_password_len + new_password = bytes(remaining_len) + new_password + + # Append the 32-bit length of the password. + new_password += int.to_bytes(new_password_len, + length=4, + byteorder='little') + + # Create a key from the MD4 hash of the new password. + key = new_password_md4[:14] + + # Encrypt the old NT hash with DES to obtain the verifier. + verifier = des_crypt_blob_16(nt_hash, key) + + nt_verifier.hash = list(verifier) + + nt_password.data = list(arcfour_encrypt(nt_hash, new_password)) + + try: + conn.ChangePasswordUser3(server=server, + account=account, + nt_password=nt_password, + nt_verifier=nt_verifier, + lm_change=False, + lm_password=None, + lm_verifier=None, + password3=None) + except NTSTATUSError as err: + self.assertIsNotNone(expect_error, 'unexpectedly failed') + + num, _ = err.args + self.assertEqual(num, expect_error) + else: + self.assertIsNone(expect_error, 'expected to fail') + + # Test SamLogon. Authentication should succeed for non-protected accounts, + # and fail for protected accounts. + def _test_samlogon(self, creds, logon_type, expect_error=None, + validation_level=netlogon.NetlogonValidationSamInfo2, + domain_joined_mach_creds=None): + samdb = self.get_samdb() + + if domain_joined_mach_creds is None: + domain_joined_mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'secure_channel_type': misc.SEC_CHAN_WKSTA}) + + dc_server = samdb.host_dns_name() + username, domain = creds.get_ntlm_username_domain() + workstation = domain_joined_mach_creds.get_username() + + # Calling this initializes netlogon_creds on mach_creds, as is required + # before calling mach_creds.encrypt_samr_password(). + conn = netlogon.netlogon(f'ncacn_ip_tcp:{dc_server}[schannel,seal]', + self.get_lp(), + domain_joined_mach_creds) + + if logon_type == netlogon.NetlogonInteractiveInformation: + logon = netlogon.netr_PasswordInfo() + + lm_pass = samr.Password() + lm_pass.hash = [0] * 16 + + nt_pass = samr.Password() + nt_pass.hash = list(creds.get_nt_hash()) + domain_joined_mach_creds.encrypt_samr_password(nt_pass) + + logon.lmpassword = lm_pass + logon.ntpassword = nt_pass + + elif logon_type == netlogon.NetlogonNetworkInformation: + computername = ntlmssp.AV_PAIR() + computername.AvId = ntlmssp.MsvAvNbComputerName + computername.Value = workstation + + domainname = ntlmssp.AV_PAIR() + domainname.AvId = ntlmssp.MsvAvNbDomainName + domainname.Value = domain + + eol = ntlmssp.AV_PAIR() + eol.AvId = ntlmssp.MsvAvEOL + + target_info = ntlmssp.AV_PAIR_LIST() + target_info.count = 3 + target_info.pair = [domainname, computername, eol] + + target_info_blob = ndr_pack(target_info) + + challenge = b'abcdefgh' + response = creds.get_ntlm_response(flags=0, + challenge=challenge, + target_info=target_info_blob) + + logon = netlogon.netr_NetworkInfo() + + logon.challenge = list(challenge) + logon.nt = netlogon.netr_ChallengeResponse() + logon.nt.length = len(response['nt_response']) + logon.nt.data = list(response['nt_response']) + + else: + self.fail(f'unknown logon type {logon_type}') + + identity_info = netlogon.netr_IdentityInfo() + identity_info.domain_name.string = domain + identity_info.account_name.string = username + identity_info.parameter_control = ( + netlogon.MSV1_0_ALLOW_SERVER_TRUST_ACCOUNT) | ( + netlogon.MSV1_0_ALLOW_WORKSTATION_TRUST_ACCOUNT) + identity_info.workstation.string = workstation + + logon.identity_info = identity_info + + netr_flags = 0 + + validation = None + + if not expect_error and not self.expect_nt_hash: + expect_error = ntstatus.NT_STATUS_NTLM_BLOCKED + + try: + (validation, authoritative, flags) = ( + conn.netr_LogonSamLogonEx(dc_server, + domain_joined_mach_creds.get_workstation(), + logon_type, + logon, + validation_level, + netr_flags)) + except NTSTATUSError as err: + status, _ = err.args + self.assertIsNotNone(expect_error, + f'unexpectedly failed with {status:08X}') + self.assertEqual(expect_error, status, 'got wrong status code') + else: + self.assertIsNone(expect_error, 'expected error') + + self.assertEqual(1, authoritative) + self.assertEqual(0, flags) + + return validation diff --git a/python/samba/tests/krb5/kdc_tests.py b/python/samba/tests/krb5/kdc_tests.py new file mode 100755 index 0000000..b4be6f8 --- /dev/null +++ b/python/samba/tests/krb5/kdc_tests.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2020 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.tests.krb5.raw_testcase import RawKerberosTest +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_PREAUTH_FAILED, + KDC_ERR_PREAUTH_REQUIRED, + KDC_ERR_SKEW, + KRB_AS_REP, + KRB_ERROR, + KU_PA_ENC_TIMESTAMP, + PADATA_ENC_TIMESTAMP, + PADATA_ETYPE_INFO2, + NT_PRINCIPAL, + NT_SRV_INST, +) + +global_asn1_print = False +global_hexdump = False + + +class KdcTests(RawKerberosTest): + """ Port of the tests in source4/torture/krb5/kdc-heimdal.c + To python. + """ + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def as_req(self, creds, etypes, padata=None): + user = creds.get_username() + realm = creds.get_realm() + + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, + names=["krbtgt", realm]) + till = self.get_KerberosTime(offset=36000) + + kdc_options = 0 + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + return rep + + def get_enc_timestamp_pa_data(self, creds, rep, skew=0): + rep_padata = self.der_decode( + rep['e-data'], + asn1Spec=krb5_asn1.METHOD_DATA()) + + for pa in rep_padata: + if pa['padata-type'] == PADATA_ETYPE_INFO2: + etype_info2 = pa['padata-value'] + break + + etype_info2 = self.der_decode( + etype_info2, asn1Spec=krb5_asn1.ETYPE_INFO2()) + + key = self.PasswordKey_from_etype_info2(creds, etype_info2[0]) + + (patime, pausec) = self.get_KerberosTimeWithUsec(offset=skew) + pa_ts = self.PA_ENC_TS_ENC_create(patime, pausec) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + pa_ts = self.EncryptedData_create(key, KU_PA_ENC_TIMESTAMP, pa_ts) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.EncryptedData()) + + pa_ts = self.PA_DATA_create(PADATA_ENC_TIMESTAMP, pa_ts) + + return pa_ts + + def check_pre_authenication(self, rep): + """ Check that the kdc response was pre-authentication required + """ + self.check_error_rep(rep, KDC_ERR_PREAUTH_REQUIRED) + + def check_as_reply(self, rep): + """ Check that the kdc response is an AS-REP and that the + values for: + msg-type + pvno + tkt-pvno + kvno + match the expected values + """ + + # Should have a reply, and it should an AS-REP message. + self.assertIsNotNone(rep) + self.assertEqual(rep['msg-type'], KRB_AS_REP) + + # Protocol version number should be 5 + pvno = int(rep['pvno']) + self.assertEqual(5, pvno) + + # The ticket version number should be 5 + tkt_vno = int(rep['ticket']['tkt-vno']) + self.assertEqual(5, tkt_vno) + + # Check that the kvno is not an RODC kvno + # MIT kerberos does not provide the kvno, so we treat it as optional. + # This is tested in compatability_test.py + if 'kvno' in rep['enc-part']: + kvno = int(rep['enc-part']['kvno']) + # If the high order bits are set this is an RODC kvno. + self.assertEqual(0, kvno & 0xFFFF0000) + + def check_error_rep(self, rep, expected): + """ Check that the reply is an error message, with the expected + error-code specified. + """ + self.assertIsNotNone(rep) + self.assertEqual(rep['msg-type'], KRB_ERROR) + self.assertEqual(rep['error-code'], expected) + + def test_aes256_cts_hmac_sha1_96(self): + creds = self.get_user_creds() + etype = (AES256_CTS_HMAC_SHA1_96,) + + rep = self.as_req(creds, etype) + self.check_pre_authenication(rep) + + padata = self.get_enc_timestamp_pa_data(creds, rep) + rep = self.as_req(creds, etype, padata=[padata]) + self.check_as_reply(rep) + + etype = rep['enc-part']['etype'] + self.assertEqual(AES256_CTS_HMAC_SHA1_96, etype) + + def test_arc4_hmac_md5(self): + creds = self.get_user_creds() + etype = (ARCFOUR_HMAC_MD5,) + + rep = self.as_req(creds, etype) + self.check_pre_authenication(rep) + + padata = self.get_enc_timestamp_pa_data(creds, rep) + rep = self.as_req(creds, etype, padata=[padata]) + self.check_as_reply(rep) + + etype = rep['enc-part']['etype'] + self.assertEqual(ARCFOUR_HMAC_MD5, etype) + + def test_aes_rc4(self): + creds = self.get_user_creds() + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + rep = self.as_req(creds, etype) + self.check_pre_authenication(rep) + + padata = self.get_enc_timestamp_pa_data(creds, rep) + rep = self.as_req(creds, etype, padata=[padata]) + self.check_as_reply(rep) + + etype = rep['enc-part']['etype'] + self.assertEqual(AES256_CTS_HMAC_SHA1_96, etype) + + def test_clock_skew(self): + creds = self.get_user_creds() + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + rep = self.as_req(creds, etype) + self.check_pre_authenication(rep) + + padata = self.get_enc_timestamp_pa_data(creds, rep, skew=3600) + rep = self.as_req(creds, etype, padata=[padata]) + + self.check_error_rep(rep, KDC_ERR_SKEW) + + def test_invalid_password(self): + creds = self.insta_creds(template=self.get_user_creds()) + creds.set_password("Not the correct password") + + etype = (AES256_CTS_HMAC_SHA1_96,) + + rep = self.as_req(creds, etype) + self.check_pre_authenication(rep) + + padata = self.get_enc_timestamp_pa_data(creds, rep) + rep = self.as_req(creds, etype, padata=[padata]) + + self.check_error_rep(rep, KDC_ERR_PREAUTH_FAILED) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/kdc_tgs_tests.py b/python/samba/tests/krb5/kdc_tgs_tests.py new file mode 100755 index 0000000..58ed49d --- /dev/null +++ b/python/samba/tests/krb5/kdc_tgs_tests.py @@ -0,0 +1,3506 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2020 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from functools import partial + +import ldb + +from samba import dsdb, ntstatus + +from samba.dcerpc import krb5pac, security + + +import samba.tests.krb5.kcrypto as kcrypto +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.raw_testcase import Krb5EncryptionKey +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + FX_FAST_ARMOR_AP_REQUEST, + KRB_ERROR, + KDC_ERR_BADKEYVER, + KDC_ERR_BADMATCH, + KDC_ERR_ETYPE_NOSUPP, + KDC_ERR_GENERIC, + KDC_ERR_MODIFIED, + KDC_ERR_NOT_US, + KDC_ERR_POLICY, + KDC_ERR_PREAUTH_REQUIRED, + KDC_ERR_C_PRINCIPAL_UNKNOWN, + KDC_ERR_S_PRINCIPAL_UNKNOWN, + KDC_ERR_SERVER_NOMATCH, + KDC_ERR_TKT_EXPIRED, + KDC_ERR_TGT_REVOKED, + KRB_ERR_TKT_NYV, + KDC_ERR_WRONG_REALM, + NT_ENTERPRISE_PRINCIPAL, + NT_PRINCIPAL, + NT_SRV_INST, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +global_asn1_print = False +global_hexdump = False + + +class KdcTgsBaseTests(KDCBaseTest): + def _as_req(self, + creds, + expected_error, + target_creds, + etype, + expected_ticket_etype=None): + user_name = creds.get_username() + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=user_name.split('/')) + + target_name = target_creds.get_username() + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', target_name[:-1]]) + + if expected_error: + expected_sname = sname + else: + expected_sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[target_name]) + + realm = creds.get_realm() + salt = creds.get_salt() + + till = self.get_KerberosTime(offset=36000) + + ticket_decryption_key = ( + self.TicketDecryptionKey_from_creds(target_creds, + etype=expected_ticket_etype)) + expected_etypes = target_creds.tgs_supported_enctypes + + kdc_options = ('forwardable,' + 'renewable,' + 'canonicalize,' + 'renewable-ok') + kdc_options = krb5_asn1.KDCOptions(kdc_options) + + if expected_error: + initial_error = (KDC_ERR_PREAUTH_REQUIRED, expected_error) + else: + initial_error = KDC_ERR_PREAUTH_REQUIRED + + rep, kdc_exchange_dict = self._test_as_exchange( + creds=creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + expected_error_mode=initial_error, + 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) + self.assertIsNotNone(rep) + self.assertEqual(KRB_ERROR, rep['msg-type']) + error_code = rep['error-code'] + if expected_error: + self.assertIn(error_code, initial_error) + if error_code == expected_error: + return + else: + self.assertEqual(initial_error, error_code) + + etype_info2 = kdc_exchange_dict['preauth_etype_info2'] + + preauth_key = self.PasswordKey_from_etype_info2(creds, + etype_info2[0], + creds.get_kvno()) + + ts_enc_padata = self.get_enc_timestamp_pa_data_from_key(preauth_key) + + padata = [ts_enc_padata] + + expected_realm = realm.upper() + + rep, kdc_exchange_dict = self._test_as_exchange( + creds=creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + expected_error_mode=expected_error, + expected_crealm=expected_realm, + expected_cname=cname, + expected_srealm=expected_realm, + expected_sname=expected_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, + expect_edata=False) + if expected_error: + self.check_error_rep(rep, expected_error) + return None + + self.check_as_reply(rep) + return kdc_exchange_dict['rep_ticket_creds'] + + def _armored_as_req(self, + client_creds, + target_creds, + armor_tgt, + *, + target_sname=None, + expected_error=0, + expected_sname=None, + expect_edata=None, + expect_status=None, + expected_status=None, + expected_groups=None, + expect_device_info=None, + expected_device_groups=None, + expect_device_claims=None, + expected_device_claims=None): + client_username = client_creds.get_username() + client_realm = client_creds.get_realm() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + if target_sname is None: + target_name = target_creds.get_username() + target_sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[target_name]) + target_realm = target_creds.get_realm() + target_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + target_etypes = target_creds.tgs_supported_enctypes + + authenticator_subkey = self.RandomKey(kcrypto.Enctype.AES256) + armor_key = self.generate_armor_key(authenticator_subkey, + armor_tgt.session_key) + + preauth_key = self.PasswordKey_from_creds(client_creds, + kcrypto.Enctype.AES256) + + client_challenge_key = ( + self.generate_client_challenge_key(armor_key, preauth_key)) + fast_padata = [self.get_challenge_pa_data(client_challenge_key)] + + def _generate_fast_padata(kdc_exchange_dict, + _callback_dict, + req_body): + return list(fast_padata), req_body + + etypes = kcrypto.Enctype.AES256, kcrypto.Enctype.RC4 + + if expected_error: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + pac_options = '1' # claims support + + samdb = self.get_samdb() + domain_sid_str = samdb.get_domain_sid() + + if expected_groups is not None: + expected_groups = self.map_sids(expected_groups, None, domain_sid_str) + + if expected_device_groups is not None: + expected_device_groups = self.map_sids(expected_device_groups, None, domain_sid_str) + + if expected_sname is None: + expected_sname = target_sname + + kdc_exchange_dict = self.as_exchange_dict( + creds=client_creds, + expected_crealm=client_realm, + expected_cname=client_cname, + expected_srealm=target_realm, + expected_sname=expected_sname, + expected_supported_etypes=target_etypes, + ticket_decryption_key=target_decryption_key, + generate_fast_fn=self.generate_simple_fast, + generate_fast_armor_fn=self.generate_ap_req, + generate_fast_padata_fn=_generate_fast_padata, + fast_armor_type=FX_FAST_ARMOR_AP_REQUEST, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error, + expected_salt=client_creds.get_salt(), + expect_edata=expect_edata, + expect_status=expect_status, + expected_status=expected_status, + expected_groups=expected_groups, + expect_device_info=expect_device_info, + expected_device_domain_sid=domain_sid_str, + expected_device_groups=expected_device_groups, + expect_device_claims=expect_device_claims, + expected_device_claims=expected_device_claims, + authenticator_subkey=authenticator_subkey, + preauth_key=preauth_key, + armor_key=armor_key, + armor_tgt=armor_tgt, + armor_subkey=authenticator_subkey, + kdc_options='0', + pac_options=pac_options, + # PA-DATA types are not important for these tests. + check_patypes=False) + + rep = self._generic_kdc_exchange( + kdc_exchange_dict, + cname=client_cname, + realm=client_realm, + sname=target_sname, + etypes=etypes) + if expected_error: + self.check_error_rep(rep, expected_error) + return None + else: + self.check_as_reply(rep) + return kdc_exchange_dict['rep_ticket_creds'] + + def _tgs_req(self, tgt, expected_error, creds, target_creds, *, + armor_tgt=None, + kdc_options='0', + pac_options=None, + expected_cname=None, + expected_sname=None, + expected_account_name=None, + expected_flags=None, + additional_ticket=None, + decryption_key=None, + generate_padata_fn=None, + generate_fast_padata_fn=None, + sname=None, + srealm=None, + till=None, + etypes=None, + expected_ticket_etype=None, + expected_supported_etypes=None, + expect_pac=True, + expect_pac_attrs=None, + expect_pac_attrs_pac_request=None, + expect_requester_sid=None, + expect_edata=False, + expected_sid=None, + expected_groups=None, + unexpected_groups=None, + expect_device_info=None, + expected_device_domain_sid=None, + expected_device_groups=None, + expect_client_claims=None, + expected_client_claims=None, + unexpected_client_claims=None, + expect_device_claims=None, + expected_device_claims=None, + expect_status=None, + expected_status=None, + expected_proxy_target=None, + expected_transited_services=None, + expected_extra_pac_buffers=None, + check_patypes=True): + if srealm is False: + srealm = None + elif srealm is None: + srealm = target_creds.get_realm() + + if sname is False: + sname = None + if expected_sname is None: + expected_sname = self.get_krbtgt_sname() + else: + if sname is None: + target_name = target_creds.get_username() + if target_name == 'krbtgt': + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, + names=[target_name, srealm]) + else: + if target_name[-1] == '$': + target_name = target_name[:-1] + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=['host', target_name]) + + if expected_sname is None: + expected_sname = sname + + if additional_ticket is not None: + additional_tickets = [additional_ticket.ticket] + if decryption_key is None: + decryption_key = additional_ticket.session_key + else: + additional_tickets = None + if decryption_key is None: + decryption_key = self.TicketDecryptionKey_from_creds( + target_creds, etype=expected_ticket_etype) + + subkey = self.RandomKey(tgt.session_key.etype) + + if armor_tgt is not None: + armor_subkey = self.RandomKey(subkey.etype) + explicit_armor_key = self.generate_armor_key(armor_subkey, + armor_tgt.session_key) + armor_key = kcrypto.cf2(explicit_armor_key.key, + subkey.key, + b'explicitarmor', + b'tgsarmor') + armor_key = Krb5EncryptionKey(armor_key, None) + + generate_fast_fn = self.generate_simple_fast + generate_fast_armor_fn = self.generate_ap_req + + if pac_options is None: + pac_options = '1' # claims support + else: + armor_subkey = None + armor_key = None + generate_fast_fn = None + generate_fast_armor_fn = None + + if etypes is None: + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + + if expected_error: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + if expected_cname is None: + expected_cname = tgt.cname + + kdc_exchange_dict = self.tgs_exchange_dict( + creds=creds, + expected_crealm=tgt.crealm, + expected_cname=expected_cname, + expected_srealm=srealm, + expected_sname=expected_sname, + expected_account_name=expected_account_name, + expected_flags=expected_flags, + ticket_decryption_key=decryption_key, + generate_padata_fn=generate_padata_fn, + generate_fast_padata_fn=generate_fast_padata_fn, + generate_fast_fn=generate_fast_fn, + generate_fast_armor_fn=generate_fast_armor_fn, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error, + expect_status=expect_status, + expected_status=expected_status, + tgt=tgt, + armor_key=armor_key, + armor_tgt=armor_tgt, + armor_subkey=armor_subkey, + pac_options=pac_options, + authenticator_subkey=subkey, + kdc_options=kdc_options, + expected_supported_etypes=expected_supported_etypes, + expect_edata=expect_edata, + expect_pac=expect_pac, + expect_pac_attrs=expect_pac_attrs, + expect_pac_attrs_pac_request=expect_pac_attrs_pac_request, + expect_requester_sid=expect_requester_sid, + expected_sid=expected_sid, + expected_groups=expected_groups, + unexpected_groups=unexpected_groups, + expect_device_info=expect_device_info, + expected_device_domain_sid=expected_device_domain_sid, + expected_device_groups=expected_device_groups, + expect_client_claims=expect_client_claims, + expected_client_claims=expected_client_claims, + unexpected_client_claims=unexpected_client_claims, + expect_device_claims=expect_device_claims, + expected_device_claims=expected_device_claims, + expected_proxy_target=expected_proxy_target, + expected_transited_services=expected_transited_services, + expected_extra_pac_buffers=expected_extra_pac_buffers, + check_patypes=check_patypes) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=srealm, + sname=sname, + till_time=till, + etypes=etypes, + additional_tickets=additional_tickets) + if expected_error: + self.check_error_rep(rep, expected_error) + return None + else: + self.check_tgs_reply(rep) + return kdc_exchange_dict['rep_ticket_creds'] + + +class KdcTgsTests(KdcTgsBaseTests): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def test_tgs_req_cname_does_not_not_match_authenticator_cname(self): + """ Try and obtain a ticket from the TGS, but supply a cname + that differs from that provided to the krbtgt + """ + # Create the user account + samdb = self.get_samdb() + user_name = "tsttktusr" + (uc, _) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96,) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[user_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a service ticket, but use a cname that does not match + # that in the original AS-REQ + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + ticket = rep['ticket'] + + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=["Administrator"]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=["host", samdb.host_dns_name()]) + + (rep, enc_part) = self.tgs_req(cname, sname, realm, ticket, key, etype, + creds=uc, + expected_error_mode=KDC_ERR_BADMATCH, + expect_edata=False) + + self.assertIsNone( + enc_part, + "rep = {%s}, enc_part = {%s}" % (rep, enc_part)) + self.assertEqual(KRB_ERROR, rep['msg-type'], "rep = {%s}" % rep) + self.assertEqual( + KDC_ERR_BADMATCH, + rep['error-code'], + "rep = {%s}" % rep) + + def test_ldap_service_ticket(self): + """Get a ticket to the ldap service + """ + # Create the user account + samdb = self.get_samdb() + user_name = "tsttktusr" + (uc, _) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96,) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[user_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + ticket = rep['ticket'] + + # Request a ticket to the ldap service + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, + names=["ldap", samdb.host_dns_name()]) + + (rep, _) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + service_creds=self.get_dc_creds()) + + self.check_tgs_reply(rep) + + def test_get_ticket_for_host_service_of_machine_account(self): + + # Create a user and machine account for the test. + # + samdb = self.get_samdb() + user_name = "tsttktusr" + (uc, dn) = self.create_account(samdb, user_name) + (mc, _) = self.create_account(samdb, "tsttktmac", + account_type=self.AccountType.COMPUTER) + realm = uc.get_realm().lower() + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[user_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the service ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + + pac_data = self.get_pac_data(enc_part['authorization-data']) + sid = uc.get_sid() + upn = "%s@%s" % (uc.get_username(), realm) + self.assertEqual( + uc.get_username(), + str(pac_data.account_name), + "rep = {%s},%s" % (rep, pac_data)) + self.assertEqual( + uc.get_username(), + pac_data.logon_name, + "rep = {%s},%s" % (rep, pac_data)) + self.assertEqual( + uc.get_realm(), + pac_data.domain_name, + "rep = {%s},%s" % (rep, pac_data)) + self.assertEqual( + upn, + pac_data.upn, + "rep = {%s},%s" % (rep, pac_data)) + self.assertEqual( + sid, + pac_data.account_sid, + "rep = {%s},%s" % (rep, pac_data)) + + def test_request(self): + client_creds = self.get_client_creds() + service_creds = self.get_service_creds() + + tgt = self.get_tgt(client_creds) + + pac = self.get_ticket_pac(tgt) + self.assertIsNotNone(pac) + + ticket = self._make_tgs_request(client_creds, service_creds, tgt) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_request_no_pac(self): + client_creds = self.get_client_creds() + service_creds = self.get_service_creds() + + tgt = self.get_tgt(client_creds, pac_request=False) + + pac = self.get_ticket_pac(tgt) + self.assertIsNotNone(pac) + + ticket = self._make_tgs_request(client_creds, service_creds, tgt, + pac_request=False, expect_pac=False) + + pac = self.get_ticket_pac(ticket, expect_pac=False) + self.assertIsNone(pac) + + def test_request_enterprise_canon(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': upn}) + service_creds = self.get_service_creds() + + user_name = client_creds.get_username() + realm = client_creds.get_realm() + client_account = f'{user_name}@{realm}' + + expected_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + + kdc_options = 'canonicalize' + + tgt = self.get_tgt(client_creds, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_cname=expected_cname, + expected_account_name=user_name, + kdc_options=kdc_options) + + self._make_tgs_request( + client_creds, service_creds, tgt, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_cname=expected_cname, + expected_account_name=user_name, + kdc_options=kdc_options) + + def test_request_enterprise_canon_case(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': upn}) + service_creds = self.get_service_creds() + + user_name = client_creds.get_username() + realm = client_creds.get_realm().lower() + client_account = f'{user_name}@{realm}' + + expected_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + + kdc_options = 'canonicalize' + + tgt = self.get_tgt(client_creds, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_cname=expected_cname, + expected_account_name=user_name, + kdc_options=kdc_options) + + self._make_tgs_request( + client_creds, service_creds, tgt, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_cname=expected_cname, + expected_account_name=user_name, + kdc_options=kdc_options) + + def test_request_enterprise_canon_mac(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + service_creds = self.get_service_creds() + + user_name = client_creds.get_username() + realm = client_creds.get_realm() + client_account = f'{user_name}@{realm}' + + expected_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + + kdc_options = 'canonicalize' + + tgt = self.get_tgt(client_creds, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_cname=expected_cname, + expected_account_name=user_name, + kdc_options=kdc_options) + + self._make_tgs_request( + client_creds, service_creds, tgt, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_cname=expected_cname, + expected_account_name=user_name, + kdc_options=kdc_options) + + def test_request_enterprise_canon_case_mac(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + service_creds = self.get_service_creds() + + user_name = client_creds.get_username() + realm = client_creds.get_realm().lower() + client_account = f'{user_name}@{realm}' + + expected_cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + + kdc_options = 'canonicalize' + + tgt = self.get_tgt(client_creds, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_cname=expected_cname, + expected_account_name=user_name, + kdc_options=kdc_options) + + self._make_tgs_request( + client_creds, service_creds, tgt, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_cname=expected_cname, + expected_account_name=user_name, + kdc_options=kdc_options) + + def test_request_enterprise_no_canon(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': upn}) + service_creds = self.get_service_creds() + + user_name = client_creds.get_username() + realm = client_creds.get_realm() + client_account = f'{user_name}@{realm}' + + kdc_options = '0' + + tgt = self.get_tgt(client_creds, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_account_name=user_name, + kdc_options=kdc_options) + + self._make_tgs_request( + client_creds, service_creds, tgt, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_account_name=user_name, + kdc_options=kdc_options) + + def test_request_enterprise_no_canon_case(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': upn}) + service_creds = self.get_service_creds() + + user_name = client_creds.get_username() + realm = client_creds.get_realm().lower() + client_account = f'{user_name}@{realm}' + + kdc_options = '0' + + tgt = self.get_tgt(client_creds, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_account_name=user_name, + kdc_options=kdc_options) + + self._make_tgs_request( + client_creds, service_creds, tgt, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_account_name=user_name, + kdc_options=kdc_options) + + def test_request_enterprise_no_canon_mac(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + service_creds = self.get_service_creds() + + user_name = client_creds.get_username() + realm = client_creds.get_realm() + client_account = f'{user_name}@{realm}' + + kdc_options = '0' + + tgt = self.get_tgt(client_creds, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_account_name=user_name, + kdc_options=kdc_options) + + self._make_tgs_request( + client_creds, service_creds, tgt, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_account_name=user_name, + kdc_options=kdc_options) + + def test_request_enterprise_no_canon_case_mac(self): + upn = self.get_new_username() + client_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + service_creds = self.get_service_creds() + + user_name = client_creds.get_username() + realm = client_creds.get_realm().lower() + client_account = f'{user_name}@{realm}' + + kdc_options = '0' + + tgt = self.get_tgt(client_creds, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_account_name=user_name, + kdc_options=kdc_options) + + self._make_tgs_request( + client_creds, service_creds, tgt, + client_account=client_account, + client_name_type=NT_ENTERPRISE_PRINCIPAL, + expected_account_name=user_name, + kdc_options=kdc_options) + + def test_client_no_auth_data_required(self): + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'no_auth_data_required': True}) + service_creds = self.get_service_creds() + + tgt = self.get_tgt(client_creds) + + pac = self.get_ticket_pac(tgt) + self.assertIsNotNone(pac) + + ticket = self._make_tgs_request(client_creds, service_creds, tgt) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_no_pac_client_no_auth_data_required(self): + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'no_auth_data_required': True}) + service_creds = self.get_service_creds() + + tgt = self.get_tgt(client_creds) + + pac = self.get_ticket_pac(tgt) + self.assertIsNotNone(pac) + + ticket = self._make_tgs_request(client_creds, service_creds, tgt, + pac_request=False, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_service_no_auth_data_required(self): + client_creds = self.get_client_creds() + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'no_auth_data_required': True}) + + tgt = self.get_tgt(client_creds) + + pac = self.get_ticket_pac(tgt) + self.assertIsNotNone(pac) + + ticket = self._make_tgs_request(client_creds, service_creds, tgt, + expect_pac=False) + + pac = self.get_ticket_pac(ticket, expect_pac=False) + self.assertIsNone(pac) + + def test_no_pac_service_no_auth_data_required(self): + client_creds = self.get_client_creds() + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'no_auth_data_required': True}) + + tgt = self.get_tgt(client_creds, pac_request=False) + + pac = self.get_ticket_pac(tgt) + self.assertIsNotNone(pac) + + ticket = self._make_tgs_request(client_creds, service_creds, tgt, + pac_request=False, expect_pac=False) + + pac = self.get_ticket_pac(ticket, expect_pac=False) + self.assertIsNone(pac) + + def test_remove_pac_service_no_auth_data_required(self): + client_creds = self.get_client_creds() + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'no_auth_data_required': True}) + + tgt = self.modified_ticket(self.get_tgt(client_creds), + exclude_pac=True) + + pac = self.get_ticket_pac(tgt, expect_pac=False) + self.assertIsNone(pac) + + self._make_tgs_request(client_creds, service_creds, tgt, + expect_error=True) + + def test_remove_pac_client_no_auth_data_required(self): + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'no_auth_data_required': True}) + service_creds = self.get_service_creds() + + tgt = self.modified_ticket(self.get_tgt(client_creds), + exclude_pac=True) + + pac = self.get_ticket_pac(tgt, expect_pac=False) + self.assertIsNone(pac) + + self._make_tgs_request(client_creds, service_creds, tgt, + expect_error=True) + + def test_remove_pac(self): + client_creds = self.get_client_creds() + service_creds = self.get_service_creds() + + tgt = self.modified_ticket(self.get_tgt(client_creds), + exclude_pac=True) + + pac = self.get_ticket_pac(tgt, expect_pac=False) + self.assertIsNone(pac) + + self._make_tgs_request(client_creds, service_creds, tgt, + expect_error=True) + + def test_upn_dns_info_ex_user(self): + client_creds = self.get_client_creds() + self._run_upn_dns_info_ex_test(client_creds) + + def test_upn_dns_info_ex_mac(self): + mach_creds = self.get_mach_creds() + self._run_upn_dns_info_ex_test(mach_creds) + + def test_upn_dns_info_ex_upn_user(self): + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={'upn': 'upn_dns_info_test_upn0@bar'}) + self._run_upn_dns_info_ex_test(client_creds) + + def test_upn_dns_info_ex_upn_mac(self): + mach_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'upn_dns_info_test_upn1@bar'}) + self._run_upn_dns_info_ex_test(mach_creds) + + def _run_upn_dns_info_ex_test(self, client_creds): + service_creds = self.get_service_creds() + + account_name = client_creds.get_username() + upn_name = client_creds.get_upn() + if upn_name is None: + realm = client_creds.get_realm().lower() + upn_name = f'{account_name}@{realm}' + sid = client_creds.get_sid() + + tgt = self.get_tgt(client_creds, + expected_account_name=account_name, + expected_upn_name=upn_name, + expected_sid=sid) + + self._make_tgs_request(client_creds, service_creds, tgt, + expected_account_name=account_name, + expected_upn_name=upn_name, + expected_sid=sid) + + # Test making a TGS request. + def test_tgs_req(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + self._run_tgs(tgt, creds, expected_error=0) + + def test_renew_req(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, renewable=True) + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + def test_validate_req(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True) + self._validate_tgt(tgt, creds, expected_error=0, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + def test_s4u2self_req(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + self._s4u2self(tgt, creds, expected_error=0) + + def test_user2user_req(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + self._user2user(tgt, creds, expected_error=0) + + def test_user2user_user_self_req(self): + creds = self._get_user_creds() + tgt = self._get_tgt(creds) + username = creds.get_username() + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[username]) + self._user2user(tgt, creds, sname=sname, user_tgt=tgt, user_creds=creds, expected_error=0) + + def test_user2user_computer_self_princ1_req(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + username = creds.get_username() + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[username]) + self._user2user(tgt, creds, sname=sname, user_tgt=tgt, user_creds=creds, expected_error=0) + + def test_user2user_computer_self_princ2_req(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + self._user2user(tgt, creds, user_tgt=tgt, user_creds=creds, expected_error=0) + + def test_fast_req(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + self._fast(tgt, creds, expected_error=0) + + def test_tgs_req_invalid(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True) + self._run_tgs(tgt, creds, expected_error=KRB_ERR_TKT_NYV) + + def test_s4u2self_req_invalid(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True) + self._s4u2self(tgt, creds, expected_error=KRB_ERR_TKT_NYV) + + def test_user2user_req_invalid(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True) + self._user2user(tgt, creds, expected_error=KRB_ERR_TKT_NYV) + + def test_fast_req_invalid(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True) + self._fast(tgt, creds, expected_error=KRB_ERR_TKT_NYV, + expected_sname=self.get_krbtgt_sname()) + + def test_tgs_req_no_requester_sid(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_requester_sid=True) + + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_tgs_req_no_pac_attrs(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac_attrs=True) + + self._run_tgs(tgt, creds, expected_error=0, expect_pac=True, + expect_pac_attrs=False) + + def test_tgs_req_from_rodc_no_requester_sid(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, remove_requester_sid=True) + + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_tgs_req_from_rodc_no_pac_attrs(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, remove_pac_attrs=True) + self._run_tgs(tgt, creds, expected_error=0, expect_pac=True, + expect_pac_attrs=False) + + def test_tgs_req_extra_pac_buffers(self): + extra_pac_buffers = [123, 456, 789] + + creds = self._get_creds() + tgt = self._get_tgt(creds, extra_pac_buffers=extra_pac_buffers) + + # Expect that the extra PAC buffers are retained in the TGT. + self._run_tgs(tgt, creds, expected_error=0, + expected_extra_pac_buffers=extra_pac_buffers) + + def test_tgs_req_from_rodc_extra_pac_buffers(self): + extra_pac_buffers = [123, 456, 789] + + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, + extra_pac_buffers=extra_pac_buffers) + + # Expect that the extra PAC buffers are removed from the RODC‐issued + # TGT. + self._run_tgs(tgt, creds, expected_error=0) + + # Test making a request without a PAC. + def test_tgs_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, renewable=True, remove_pac=True) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True, remove_pac=True) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True) + self._s4u2self(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED, + expect_edata=False) + + def test_user2user_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True) + self._user2user(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True) + self._fast(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + def test_fast_as_req_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True) + self._fast_as_req(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + # Test making a request with authdata and without a PAC. + def test_tgs_authdata_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True, allow_empty_authdata=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_authdata_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, renewable=True, remove_pac=True, + allow_empty_authdata=True) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_authdata_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True, remove_pac=True, + allow_empty_authdata=True) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_authdata_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True, allow_empty_authdata=True) + self._s4u2self(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED, + expect_edata=False) + + def test_user2user_authdata_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True, allow_empty_authdata=True) + self._user2user(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_authdata_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True, allow_empty_authdata=True) + self._fast(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + def test_fast_as_req_authdata_no_pac(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, remove_pac=True, allow_empty_authdata=True) + self._fast_as_req(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + # Test changing the SID in the PAC to that of another account. + def test_tgs_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, new_rid=existing_rid) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, renewable=True, new_rid=existing_rid) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, invalid=True, new_rid=existing_rid) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, new_rid=existing_rid) + self._s4u2self(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED) + + def test_user2user_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, new_rid=existing_rid) + self._user2user(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, new_rid=existing_rid) + self._fast(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + def test_fast_as_req_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, new_rid=existing_rid) + self._fast_as_req(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + def test_requester_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, new_rid=existing_rid, + can_modify_logon_info=False) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_logon_info_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, new_rid=existing_rid, + can_modify_requester_sid=False) + self._run_tgs(tgt, creds, expected_error=0) + + def test_logon_info_only_sid_mismatch_existing(self): + creds = self._get_creds() + existing_rid = self._get_existing_rid() + tgt = self._get_tgt(creds, new_rid=existing_rid, + remove_requester_sid=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + # Test changing the SID in the PAC to a non-existent one. + def test_tgs_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, new_rid=nonexistent_rid) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, renewable=True, + new_rid=nonexistent_rid) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, invalid=True, + new_rid=nonexistent_rid) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, new_rid=nonexistent_rid) + self._s4u2self(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED) + + def test_user2user_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, new_rid=nonexistent_rid) + self._user2user(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, new_rid=nonexistent_rid) + self._fast(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + def test_fast_as_req_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, new_rid=nonexistent_rid) + self._fast_as_req(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + def test_requester_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, new_rid=nonexistent_rid, + can_modify_logon_info=False) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_logon_info_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, new_rid=nonexistent_rid, + can_modify_requester_sid=False) + self._run_tgs(tgt, creds, expected_error=0) + + def test_logon_info_only_sid_mismatch_nonexisting(self): + creds = self._get_creds() + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, new_rid=nonexistent_rid, + remove_requester_sid=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + # Test with an RODC-issued ticket where the client is revealed to the RODC. + def test_tgs_rodc_revealed(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._run_tgs(tgt, creds, expected_error=0) + + def test_renew_rodc_revealed(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, renewable=True, from_rodc=True) + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_validate_rodc_revealed(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, invalid=True, from_rodc=True) + self._validate_tgt(tgt, creds, expected_error=0, + expect_pac_attrs=False, + expect_requester_sid=True) + + # This test fails on Windows, which gives KDC_ERR_C_PRINCIPAL_UNKNOWN when + # attempting to use S4U2Self with a TGT from an RODC. + def test_s4u2self_rodc_revealed(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._s4u2self(tgt, creds, + expected_error=KDC_ERR_C_PRINCIPAL_UNKNOWN) + + def test_user2user_rodc_revealed(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._user2user(tgt, creds, expected_error=0) + + # Test with an RODC-issued ticket where the SID in the PAC is changed to + # that of another account. + def test_tgs_rodc_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, new_rid=existing_rid) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_rodc_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, renewable=True, from_rodc=True, + new_rid=existing_rid) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_rodc_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, invalid=True, from_rodc=True, + new_rid=existing_rid) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_rodc_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, new_rid=existing_rid) + self._s4u2self(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_user2user_rodc_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, new_rid=existing_rid) + self._user2user(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_rodc_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, new_rid=existing_rid) + self._fast(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + def test_tgs_rodc_requester_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, new_rid=existing_rid, + can_modify_logon_info=False) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_tgs_rodc_logon_info_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, new_rid=existing_rid, + can_modify_requester_sid=False) + self._run_tgs(tgt, creds, expected_error=0) + + def test_tgs_rodc_logon_info_only_sid_mismatch_existing(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + existing_rid = self._get_existing_rid(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True, new_rid=existing_rid, + remove_requester_sid=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + # Test with an RODC-issued ticket where the SID in the PAC is changed to a + # non-existent one. + def test_tgs_rodc_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, from_rodc=True, new_rid=nonexistent_rid) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_rodc_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, renewable=True, from_rodc=True, + new_rid=nonexistent_rid) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_rodc_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, invalid=True, from_rodc=True, + new_rid=nonexistent_rid) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_rodc_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, from_rodc=True, new_rid=nonexistent_rid) + self._s4u2self(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_user2user_rodc_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, from_rodc=True, new_rid=nonexistent_rid) + self._user2user(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_rodc_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, from_rodc=True, new_rid=nonexistent_rid) + self._fast(tgt, creds, + expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + def test_tgs_rodc_requester_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, from_rodc=True, new_rid=nonexistent_rid, + can_modify_logon_info=False) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_tgs_rodc_logon_info_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, from_rodc=True, new_rid=nonexistent_rid, + can_modify_requester_sid=False) + self._run_tgs(tgt, creds, expected_error=0) + + def test_tgs_rodc_logon_info_only_sid_mismatch_nonexisting(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + nonexistent_rid = self._get_non_existent_rid() + tgt = self._get_tgt(creds, from_rodc=True, new_rid=nonexistent_rid, + remove_requester_sid=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + # Test with an RODC-issued ticket where the client is not revealed to the + # RODC. + def test_tgs_rodc_not_revealed(self): + creds = self._get_creds(replication_allowed=True) + tgt = self._get_tgt(creds, from_rodc=True) + # TODO: error code + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_rodc_not_revealed(self): + creds = self._get_creds(replication_allowed=True) + tgt = self._get_tgt(creds, renewable=True, from_rodc=True) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_rodc_not_revealed(self): + creds = self._get_creds(replication_allowed=True) + tgt = self._get_tgt(creds, invalid=True, from_rodc=True) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_rodc_not_revealed(self): + creds = self._get_creds(replication_allowed=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._s4u2self(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_user2user_rodc_not_revealed(self): + creds = self._get_creds(replication_allowed=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._user2user(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + # Test with an RODC-issued ticket where the RODC account does not have the + # PARTIAL_SECRETS bit set. + def test_tgs_rodc_no_partial_secrets(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._remove_rodc_partial_secrets() + self._run_tgs(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_renew_rodc_no_partial_secrets(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, renewable=True, from_rodc=True) + self._remove_rodc_partial_secrets() + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_validate_rodc_no_partial_secrets(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, invalid=True, from_rodc=True) + self._remove_rodc_partial_secrets() + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_s4u2self_rodc_no_partial_secrets(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._remove_rodc_partial_secrets() + self._s4u2self(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_user2user_rodc_no_partial_secrets(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._remove_rodc_partial_secrets() + self._user2user(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_fast_rodc_no_partial_secrets(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._remove_rodc_partial_secrets() + self._fast(tgt, creds, expected_error=KDC_ERR_POLICY, + expected_sname=self.get_krbtgt_sname()) + + # Test with an RODC-issued ticket where the RODC account does not have an + # msDS-KrbTgtLink. + def test_tgs_rodc_no_krbtgt_link(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._remove_rodc_krbtgt_link() + self._run_tgs(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_renew_rodc_no_krbtgt_link(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, renewable=True, from_rodc=True) + self._remove_rodc_krbtgt_link() + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_validate_rodc_no_krbtgt_link(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, invalid=True, from_rodc=True) + self._remove_rodc_krbtgt_link() + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_s4u2self_rodc_no_krbtgt_link(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._remove_rodc_krbtgt_link() + self._s4u2self(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_user2user_rodc_no_krbtgt_link(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._remove_rodc_krbtgt_link() + self._user2user(tgt, creds, expected_error=KDC_ERR_POLICY) + + def test_fast_rodc_no_krbtgt_link(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._remove_rodc_krbtgt_link() + self._fast(tgt, creds, expected_error=KDC_ERR_POLICY, + expected_sname=self.get_krbtgt_sname()) + + # Test with an RODC-issued ticket where the client is not allowed to + # replicate to the RODC. + def test_tgs_rodc_not_allowed(self): + creds = self._get_creds(revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_rodc_not_allowed(self): + creds = self._get_creds(revealed_to_rodc=True) + tgt = self._get_tgt(creds, renewable=True, from_rodc=True) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_rodc_not_allowed(self): + creds = self._get_creds(revealed_to_rodc=True) + tgt = self._get_tgt(creds, invalid=True, from_rodc=True) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_rodc_not_allowed(self): + creds = self._get_creds(revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._s4u2self(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_user2user_rodc_not_allowed(self): + creds = self._get_creds(revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._user2user(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_rodc_not_allowed(self): + creds = self._get_creds(revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._fast(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + # Test with an RODC-issued ticket where the client is denied from + # replicating to the RODC. + def test_tgs_rodc_denied(self): + creds = self._get_creds(replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_rodc_denied(self): + creds = self._get_creds(replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, renewable=True, from_rodc=True) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_rodc_denied(self): + creds = self._get_creds(replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, invalid=True, from_rodc=True) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_rodc_denied(self): + creds = self._get_creds(replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._s4u2self(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_user2user_rodc_denied(self): + creds = self._get_creds(replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._user2user(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_rodc_denied(self): + creds = self._get_creds(replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._fast(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + # Test with an RODC-issued ticket where the client is both allowed and + # denied replicating to the RODC. + def test_tgs_rodc_allowed_denied(self): + creds = self._get_creds(replication_allowed=True, + replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_renew_rodc_allowed_denied(self): + creds = self._get_creds(replication_allowed=True, + replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, renewable=True, from_rodc=True) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_validate_rodc_allowed_denied(self): + creds = self._get_creds(replication_allowed=True, + replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, invalid=True, from_rodc=True) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_s4u2self_rodc_allowed_denied(self): + creds = self._get_creds(replication_allowed=True, + replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._s4u2self(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_user2user_rodc_allowed_denied(self): + creds = self._get_creds(replication_allowed=True, + replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._user2user(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_fast_rodc_allowed_denied(self): + creds = self._get_creds(replication_allowed=True, + replication_denied=True, + revealed_to_rodc=True) + tgt = self._get_tgt(creds, from_rodc=True) + self._fast(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED, + expected_sname=self.get_krbtgt_sname()) + + # Test making a TGS request with an RC4-encrypted TGT. + def test_tgs_rc4(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, etype=kcrypto.Enctype.RC4) + self._run_tgs(tgt, creds, expected_error=(KDC_ERR_GENERIC, + KDC_ERR_BADKEYVER), + expect_edata=True, + # We aren’t particular about whether or not we get an + # NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_INSUFFICIENT_RESOURCES) + + def test_renew_rc4(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, renewable=True, etype=kcrypto.Enctype.RC4) + self._renew_tgt(tgt, creds, expected_error=(KDC_ERR_GENERIC, + KDC_ERR_BADKEYVER), + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + def test_validate_rc4(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True, etype=kcrypto.Enctype.RC4) + self._validate_tgt(tgt, creds, expected_error=(KDC_ERR_GENERIC, + KDC_ERR_BADKEYVER), + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + def test_s4u2self_rc4(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, etype=kcrypto.Enctype.RC4) + self._s4u2self(tgt, creds, expected_error=(KDC_ERR_GENERIC, + KDC_ERR_BADKEYVER), + expect_edata=True, + # We aren’t particular about whether or not we get an + # NTSTATUS. + expect_status=None, + expected_status=ntstatus.NT_STATUS_INSUFFICIENT_RESOURCES) + + def test_user2user_rc4(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, etype=kcrypto.Enctype.RC4) + self._user2user(tgt, creds, expected_error=KDC_ERR_ETYPE_NOSUPP) + + def test_fast_rc4(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, etype=kcrypto.Enctype.RC4) + self._fast(tgt, creds, expected_error=KDC_ERR_GENERIC, + expect_edata=self.expect_padata_outer) + + # Test with a TGT that has the lifetime of a kpasswd ticket (two minutes). + def test_tgs_kpasswd(self): + creds = self._get_creds() + tgt = self.modify_lifetime(self._get_tgt(creds), lifetime=2 * 60) + self._run_tgs(tgt, creds, expected_error=KDC_ERR_TKT_EXPIRED) + + def test_renew_kpasswd(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, renewable=True) + tgt = self.modify_lifetime(tgt, lifetime=2 * 60) + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TKT_EXPIRED) + + def test_validate_kpasswd(self): + creds = self._get_creds() + tgt = self._get_tgt(creds, invalid=True) + tgt = self.modify_lifetime(tgt, lifetime=2 * 60) + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TKT_EXPIRED) + + def test_s4u2self_kpasswd(self): + creds = self._get_creds() + tgt = self.modify_lifetime(self._get_tgt(creds), lifetime=2 * 60) + self._s4u2self(tgt, creds, expected_error=KDC_ERR_TKT_EXPIRED) + + def test_user2user_kpasswd(self): + creds = self._get_creds() + tgt = self.modify_lifetime(self._get_tgt(creds), lifetime=2 * 60) + self._user2user(tgt, creds, expected_error=KDC_ERR_TKT_EXPIRED) + + def test_fast_kpasswd(self): + creds = self._get_creds() + tgt = self.modify_lifetime(self._get_tgt(creds), lifetime=2 * 60) + self._fast(tgt, creds, expected_error=KDC_ERR_TKT_EXPIRED) + + # Test user-to-user with incorrect service principal names. + def test_user2user_matching_sname_host(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + user_name = creds.get_username() + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', user_name]) + + self._user2user(tgt, creds, sname=sname, + expected_error=KDC_ERR_S_PRINCIPAL_UNKNOWN) + + def test_user2user_matching_sname_no_host(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + user_name = creds.get_username() + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[user_name]) + + self._user2user(tgt, creds, sname=sname, expected_error=0) + + def test_user2user_wrong_sname(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + other_creds = self._get_mach_creds() + user_name = other_creds.get_username() + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[user_name]) + + self._user2user(tgt, creds, sname=sname, + expected_error=KDC_ERR_BADMATCH) + + def test_user2user_other_sname(self): + other_name = self.get_new_username() + spn = f'host/{other_name}' + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'spn': spn}) + tgt = self._get_tgt(creds) + + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', other_name]) + + self._user2user(tgt, creds, sname=sname, expected_error=0) + + def test_user2user_wrong_sname_krbtgt(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + sname = self.get_krbtgt_sname() + + self._user2user(tgt, creds, sname=sname, + expected_error=KDC_ERR_BADMATCH) + + def test_user2user_wrong_srealm(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + self._user2user(tgt, creds, srealm='OTHER.REALM', + expected_error=(KDC_ERR_WRONG_REALM, + KDC_ERR_S_PRINCIPAL_UNKNOWN)) + + def test_user2user_tgt_correct_realm(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + realm = creds.get_realm().encode('utf-8') + tgt = self._modify_tgt(tgt, crealm=realm) + + self._user2user(tgt, creds, + expected_error=0) + + def test_user2user_tgt_wrong_realm(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + tgt = self._modify_tgt(tgt, crealm=b'OTHER.REALM') + + self._user2user(tgt, creds, + expected_error=( + KDC_ERR_POLICY, # Windows + KDC_ERR_C_PRINCIPAL_UNKNOWN, # Heimdal + KDC_ERR_SERVER_NOMATCH, # MIT + ), + expect_edata=True, + expected_status=ntstatus.NT_STATUS_NO_MATCH) + + def test_user2user_tgt_correct_cname(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + user_name = creds.get_username() + user_name = user_name.encode('utf-8') + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[user_name]) + + tgt = self._modify_tgt(tgt, cname=cname) + + self._user2user(tgt, creds, expected_error=0) + + def test_user2user_tgt_other_cname(self): + samdb = self.get_samdb() + + other_name = self.get_new_username() + upn = f'{other_name}@{samdb.domain_dns_name()}' + + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': upn}) + tgt = self._get_tgt(creds) + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[other_name.encode('utf-8')]) + + tgt = self._modify_tgt(tgt, cname=cname) + + self._user2user(tgt, creds, expected_error=0) + + def test_user2user_tgt_cname_host(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + user_name = creds.get_username() + user_name = user_name.encode('utf-8') + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[b'host', user_name]) + + tgt = self._modify_tgt(tgt, cname=cname) + + self._user2user(tgt, creds, + expected_error=(KDC_ERR_TGT_REVOKED, + KDC_ERR_C_PRINCIPAL_UNKNOWN)) + + def test_user2user_non_existent_sname(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', 'non_existent_user']) + + self._user2user(tgt, creds, sname=sname, + expected_error=KDC_ERR_S_PRINCIPAL_UNKNOWN) + + def test_user2user_no_sname(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + self._user2user(tgt, creds, sname=False, + expected_error=(KDC_ERR_GENERIC, + KDC_ERR_S_PRINCIPAL_UNKNOWN)) + + def test_tgs_service_ticket(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + service_creds = self.get_service_creds() + service_ticket = self.get_service_ticket(tgt, service_creds) + + self._run_tgs(service_ticket, creds, + expected_error=(KDC_ERR_NOT_US, KDC_ERR_POLICY)) + + def test_renew_service_ticket(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + service_creds = self.get_service_creds() + service_ticket = self.get_service_ticket(tgt, service_creds) + + service_ticket = self.modified_ticket( + service_ticket, + modify_fn=self._modify_renewable, + checksum_keys=self.get_krbtgt_checksum_key()) + + self._renew_tgt(service_ticket, creds, + expected_error=KDC_ERR_POLICY) + + def test_validate_service_ticket(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + service_creds = self.get_service_creds() + service_ticket = self.get_service_ticket(tgt, service_creds) + + service_ticket = self.modified_ticket( + service_ticket, + modify_fn=self._modify_invalid, + checksum_keys=self.get_krbtgt_checksum_key()) + + self._validate_tgt(service_ticket, creds, + expected_error=KDC_ERR_POLICY) + + def test_s4u2self_service_ticket(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + service_creds = self.get_service_creds() + service_ticket = self.get_service_ticket(tgt, service_creds) + + self._s4u2self(service_ticket, creds, + expected_error=(KDC_ERR_NOT_US, KDC_ERR_POLICY)) + + def test_user2user_service_ticket(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + service_creds = self.get_service_creds() + service_ticket = self.get_service_ticket(tgt, service_creds) + + self._user2user(service_ticket, creds, + expected_error=(KDC_ERR_MODIFIED, KDC_ERR_POLICY)) + + # Expected to fail against Windows, which does not produce an error. + def test_fast_service_ticket(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + service_creds = self.get_service_creds() + service_ticket = self.get_service_ticket(tgt, service_creds) + + self._fast(service_ticket, creds, + expected_error=(KDC_ERR_POLICY, + KDC_ERR_S_PRINCIPAL_UNKNOWN)) + + def test_single_component_krbtgt_requester_sid_as_req(self): + """Test that TGTs issued to a single‐component krbtgt principal always + contain a requester SID PAC buffer. + """ + + creds = self._get_creds() + + # Create a single‐component principal of the form ‘krbtgt@REALM’. + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['krbtgt']) + + # Don’t request canonicalization. + kdc_options = 'forwardable,renewable,renewable-ok' + + # Get a TGT and assert that the requester SID PAC buffer is present. + self.get_tgt(creds, + sname=sname, + kdc_options=kdc_options, + expect_requester_sid=True) + + def test_single_component_krbtgt_requester_sid_tgs_req(self): + """Test that TGTs issued to a single‐component krbtgt principal always + contain a requester SID PAC buffer. + """ + + creds = self._get_creds() + tgt = self.get_tgt(creds) + + # Create a single‐component principal of the form ‘krbtgt@REALM’. + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['krbtgt']) + + # Don’t request canonicalization. + kdc_options = '0' + + # Get a TGT and assert that the requester SID PAC buffer is present. + self.get_service_ticket(tgt, + self.get_krbtgt_creds(), + sname=sname, + kdc_options=kdc_options, + expect_requester_sid=True) + + def test_single_component_krbtgt_no_pac_as_req(self): + """Test that TGTs issued to a single‐component krbtgt principal always + contain a PAC. + """ + + creds = self._get_creds() + + # Create a single‐component principal of the form ‘krbtgt@REALM’. + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['krbtgt']) + + # Don’t request canonicalization. + kdc_options = 'forwardable,renewable,renewable-ok' + + # Get a TGT and assert that the requester SID PAC buffer is present. + self.get_tgt(creds, + sname=sname, + kdc_options=kdc_options, + # Request that no PAC be issued. + pac_request=False, + # Ensure that a PAC is issued nonetheless. + expect_pac=True) + + def test_single_component_krbtgt_no_pac_tgs_req(self): + """Test that TGTs issued to a single‐component krbtgt principal always + contain a PAC. + """ + + creds = self._get_creds() + tgt = self.get_tgt(creds) + + # Create a single‐component principal of the form ‘krbtgt@REALM’. + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['krbtgt']) + + # Don’t request canonicalization. + kdc_options = '0' + + # Get a TGT and assert that the requester SID PAC buffer is present. + self.get_service_ticket(tgt, + self.get_krbtgt_creds(), + sname=sname, + kdc_options=kdc_options, + # Request that no PAC be issued. + pac_request=False, + # Ensure that a PAC is issued nonetheless. + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True) + + def test_single_component_krbtgt_service_ticket(self): + """Test that TGTs issued to a single‐component krbtgt principal can be + used to get service tickets. + """ + + creds = self._get_creds() + + # Create a single‐component principal of the form ‘krbtgt@REALM’. + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['krbtgt']) + + # Don’t request canonicalization. + kdc_options = 'forwardable,renewable,renewable-ok' + + # Get a TGT. + tgt = self.get_tgt(creds, + sname=sname, + kdc_options=kdc_options) + + # Ensure that we can use the TGT to get a service ticket. + self._run_tgs(tgt, creds, expected_error=0) + + def test_pac_attrs_none(self): + creds = self._get_creds() + self.get_tgt(creds, pac_request=None, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None) + + def test_pac_attrs_false(self): + creds = self._get_creds() + self.get_tgt(creds, pac_request=False, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False) + + def test_pac_attrs_true(self): + creds = self._get_creds() + self.get_tgt(creds, pac_request=True, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True) + + def test_pac_attrs_renew_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None) + tgt = self._modify_tgt(tgt, renewable=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None, + expect_requester_sid=True) + + def test_pac_attrs_renew_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False) + tgt = self._modify_tgt(tgt, renewable=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False, + expect_requester_sid=True) + + def test_pac_attrs_renew_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True) + tgt = self._modify_tgt(tgt, renewable=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + def test_pac_attrs_rodc_renew_none(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None) + tgt = self._modify_tgt(tgt, from_rodc=True, renewable=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_pac_attrs_rodc_renew_false(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=False, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False) + tgt = self._modify_tgt(tgt, from_rodc=True, renewable=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_pac_attrs_rodc_renew_true(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=True, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True) + tgt = self._modify_tgt(tgt, from_rodc=True, renewable=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_pac_attrs_missing_renew_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None) + tgt = self._modify_tgt(tgt, renewable=True, + remove_pac_attrs=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_pac_attrs_missing_renew_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False) + tgt = self._modify_tgt(tgt, renewable=True, + remove_pac_attrs=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_pac_attrs_missing_renew_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True) + tgt = self._modify_tgt(tgt, renewable=True, + remove_pac_attrs=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_pac_attrs_missing_rodc_renew_none(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None) + tgt = self._modify_tgt(tgt, from_rodc=True, renewable=True, + remove_pac_attrs=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_pac_attrs_missing_rodc_renew_false(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=False, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False) + tgt = self._modify_tgt(tgt, from_rodc=True, renewable=True, + remove_pac_attrs=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_pac_attrs_missing_rodc_renew_true(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=True, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True) + tgt = self._modify_tgt(tgt, from_rodc=True, renewable=True, + remove_pac_attrs=True) + + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac=True, + expect_pac_attrs=False, + expect_requester_sid=True) + + def test_tgs_pac_attrs_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None) + + self._run_tgs(tgt, creds, expected_error=0, expect_pac=True, + expect_pac_attrs=False) + + def test_tgs_pac_attrs_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False) + + self._run_tgs(tgt, creds, expected_error=0, expect_pac=False, + expect_pac_attrs=False) + + def test_tgs_pac_attrs_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True, + expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True) + + self._run_tgs(tgt, creds, expected_error=0, expect_pac=True, + expect_pac_attrs=False) + + def test_as_requester_sid(self): + creds = self._get_creds() + + sid = creds.get_sid() + + self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + + def test_tgs_requester_sid(self): + creds = self._get_creds() + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + + self._run_tgs(tgt, creds, expected_error=0, expect_pac=True, + expect_requester_sid=False) + + def test_tgs_requester_sid_renew(self): + creds = self._get_creds() + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + tgt = self._modify_tgt(tgt, renewable=True) + + self._renew_tgt(tgt, creds, expected_error=0, expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None, + expected_sid=sid, + expect_requester_sid=True) + + def test_tgs_requester_sid_rodc_renew(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + tgt = self._modify_tgt(tgt, from_rodc=True, renewable=True) + + self._renew_tgt(tgt, creds, expected_error=0, expect_pac=True, + expect_pac_attrs=False, + expected_sid=sid, + expect_requester_sid=True) + + def test_tgs_requester_sid_missing_renew(self): + creds = self._get_creds() + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + tgt = self._modify_tgt(tgt, renewable=True, + remove_requester_sid=True) + + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_tgs_requester_sid_missing_rodc_renew(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + tgt = self._modify_tgt(tgt, from_rodc=True, renewable=True, + remove_requester_sid=True) + + self._renew_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_tgs_requester_sid_validate(self): + creds = self._get_creds() + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + tgt = self._modify_tgt(tgt, invalid=True) + + self._validate_tgt(tgt, creds, expected_error=0, expect_pac=True, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None, + expected_sid=sid, + expect_requester_sid=True) + + def test_tgs_requester_sid_rodc_validate(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + tgt = self._modify_tgt(tgt, from_rodc=True, invalid=True) + + self._validate_tgt(tgt, creds, expected_error=0, expect_pac=True, + expect_pac_attrs=False, + expected_sid=sid, + expect_requester_sid=True) + + def test_tgs_requester_sid_missing_validate(self): + creds = self._get_creds() + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + tgt = self._modify_tgt(tgt, invalid=True, + remove_requester_sid=True) + + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_tgs_requester_sid_missing_rodc_validate(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + + sid = creds.get_sid() + + tgt = self.get_tgt(creds, pac_request=None, + expect_pac=True, + expected_sid=sid, + expect_requester_sid=True) + tgt = self._modify_tgt(tgt, from_rodc=True, invalid=True, + remove_requester_sid=True) + + self._validate_tgt(tgt, creds, expected_error=KDC_ERR_TGT_REVOKED) + + def test_tgs_pac_request_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_tgs_pac_request_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False, expect_pac=None) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=False) + + pac = self.get_ticket_pac(ticket, expect_pac=False) + self.assertIsNone(pac) + + def test_tgs_pac_request_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_renew_pac_request_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None) + tgt = self._modify_tgt(tgt, renewable=True) + + tgt = self._renew_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_renew_pac_request_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False, expect_pac=None) + tgt = self._modify_tgt(tgt, renewable=True) + + tgt = self._renew_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=False) + + pac = self.get_ticket_pac(ticket, expect_pac=False) + self.assertIsNone(pac) + + def test_renew_pac_request_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True) + tgt = self._modify_tgt(tgt, renewable=True) + + tgt = self._renew_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_rodc_renew_pac_request_none(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=None) + tgt = self._modify_tgt(tgt, renewable=True, from_rodc=True) + + tgt = self._renew_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=False, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_rodc_renew_pac_request_false(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=False, expect_pac=None) + tgt = self._modify_tgt(tgt, renewable=True, from_rodc=True) + + tgt = self._renew_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=False, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_rodc_renew_pac_request_true(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=True) + tgt = self._modify_tgt(tgt, renewable=True, from_rodc=True) + + tgt = self._renew_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=False, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_validate_pac_request_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None) + tgt = self._modify_tgt(tgt, invalid=True) + + tgt = self._validate_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=None, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_validate_pac_request_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False, expect_pac=None) + tgt = self._modify_tgt(tgt, invalid=True) + + tgt = self._validate_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=False, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=False) + + pac = self.get_ticket_pac(ticket, expect_pac=False) + self.assertIsNone(pac) + + def test_validate_pac_request_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True) + tgt = self._modify_tgt(tgt, invalid=True) + + tgt = self._validate_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_rodc_validate_pac_request_none(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=None) + tgt = self._modify_tgt(tgt, invalid=True, from_rodc=True) + + tgt = self._validate_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=False, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_rodc_validate_pac_request_false(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=False, expect_pac=None) + tgt = self._modify_tgt(tgt, invalid=True, from_rodc=True) + + tgt = self._validate_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=False, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_rodc_validate_pac_request_true(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=True) + tgt = self._modify_tgt(tgt, invalid=True, from_rodc=True) + + tgt = self._validate_tgt(tgt, creds, expected_error=0, expect_pac=None, + expect_pac_attrs=False, + expect_requester_sid=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_s4u2self_pac_request_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None) + + ticket = self._s4u2self(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_s4u2self_pac_request_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False, expect_pac=None) + + ticket = self._s4u2self(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_s4u2self_pac_request_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True) + + ticket = self._s4u2self(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_user2user_pac_request_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None) + + ticket = self._user2user(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_user2user_pac_request_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False, expect_pac=None) + + ticket = self._user2user(tgt, creds, expected_error=0, + expect_pac=True) + + pac = self.get_ticket_pac(ticket, expect_pac=True) + self.assertIsNotNone(pac) + + def test_user2user_pac_request_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True) + + ticket = self._user2user(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_user2user_user_pac_request_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds) + + user_creds = self._get_mach_creds() + user_tgt = self.get_tgt(user_creds, pac_request=None) + + ticket = self._user2user(tgt, creds, expected_error=0, + user_tgt=user_tgt, user_creds=user_creds, + expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_user2user_user_pac_request_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds) + + user_creds = self._get_mach_creds() + user_tgt = self.get_tgt(user_creds, pac_request=False, expect_pac=None) + + ticket = self._user2user(tgt, creds, expected_error=0, + user_tgt=user_tgt, user_creds=user_creds, + expect_pac=False) + + pac = self.get_ticket_pac(ticket, expect_pac=False) + self.assertIsNone(pac) + + def test_user2user_user_pac_request_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds) + + user_creds = self._get_mach_creds() + user_tgt = self.get_tgt(user_creds, pac_request=True) + + ticket = self._user2user(tgt, creds, expected_error=0, + user_tgt=user_tgt, user_creds=user_creds, + expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_fast_pac_request_none(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=None) + + ticket = self._fast(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_fast_pac_request_false(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=False) + + ticket = self._fast(tgt, creds, expected_error=0, + expect_pac=True) + + pac = self.get_ticket_pac(ticket, expect_pac=True) + self.assertIsNotNone(pac) + + def test_fast_pac_request_true(self): + creds = self._get_creds() + tgt = self.get_tgt(creds, pac_request=True) + + ticket = self._fast(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_tgs_rodc_pac_request_none(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=None) + tgt = self._modify_tgt(tgt, from_rodc=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_tgs_rodc_pac_request_false(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=False, expect_pac=None) + tgt = self._modify_tgt(tgt, from_rodc=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_tgs_rodc_pac_request_true(self): + creds = self._get_creds(replication_allowed=True, + revealed_to_rodc=True) + tgt = self.get_tgt(creds, pac_request=True) + tgt = self._modify_tgt(tgt, from_rodc=True) + + ticket = self._run_tgs(tgt, creds, expected_error=0, expect_pac=True) + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + def test_tgs_rename(self): + creds = self.get_cached_creds(account_type=self.AccountType.USER, + use_cache=False) + tgt = self.get_tgt(creds) + + # Rename the account. + new_name = self.get_new_username() + + samdb = self.get_samdb() + msg = ldb.Message(creds.get_dn()) + msg['sAMAccountName'] = ldb.MessageElement(new_name, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + + self._run_tgs(tgt, creds, expected_error=(KDC_ERR_TGT_REVOKED, + KDC_ERR_C_PRINCIPAL_UNKNOWN)) + + # Test making a TGS request for a ticket expiring post-2038. + def test_tgs_req_future_till(self): + creds = self._get_creds() + tgt = self._get_tgt(creds) + + target_creds = self.get_service_creds() + self._tgs_req( + tgt=tgt, + expected_error=0, + creds=creds, + target_creds=target_creds, + till='99990913024805Z') + + def test_tgs_unicode(self): + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_prefix': '🔐'}) + tgt = self._get_tgt(creds) + self._run_tgs(tgt, creds, expected_error=0) + + def test_renew_unicode(self): + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_prefix': '🔐'}) + tgt = self._get_tgt(creds, renewable=True) + self._renew_tgt(tgt, creds, expected_error=0, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + def test_validate_unicode(self): + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_prefix': '🔐'}) + tgt = self._get_tgt(creds, invalid=True) + self._validate_tgt(tgt, creds, expected_error=0, + expect_pac_attrs=True, + expect_pac_attrs_pac_request=True, + expect_requester_sid=True) + + def test_s4u2self_unicode(self): + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_prefix': '🔐'}) + tgt = self._get_tgt(creds) + self._s4u2self(tgt, creds, + expected_error=0, + expect_edata=False) + + def test_user2user_unicode(self): + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_prefix': '🔐'}) + tgt = self._get_tgt(creds) + self._user2user(tgt, creds, expected_error=0) + + def test_fast_unicode(self): + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_prefix': '🔐'}) + tgt = self._get_tgt(creds) + self._fast(tgt, creds, expected_error=0) + + def test_fast_as_req_unicode(self): + creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_prefix': '🔐'}) + tgt = self._get_tgt(creds) + self._fast_as_req(tgt, creds, expected_error=0) + + def _modify_renewable(self, enc_part): + # Set the renewable flag. + enc_part = self.modify_ticket_flag(enc_part, 'renewable', value=True) + + # Set the renew-till time to be in the future. + renew_till = self.get_KerberosTime(offset=100 * 60 * 60) + enc_part['renew-till'] = renew_till + + return enc_part + + def _modify_invalid(self, enc_part): + # Set the invalid flag. + enc_part = self.modify_ticket_flag(enc_part, 'invalid', value=True) + + # Set the ticket start time to be in the past. + past_time = self.get_KerberosTime(offset=-100 * 60 * 60) + enc_part['starttime'] = past_time + + return enc_part + + def _get_tgt(self, + client_creds, + renewable=False, + invalid=False, + from_rodc=False, + new_rid=None, + remove_pac=False, + allow_empty_authdata=False, + can_modify_logon_info=True, + can_modify_requester_sid=True, + remove_pac_attrs=False, + remove_requester_sid=False, + etype=None, + cksum_etype=None, + extra_pac_buffers=None): + self.assertFalse(renewable and invalid) + + if remove_pac: + self.assertIsNone(new_rid) + + tgt = self.get_tgt(client_creds) + + return self._modify_tgt( + tgt=tgt, + renewable=renewable, + invalid=invalid, + from_rodc=from_rodc, + new_rid=new_rid, + remove_pac=remove_pac, + allow_empty_authdata=allow_empty_authdata, + can_modify_logon_info=can_modify_logon_info, + can_modify_requester_sid=can_modify_requester_sid, + remove_pac_attrs=remove_pac_attrs, + remove_requester_sid=remove_requester_sid, + etype=etype, + cksum_etype=cksum_etype, + extra_pac_buffers=extra_pac_buffers) + + def _modify_tgt(self, + tgt, + *, + renewable=False, + invalid=False, + from_rodc=False, + new_rid=None, + remove_pac=False, + allow_empty_authdata=False, + cname=None, + crealm=None, + can_modify_logon_info=True, + can_modify_requester_sid=True, + remove_pac_attrs=False, + remove_requester_sid=False, + etype=None, + cksum_etype=None, + extra_pac_buffers=None): + if from_rodc: + krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + else: + krbtgt_creds = self.get_krbtgt_creds() + + modify_pac_fns = [] + + if new_rid is not None or remove_requester_sid or remove_pac_attrs: + def change_sid_fn(pac): + pac_buffers = pac.buffers + for pac_buffer in pac_buffers: + if pac_buffer.type == krb5pac.PAC_TYPE_LOGON_INFO: + if new_rid is not None and can_modify_logon_info: + logon_info = pac_buffer.info.info + + logon_info.info3.base.rid = new_rid + elif pac_buffer.type == krb5pac.PAC_TYPE_REQUESTER_SID: + if remove_requester_sid: + pac.num_buffers -= 1 + pac_buffers.remove(pac_buffer) + elif new_rid is not None and can_modify_requester_sid: + requester_sid = pac_buffer.info + + samdb = self.get_samdb() + domain_sid = samdb.get_domain_sid() + + new_sid = f'{domain_sid}-{new_rid}' + + requester_sid.sid = security.dom_sid(new_sid) + elif pac_buffer.type == krb5pac.PAC_TYPE_ATTRIBUTES_INFO: + if remove_pac_attrs: + pac.num_buffers -= 1 + pac_buffers.remove(pac_buffer) + + pac.buffers = pac_buffers + + return pac + + modify_pac_fns.append(change_sid_fn) + + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds, + etype) + + if remove_pac: + checksum_keys = None + else: + if etype == cksum_etype: + cksum_key = krbtgt_key + else: + cksum_key = self.TicketDecryptionKey_from_creds(krbtgt_creds, + cksum_etype) + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: cksum_key + } + + if renewable: + flags_modify_fn = self._modify_renewable + elif invalid: + flags_modify_fn = self._modify_invalid + else: + flags_modify_fn = None + + if cname is not None or crealm is not None: + def modify_fn(enc_part): + if flags_modify_fn is not None: + enc_part = flags_modify_fn(enc_part) + + if cname is not None: + enc_part['cname'] = cname + + if crealm is not None: + enc_part['crealm'] = crealm + + return enc_part + else: + modify_fn = flags_modify_fn + + if cname is not None: + def change_cname_fn(pac): + for pac_buffer in pac.buffers: + if pac_buffer.type == krb5pac.PAC_TYPE_LOGON_NAME: + logon_info = pac_buffer.info + + logon_info.account_name = ( + cname['name-string'][0].decode('utf-8')) + + return pac + + modify_pac_fns.append(change_cname_fn) + + if extra_pac_buffers is not None: + modify_pac_fns.append(partial(self.add_extra_pac_buffers, + buffers=extra_pac_buffers)) + + return self.modified_ticket( + tgt, + new_ticket_key=krbtgt_key, + modify_fn=modify_fn, + modify_pac_fn=modify_pac_fns or None, + exclude_pac=remove_pac, + allow_empty_authdata=allow_empty_authdata, + update_pac_checksums=not remove_pac, + checksum_keys=checksum_keys) + + def _remove_rodc_partial_secrets(self): + samdb = self.get_samdb() + + rodc_ctx = self.get_mock_rodc_ctx() + rodc_dn = ldb.Dn(samdb, rodc_ctx.acct_dn) + + def add_rodc_partial_secrets(): + msg = ldb.Message() + msg.dn = rodc_dn + msg['userAccountControl'] = ldb.MessageElement( + str(rodc_ctx.userAccountControl), + ldb.FLAG_MOD_REPLACE, + 'userAccountControl') + samdb.modify(msg) + + self.addCleanup(add_rodc_partial_secrets) + + uac = rodc_ctx.userAccountControl & ~dsdb.UF_PARTIAL_SECRETS_ACCOUNT + + msg = ldb.Message() + msg.dn = rodc_dn + msg['userAccountControl'] = ldb.MessageElement( + str(uac), + ldb.FLAG_MOD_REPLACE, + 'userAccountControl') + samdb.modify(msg) + + def _remove_rodc_krbtgt_link(self): + samdb = self.get_samdb() + + rodc_ctx = self.get_mock_rodc_ctx() + rodc_dn = ldb.Dn(samdb, rodc_ctx.acct_dn) + + def add_rodc_krbtgt_link(): + msg = ldb.Message() + msg.dn = rodc_dn + msg['msDS-KrbTgtLink'] = ldb.MessageElement( + rodc_ctx.new_krbtgt_dn, + ldb.FLAG_MOD_ADD, + 'msDS-KrbTgtLink') + samdb.modify(msg) + + self.addCleanup(add_rodc_krbtgt_link) + + msg = ldb.Message() + msg.dn = rodc_dn + msg['msDS-KrbTgtLink'] = ldb.MessageElement( + [], + ldb.FLAG_MOD_DELETE, + 'msDS-KrbTgtLink') + samdb.modify(msg) + + def _get_creds(self, + replication_allowed=False, + replication_denied=False, + revealed_to_rodc=False): + return self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'allowed_replication_mock': replication_allowed, + 'denied_replication_mock': replication_denied, + 'revealed_to_mock_rodc': revealed_to_rodc, + 'id': 0 + }) + + def _get_existing_rid(self, + replication_allowed=False, + replication_denied=False, + revealed_to_rodc=False): + other_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'allowed_replication_mock': replication_allowed, + 'denied_replication_mock': replication_denied, + 'revealed_to_mock_rodc': revealed_to_rodc, + 'id': 1 + }) + + other_sid = other_creds.get_sid() + other_rid = int(other_sid.rsplit('-', 1)[1]) + + return other_rid + + def _get_mach_creds(self): + return self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'allowed_replication_mock': True, + 'denied_replication_mock': False, + 'revealed_to_mock_rodc': True, + 'id': 2 + }) + + def _get_user_creds(self, + replication_allowed=False, + replication_denied=False, + revealed_to_rodc=False): + return self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'allowed_replication_mock': replication_allowed, + 'denied_replication_mock': replication_denied, + 'revealed_to_mock_rodc': revealed_to_rodc, + 'id': 3 + }) + + def _get_non_existent_rid(self): + return (1 << 30) - 1 + + def _run_tgs(self, tgt, creds, expected_error, *, expect_pac=True, + expect_pac_attrs=None, expect_pac_attrs_pac_request=None, + expect_requester_sid=None, expected_sid=None, + expect_edata=False, expect_status=None, expected_status=None, + expected_extra_pac_buffers=None): + target_creds = self.get_service_creds() + return self._tgs_req( + tgt, expected_error, creds, target_creds, + expect_pac=expect_pac, + expect_pac_attrs=expect_pac_attrs, + expect_pac_attrs_pac_request=expect_pac_attrs_pac_request, + expect_requester_sid=expect_requester_sid, + expected_sid=expected_sid, + expect_edata=expect_edata, + expect_status=expect_status, + expected_status=expected_status, + expected_extra_pac_buffers=expected_extra_pac_buffers) + + # These tests fail against Windows, which does not implement ticket + # renewal. + def _renew_tgt(self, tgt, creds, expected_error, *, expect_pac=True, + expect_pac_attrs=None, expect_pac_attrs_pac_request=None, + expect_requester_sid=None, expected_sid=None): + krbtgt_creds = self.get_krbtgt_creds() + kdc_options = str(krb5_asn1.KDCOptions('renew')) + return self._tgs_req( + tgt, expected_error, creds, krbtgt_creds, + kdc_options=kdc_options, + expect_pac=expect_pac, + expect_pac_attrs=expect_pac_attrs, + expect_pac_attrs_pac_request=expect_pac_attrs_pac_request, + expect_requester_sid=expect_requester_sid, + expected_sid=expected_sid) + + # These tests fail against Windows, which does not implement ticket + # validation. + def _validate_tgt(self, tgt, creds, expected_error, *, expect_pac=True, + expect_pac_attrs=None, + expect_pac_attrs_pac_request=None, + expect_requester_sid=None, + expected_sid=None): + krbtgt_creds = self.get_krbtgt_creds() + kdc_options = str(krb5_asn1.KDCOptions('validate')) + return self._tgs_req( + tgt, expected_error, creds, krbtgt_creds, + kdc_options=kdc_options, + expect_pac=expect_pac, + expect_pac_attrs=expect_pac_attrs, + expect_pac_attrs_pac_request=expect_pac_attrs_pac_request, + expect_requester_sid=expect_requester_sid, + expected_sid=expected_sid) + + def _s4u2self(self, tgt, tgt_creds, expected_error, *, expect_pac=True, + expect_edata=False, expect_status=None, + expected_status=None): + user_creds = self._get_mach_creds() + + user_name = user_creds.get_username() + user_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[user_name]) + user_realm = user_creds.get_realm() + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + padata = self.PA_S4U2Self_create( + name=user_cname, + realm=user_realm, + tgt_session_key=tgt.session_key, + ctype=None) + + return [padata], req_body + + return self._tgs_req(tgt, expected_error, tgt_creds, tgt_creds, + expected_cname=user_cname, + generate_padata_fn=generate_s4u2self_padata, + expect_edata=expect_edata, + expect_status=expect_status, + expected_status=expected_status, + expect_pac=expect_pac) + + def _user2user(self, tgt, tgt_creds, expected_error, *, + sname=None, + srealm=None, user_tgt=None, user_creds=None, + expect_edata=False, + expect_pac=True, expected_status=None): + if user_tgt is None: + user_creds = self._get_mach_creds() + user_tgt = self.get_tgt(user_creds) + else: + self.assertIsNotNone(user_creds, + 'if supplying user_tgt, user_creds should be ' + 'supplied also') + + kdc_options = str(krb5_asn1.KDCOptions('enc-tkt-in-skey')) + return self._tgs_req(user_tgt, expected_error, user_creds, tgt_creds, + kdc_options=kdc_options, + additional_ticket=tgt, + sname=sname, + srealm=srealm, + expect_edata=expect_edata, + expect_pac=expect_pac, + expected_status=expected_status) + + def _fast(self, armor_tgt, armor_tgt_creds, expected_error, + expected_sname=None, expect_pac=True, expect_edata=False): + user_creds = self._get_mach_creds() + user_tgt = self.get_tgt(user_creds) + + target_creds = self.get_service_creds() + + return self._tgs_req(user_tgt, expected_error, + user_creds, target_creds, + armor_tgt=armor_tgt, + expected_sname=expected_sname, + expect_pac=expect_pac, + expect_edata=expect_edata) + + def _fast_as_req(self, armor_tgt, armor_tgt_creds, expected_error, + expected_sname=None): + user_creds = self._get_mach_creds() + target_creds = self.get_service_creds() + + return self._armored_as_req(user_creds, target_creds, armor_tgt, + expected_error=expected_error, + expected_sname=expected_sname, + expect_edata=False) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/kdc_tgt_tests.py b/python/samba/tests/krb5/kdc_tgt_tests.py new file mode 100755 index 0000000..5a52a95 --- /dev/null +++ b/python/samba/tests/krb5/kdc_tgt_tests.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2020 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +class KdcTgtTests(KDCBaseTest): + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def test_ticket_signature(self): + # Ensure that a DC correctly issues tickets signed with its krbtgt key. + user_creds = self.get_client_creds() + target_creds = self.get_service_creds() + + krbtgt_creds = self.get_krbtgt_creds() + key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + # Get a TGT from the DC. + tgt = self.get_tgt(user_creds) + + # Ensure the PAC contains the expected checksums. + self.verify_ticket(tgt, key, service_ticket=False) + + # Get a service ticket from the DC. + service_ticket = self.get_service_ticket(tgt, target_creds) + + # Ensure the PAC contains the expected checksums. + self.verify_ticket(service_ticket, key, service_ticket=True, + expect_ticket_checksum=True) + + def test_full_signature(self): + # Ensure that a DC correctly issues tickets signed with its krbtgt key. + user_creds = self.get_client_creds() + target_creds = self.get_service_creds() + + krbtgt_creds = self.get_krbtgt_creds() + key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + # Get a TGT from the DC. + tgt = self.get_tgt(user_creds) + + # Ensure the PAC contains the expected checksums. + self.verify_ticket(tgt, key, service_ticket=False) + + # Get a service ticket from the DC. + service_ticket = self.get_service_ticket(tgt, target_creds) + + # Ensure the PAC contains the expected checksums. + self.verify_ticket(service_ticket, key, service_ticket=True, + expect_ticket_checksum=True, + expect_full_checksum=True) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/kpasswd_tests.py b/python/samba/tests/krb5/kpasswd_tests.py new file mode 100755 index 0000000..0f1fe65 --- /dev/null +++ b/python/samba/tests/krb5/kpasswd_tests.py @@ -0,0 +1,983 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from functools import partial + +from samba import generate_random_password +from samba.dcerpc import krb5pac +from samba.sd_utils import SDUtils + +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.rfc4120_constants import ( + KDC_ERR_TGT_REVOKED, + KDC_ERR_TKT_EXPIRED, + KPASSWD_ACCESSDENIED, + KPASSWD_AUTHERROR, + KPASSWD_HARDERROR, + KPASSWD_INITIAL_FLAG_NEEDED, + KPASSWD_MALFORMED, + KPASSWD_SOFTERROR, + KPASSWD_SUCCESS, + NT_PRINCIPAL, + NT_SRV_INST, +) + +global_asn1_print = False +global_hexdump = False + + +# Note: these tests do not pass on Windows, which returns different error codes +# to the ones we have chosen, and does not always return additional error data. +class KpasswdTests(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, expired=False): + opts = { + 'expired_password': expired + } + + # Create the account. + creds = self.get_cached_creds(account_type=self.AccountType.USER, + opts=opts, + use_cache=False) + + 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 + + authtime = enc_part['authtime'] + starttime = enc_part.get('starttime', authtime) + endtime = enc_part['endtime'] + + starttime = self.get_EpochFromKerberosTime(starttime) + endtime = self.get_EpochFromKerberosTime(endtime) + + return endtime - starttime + + # Test setting a password with kpasswd. + def test_kpasswd_set(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') + + 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) + + # Test the newly set password. + creds.update_password(new_password) + self.get_tgt(creds, fresh=True) + + # Test changing a password with kpasswd. + def test_kpasswd_change(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') + + 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) + + # Test the newly set password. + creds.update_password(new_password) + self.get_tgt(creds, fresh=True) + + # Test kpasswd without setting the canonicalize option. + def test_kpasswd_no_canonicalize(self): + # Create an account for testing. + creds = self._get_creds() + + sname = self.get_kpasswd_sname() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=sname, + kdc_options='0') + + 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) + + creds.update_password(new_password) + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=sname, + kdc_options='0') + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test kpasswd with the canonicalize option reset and a non-canonical + # (by conversion to title case) realm. + def test_kpasswd_no_canonicalize_realm_case(self): + # Create an account for testing. + creds = self._get_creds() + + sname = self.get_kpasswd_sname() + realm = creds.get_realm().capitalize() # We use a title-cased realm. + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=sname, + realm=realm, + kdc_options='0') + + 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) + + creds.update_password(new_password) + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=sname, + realm=realm, + kdc_options='0') + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test kpasswd with the canonicalize option set. + def test_kpasswd_canonicalize(self): + # Create an account for testing. + creds = self._get_creds() + + # Get an initial ticket to kpasswd. We set the canonicalize flag here. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='canonicalize') + + 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) + + creds.update_password(new_password) + + # Get an initial ticket to kpasswd. We set the canonicalize flag here. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='canonicalize') + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test kpasswd with the canonicalize option set and a non-canonical (by + # conversion to title case) realm. + def test_kpasswd_canonicalize_realm_case(self): + # Create an account for testing. + creds = self._get_creds() + + sname = self.get_kpasswd_sname() + realm = creds.get_realm().capitalize() # We use a title-cased realm. + + # Get an initial ticket to kpasswd. We set the canonicalize flag here. + ticket = self.get_tgt(creds, sname=sname, + realm=realm, + kdc_options='canonicalize') + + 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) + + creds.update_password(new_password) + + # Get an initial ticket to kpasswd. We set the canonicalize flag here. + ticket = self.get_tgt(creds, sname=sname, + realm=realm, + kdc_options='canonicalize') + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test kpasswd rejects a password that does not meet complexity + # requirements. + def test_kpasswd_too_weak(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') + + expected_code = KPASSWD_SOFTERROR + expected_msg = b'Password does not meet complexity requirements' + + # Set the password. + new_password = 'password' + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + # Change the password. + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test kpasswd rejects an empty new password. + def test_kpasswd_empty(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') + + expected_code = KPASSWD_SOFTERROR, KPASSWD_HARDERROR + expected_msg = (b'Password too short, password must be at least 7 ' + b'characters long.', + b'String conversion failed!') + + # Set the password. + new_password = '' + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + expected_code = KPASSWD_HARDERROR + expected_msg = b'String conversion failed!' + + # Change the password. + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test kpasswd rejects a request that does not include a random sequence + # number. + def test_kpasswd_no_seq_number(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') + + expected_code = KPASSWD_HARDERROR + expected_msg = b'gensec_unwrap failed - NT_STATUS_ACCESS_DENIED\n' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET, + send_seq_number=False) + + # Change the password. + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE, + send_seq_number=False) + + # Test kpasswd rejects a ticket issued by an RODC. + def test_kpasswd_from_rodc(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') + + # Have the ticket be issued by the RODC. + ticket = self.issued_by_rodc(ticket) + + expected_code = KPASSWD_HARDERROR + expected_msg = b'gensec_update failed - NT_STATUS_LOGON_FAILURE\n' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + # Change the password. + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test setting a password, specifying the principal of the target user. + def test_kpasswd_set_target_princ_only(self): + # Create an account for testing. + creds = self._get_creds() + username = creds.get_username() + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=username.split('/')) + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + expected_code = KPASSWD_MALFORMED + expected_msg = (b'Realm and principal must be both present, or ' + b'neither present', + b'Failed to decode packet') + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET, + target_princ=cname) + + # Test that kpasswd rejects a password set specifying only the realm of the + # target user. + def test_kpasswd_set_target_realm_only(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') + + expected_code = KPASSWD_MALFORMED, KPASSWD_ACCESSDENIED + expected_msg = (b'Realm and principal must be both present, or ' + b'neither present', + b'Failed to decode packet', + b'No such user when changing password') + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET, + target_realm=creds.get_realm()) + + # Show that a user cannot set a password, specifying both principal and + # realm of the target user, without having control access. + def test_kpasswd_set_target_princ_and_realm_no_access(self): + # Create an account for testing. + creds = self._get_creds() + username = creds.get_username() + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=username.split('/')) + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + expected_code = KPASSWD_ACCESSDENIED + expected_msg = b'Not permitted to change password' + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET, + target_princ=cname, + target_realm=creds.get_realm()) + + # Test setting a password, specifying both principal and realm of the + # target user, when the user has control access on their account. + def test_kpasswd_set_target_princ_and_realm_access(self): + # Create an account for testing. + creds = self._get_creds() + username = creds.get_username() + tgt = self.get_tgt(creds) + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=username.split('/')) + + samdb = self.get_samdb() + sd_utils = SDUtils(samdb) + + user_dn = creds.get_dn() + user_sid = creds.get_sid() + + # Give the user control access on their account. + ace = f'(A;;CR;;;{user_sid})' + sd_utils.dacl_add_ace(user_dn, ace) + + # Get a non-initial ticket to kpasswd. Since we have the right to + # change the account's password, we don't need an initial ticket. + krbtgt_creds = self.get_krbtgt_creds() + ticket = self.get_service_ticket(tgt, + krbtgt_creds, + service='kadmin', + target_name='changepw', + kdc_options='0') + + 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.SET, + target_princ=cname, + target_realm=creds.get_realm()) + + # Test setting a password when the existing password has expired. + def test_kpasswd_set_expired_password(self): + # Create an account for testing, with an expired password. + creds = self._get_creds(expired=True) + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + 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) + + # Test changing a password when the existing password has expired. + def test_kpasswd_change_expired_password(self): + # Create an account for testing, with an expired password. + creds = self._get_creds(expired=True) + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + 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) + + # Check the lifetime of a kpasswd ticket is not more than two minutes. + def test_kpasswd_ticket_lifetime(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') + + # Check the lifetime of the ticket is equal to two minutes. + lifetime = self.get_ticket_lifetime(ticket) + self.assertEqual(2 * 60, lifetime) + + # Ensure we cannot perform a TGS-REQ with a kpasswd ticket. + def test_kpasswd_ticket_tgs(self): + creds = self.get_client_creds() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + # Change the sname of the ticket to match that of a TGT. + realm = creds.get_realm() + krbtgt_sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=['krbtgt', realm]) + ticket.set_sname(krbtgt_sname) + + # Try to use that ticket to get a service ticket. + service_creds = self.get_service_creds() + + # This fails due to missing REQUESTER_SID buffer. + self._make_tgs_request(creds, service_creds, ticket, + expect_error=(KDC_ERR_TGT_REVOKED, + KDC_ERR_TKT_EXPIRED)) + + # Ensure we cannot perform a TGS-REQ with a kpasswd ticket containing a + # requester SID and having a remaining lifetime of two minutes. + def test_kpasswd_ticket_requester_sid_tgs(self): + creds = self.get_client_creds() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + # Change the sname of the ticket to match that of a TGT. + realm = creds.get_realm() + krbtgt_sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=['krbtgt', realm]) + ticket.set_sname(krbtgt_sname) + + # Modify the ticket to add a requester SID and give it two minutes to + # live. + ticket = self.modify_lifetime(ticket, + lifetime=2 * 60, + requester_sid=creds.get_sid()) + + # Try to use that ticket to get a service ticket. + service_creds = self.get_service_creds() + + # This fails due to the lifetime being too short. + self._make_tgs_request(creds, service_creds, ticket, + expect_error=KDC_ERR_TKT_EXPIRED) + + # Show we can perform a TGS-REQ with a kpasswd ticket containing a + # requester SID if the remaining lifetime exceeds two minutes. + def test_kpasswd_ticket_requester_sid_lifetime_tgs(self): + creds = self.get_client_creds() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=self.get_kpasswd_sname(), + kdc_options='0') + + # Change the sname of the ticket to match that of a TGT. + realm = creds.get_realm() + krbtgt_sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=['krbtgt', realm]) + ticket.set_sname(krbtgt_sname) + + # Modify the ticket to add a requester SID and give it two minutes and + # ten seconds to live. + ticket = self.modify_lifetime(ticket, + lifetime=2 * 60 + 10, + requester_sid=creds.get_sid()) + + # Try to use that ticket to get a service ticket. + service_creds = self.get_service_creds() + + # This succeeds. + self._make_tgs_request(creds, service_creds, ticket, + expect_error=False) + + # Show that we cannot provide a TGT to kpasswd to change the password. + def test_kpasswd_tgt(self): + # Create an account for testing, and get a TGT. + creds = self._get_creds() + tgt = self.get_tgt(creds) + + # Change the sname of the ticket to match that of kadmin/changepw. + tgt.set_sname(self.get_kpasswd_sname()) + + expected_code = KPASSWD_AUTHERROR + expected_msg = b'A TGT may not be used as a ticket to kpasswd' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(tgt, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + # Change the password. + self.kpasswd_exchange(tgt, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Show that we cannot provide a TGT to kpasswd that was obtained with a + # single‐component principal. + def test_kpasswd_tgt_single_component_krbtgt(self): + # Create an account for testing. + creds = self._get_creds() + + # Create a single‐component principal of the form ‘krbtgt@REALM’. + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['krbtgt']) + + # Don’t request canonicalization. + kdc_options = 'forwardable,renewable,renewable-ok' + + # Get a TGT. + tgt = self.get_tgt(creds, sname=sname, kdc_options=kdc_options) + + # Change the sname of the ticket to match that of kadmin/changepw. + tgt.set_sname(self.get_kpasswd_sname()) + + expected_code = KPASSWD_AUTHERROR + expected_msg = b'A TGT may not be used as a ticket to kpasswd' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(tgt, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + # Change the password. + self.kpasswd_exchange(tgt, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test that kpasswd rejects requests with a service ticket. + def test_kpasswd_non_initial(self): + # Create an account for testing, and get a TGT. + creds = self._get_creds() + tgt = self.get_tgt(creds) + + # Get a non-initial ticket to kpasswd. + krbtgt_creds = self.get_krbtgt_creds() + ticket = self.get_service_ticket(tgt, + krbtgt_creds, + service='kadmin', + target_name='changepw', + kdc_options='0') + + expected_code = KPASSWD_INITIAL_FLAG_NEEDED + expected_msg = b'Expected an initial ticket' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + # Change the password. + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Show that kpasswd accepts requests with a service ticket modified to set + # the 'initial' flag. + def test_kpasswd_initial(self): + # Create an account for testing, and get a TGT. + creds = self._get_creds() + + krbtgt_creds = self.get_krbtgt_creds() + + # Get a service ticket, and modify it to set the 'initial' flag. + def get_ticket(): + tgt = self.get_tgt(creds, fresh=True) + + # Get a non-initial ticket to kpasswd. + ticket = self.get_service_ticket(tgt, + krbtgt_creds, + service='kadmin', + target_name='changepw', + kdc_options='0', + fresh=True) + + set_initial_flag = partial(self.modify_ticket_flag, flag='initial', + value=True) + + checksum_keys = self.get_krbtgt_checksum_key() + return self.modified_ticket(ticket, + modify_fn=set_initial_flag, + checksum_keys=checksum_keys) + + expected_code = KPASSWD_SUCCESS + expected_msg = b'Password changed' + + ticket = get_ticket() + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + creds.update_password(new_password) + ticket = get_ticket() + + # Change the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test that kpasswd rejects requests where the ticket is encrypted with a + # key other than the krbtgt's. + def test_kpasswd_wrong_key(self): + # Create an account for testing. + creds = self._get_creds() + + sname = self.get_kpasswd_sname() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=sname, + kdc_options='0') + + # Get a key belonging to the Administrator account. + admin_creds = self.get_admin_creds() + admin_key = self.TicketDecryptionKey_from_creds(admin_creds) + self.assertIsNotNone(admin_key.kvno, + 'a kvno is required to tell the DB ' + 'which key to look up.') + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: admin_key, + } + + # Re-encrypt the ticket using the Administrator's key. + ticket = self.modified_ticket(ticket, + new_ticket_key=admin_key, + checksum_keys=checksum_keys) + + # Set the sname of the ticket to that of the Administrator account. + admin_sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['Administrator']) + ticket.set_sname(admin_sname) + + expected_code = KPASSWD_HARDERROR + expected_msg = b'gensec_update failed - NT_STATUS_LOGON_FAILURE\n' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + # Change the password. + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + def test_kpasswd_wrong_key_service(self): + # Create an account for testing. + creds = self.get_cached_creds(account_type=self.AccountType.COMPUTER, + use_cache=False) + + sname = self.get_kpasswd_sname() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=sname, + kdc_options='0') + + # Get a key belonging to our account. + our_key = self.TicketDecryptionKey_from_creds(creds) + self.assertIsNotNone(our_key.kvno, + 'a kvno is required to tell the DB ' + 'which key to look up.') + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: our_key, + } + + # Re-encrypt the ticket using our key. + ticket = self.modified_ticket(ticket, + new_ticket_key=our_key, + checksum_keys=checksum_keys) + + # Set the sname of the ticket to that of our account. + username = creds.get_username() + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=username.split('/')) + ticket.set_sname(sname) + + expected_code = KPASSWD_HARDERROR + expected_msg = b'gensec_update failed - NT_STATUS_LOGON_FAILURE\n' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + # Change the password. + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + # Test that kpasswd rejects requests where the ticket is encrypted with a + # key belonging to a server account other than the krbtgt. + def test_kpasswd_wrong_key_server(self): + # Create an account for testing. + creds = self._get_creds() + + sname = self.get_kpasswd_sname() + + # Get an initial ticket to kpasswd. + ticket = self.get_tgt(creds, sname=sname, + kdc_options='0') + + # Get a key belonging to the DC's account. + dc_creds = self.get_dc_creds() + dc_key = self.TicketDecryptionKey_from_creds(dc_creds) + self.assertIsNotNone(dc_key.kvno, + 'a kvno is required to tell the DB ' + 'which key to look up.') + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: dc_key, + } + + # Re-encrypt the ticket using the DC's key. + ticket = self.modified_ticket(ticket, + new_ticket_key=dc_key, + checksum_keys=checksum_keys) + + # Set the sname of the ticket to that of the DC's account. + dc_username = dc_creds.get_username() + dc_sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=dc_username.split('/')) + ticket.set_sname(dc_sname) + + expected_code = KPASSWD_HARDERROR + expected_msg = b'gensec_update failed - NT_STATUS_LOGON_FAILURE\n' + + # Set the password. + new_password = generate_random_password(32, 32) + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.SET) + + # Change the password. + self.kpasswd_exchange(ticket, + new_password, + expected_code, + expected_msg, + mode=self.KpasswdMode.CHANGE) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/lockout_tests.py b/python/samba/tests/krb5/lockout_tests.py new file mode 100755 index 0000000..d91eb1d --- /dev/null +++ b/python/samba/tests/krb5/lockout_tests.py @@ -0,0 +1,1137 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from concurrent import futures +from enum import Enum +from functools import partial +from multiprocessing import Pipe +import time + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers.base import Cipher +from cryptography.hazmat.primitives.ciphers import algorithms + +import ldb + +from samba import ( + NTSTATUSError, + dsdb, + generate_random_bytes, + generate_random_password, + ntstatus, + unix2nttime, + werror, +) +from samba.credentials import DONT_USE_KERBEROS, MUST_USE_KERBEROS +from samba.crypto import ( + aead_aes_256_cbc_hmac_sha512_blob, + des_crypt_blob_16, + md4_hash_blob, + sha512_pbkdf2, +) +from samba.dcerpc import lsa, samr +from samba.samdb import SamDB + +from samba.tests import connect_samdb, env_get_var_value, env_loadparm + +from samba.tests.krb5.as_req_tests import AsReqBaseTest +from samba.tests.krb5 import kcrypto +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.raw_testcase import KerberosCredentials +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +from samba.tests.krb5.rfc4120_constants import ( + KDC_ERR_CLIENT_REVOKED, + KDC_ERR_PREAUTH_FAILED, + KRB_AS_REP, + KRB_ERROR, + NT_PRINCIPAL, + NT_SRV_INST, +) + +global_asn1_print = False +global_hexdump = False + + +class ConnectionResult(Enum): + LOCKED_OUT = 1 + WRONG_PASSWORD = 2 + SUCCESS = 3 + + +def connect_kdc(pipe, + url, + hostname, + username, + password, + domain, + realm, + workstation, + dn, + expect_error=True, + expect_status=None): + AsReqBaseTest.setUpClass() + as_req_base = AsReqBaseTest() + as_req_base.setUp() + + user_creds = KerberosCredentials() + user_creds.set_username(username) + user_creds.set_password(password) + user_creds.set_domain(domain) + user_creds.set_realm(realm) + user_creds.set_workstation(workstation) + user_creds.set_kerberos_state(DONT_USE_KERBEROS) + + user_name = user_creds.get_username() + cname = as_req_base.PrincipalName_create(name_type=NT_PRINCIPAL, + names=user_name.split('/')) + + krbtgt_creds = as_req_base.get_krbtgt_creds() + krbtgt_supported_etypes = krbtgt_creds.tgs_supported_enctypes + realm = krbtgt_creds.get_realm() + + krbtgt_account = krbtgt_creds.get_username() + sname = as_req_base.PrincipalName_create(name_type=NT_SRV_INST, + names=[krbtgt_account, realm]) + + expected_salt = user_creds.get_salt() + + till = as_req_base.get_KerberosTime(offset=36000) + + kdc_options = krb5_asn1.KDCOptions('postdated') + + preauth_key = as_req_base.PasswordKey_from_creds(user_creds, + kcrypto.Enctype.AES256) + + ts_enc_padata = as_req_base.get_enc_timestamp_pa_data_from_key(preauth_key) + padata = [ts_enc_padata] + + krbtgt_decryption_key = ( + as_req_base.TicketDecryptionKey_from_creds(krbtgt_creds)) + + etypes = as_req_base.get_default_enctypes(user_creds) + + # Remove the LDAP connection. + del type(as_req_base)._ldb + + if expect_error: + expected_error_modes = (KDC_ERR_CLIENT_REVOKED, + KDC_ERR_PREAUTH_FAILED) + + # Wrap generic_check_kdc_error() to expect an NTSTATUS code when the + # account is locked out. + def check_error_fn(kdc_exchange_dict, + callback_dict, + rep): + error_code = rep.get('error-code') + if error_code == KDC_ERR_CLIENT_REVOKED: + # The account was locked out. + kdc_exchange_dict['expected_status'] = ( + ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT) + + if expect_status: + # Expect to get a LOCKED_OUT NTSTATUS code. + kdc_exchange_dict['expect_edata'] = True + kdc_exchange_dict['expect_status'] = True + + elif error_code == KDC_ERR_PREAUTH_FAILED: + # Just a wrong password: the account wasn’t locked out. Don’t + # expect an NTSTATUS code. + kdc_exchange_dict['expect_status'] = False + + # Continue with the generic error-checking logic. + return as_req_base.generic_check_kdc_error( + kdc_exchange_dict, + callback_dict, + rep) + + check_rep_fn = None + else: + expected_error_modes = 0 + + check_error_fn = None + check_rep_fn = as_req_base.generic_check_kdc_rep + + def _generate_padata_copy(_kdc_exchange_dict, + _callback_dict, + req_body): + return padata, req_body + + kdc_exchange_dict = as_req_base.as_exchange_dict( + creds=user_creds, + expected_crealm=realm, + expected_cname=cname, + expected_srealm=realm, + expected_sname=sname, + expected_account_name=user_name, + expected_supported_etypes=krbtgt_supported_etypes, + ticket_decryption_key=krbtgt_decryption_key, + generate_padata_fn=_generate_padata_copy, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=as_req_base.generic_check_kdc_private, + expected_error_mode=expected_error_modes, + expected_salt=expected_salt, + preauth_key=preauth_key, + kdc_options=str(kdc_options), + pac_request=True) + + # Indicate that we're ready. This ensures we hit the right transaction + # lock. + pipe.send_bytes(b'0') + + # Wait for the main process to take out a transaction lock. + if not pipe.poll(timeout=5): + raise AssertionError('main process failed to indicate readiness') + + # Try making a Kerberos AS-REQ to the KDC. This might fail, either due to + # the user's account being locked out or due to using the wrong password. + as_rep = as_req_base._generic_kdc_exchange(kdc_exchange_dict, + cname=cname, + realm=realm, + sname=sname, + till_time=till, + etypes=etypes) + + as_req_base.assertIsNotNone(as_rep) + + msg_type = as_rep['msg-type'] + if expect_error and msg_type != KRB_ERROR or ( + not expect_error and msg_type != KRB_AS_REP): + raise AssertionError(f'wrong message type {msg_type}') + + if not expect_error: + return ConnectionResult.SUCCESS + + error_code = as_rep['error-code'] + if error_code == KDC_ERR_CLIENT_REVOKED: + return ConnectionResult.LOCKED_OUT + elif error_code == KDC_ERR_PREAUTH_FAILED: + return ConnectionResult.WRONG_PASSWORD + else: + raise AssertionError(f'wrong error code {error_code}') + + +def connect_ntlm(pipe, + url, + hostname, + username, + password, + domain, + realm, + workstation, + dn): + user_creds = KerberosCredentials() + user_creds.set_username(username) + user_creds.set_password(password) + user_creds.set_domain(domain) + user_creds.set_workstation(workstation) + user_creds.set_kerberos_state(DONT_USE_KERBEROS) + + # Indicate that we're ready. This ensures we hit the right transaction + # lock. + pipe.send_bytes(b'0') + + # Wait for the main process to take out a transaction lock. + if not pipe.poll(timeout=5): + raise AssertionError('main process failed to indicate readiness') + + try: + # Try connecting to SamDB. This should fail, either due to our + # account being locked out or due to using the wrong password. + SamDB(url=url, + credentials=user_creds, + lp=env_loadparm()) + except ldb.LdbError as err: + num, estr = err.args + + if num != ldb.ERR_INVALID_CREDENTIALS: + raise AssertionError(f'connection raised wrong error code ' + f'({err})') + + if f'data {werror.WERR_ACCOUNT_LOCKED_OUT:x},' in estr: + return ConnectionResult.LOCKED_OUT + elif f'data {werror.WERR_LOGON_FAILURE:x},' in estr: + return ConnectionResult.WRONG_PASSWORD + else: + raise AssertionError(f'connection raised wrong error code ' + f'({estr})') + else: + return ConnectionResult.SUCCESS + + +def connect_samr(pipe, + url, + hostname, + username, + password, + domain, + realm, + workstation, + dn): + # Get the user's NT hash. + user_creds = KerberosCredentials() + user_creds.set_password(password) + nt_hash = user_creds.get_nt_hash() + + # Generate a new UTF-16 password. + new_password = generate_random_password(32, 32) + new_password = new_password.encode('utf-16le') + + # Generate the MD4 hash of the password. + new_password_md4 = md4_hash_blob(new_password) + + # Prefix the password with padding so it is 512 bytes long. + new_password_len = len(new_password) + remaining_len = 512 - new_password_len + new_password = bytes(remaining_len) + new_password + + # Append the 32-bit length of the password.. + new_password += int.to_bytes(new_password_len, + length=4, + byteorder='little') + + # Encrypt the password with RC4 and the existing NT hash. + encryptor = Cipher(algorithms.ARC4(nt_hash), + None, + default_backend()).encryptor() + new_password = encryptor.update(new_password) + + # Create a key from the MD4 hash of the new password. + key = new_password_md4[:14] + + # Encrypt the old NT hash with DES to obtain the verifier. + verifier = des_crypt_blob_16(nt_hash, key) + + server = lsa.String() + server.string = hostname + + account = lsa.String() + account.string = username + + nt_password = samr.CryptPassword() + nt_password.data = list(new_password) + + nt_verifier = samr.Password() + nt_verifier.hash = list(verifier) + + conn = samr.samr(f'ncacn_np:{hostname}[krb5,seal,smb2]') + + # Indicate that we're ready. This ensures we hit the right transaction + # lock. + pipe.send_bytes(b'0') + + # Wait for the main process to take out a transaction lock. + if not pipe.poll(timeout=5): + raise AssertionError('main process failed to indicate readiness') + + try: + # Try changing the password. This should fail, either due to our + # account being locked out or due to using the wrong password. + conn.ChangePasswordUser3(server=server, + account=account, + nt_password=nt_password, + nt_verifier=nt_verifier, + lm_change=True, + lm_password=None, + lm_verifier=None, + password3=None) + except NTSTATUSError as err: + num, estr = err.args + + if num == ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT: + return ConnectionResult.LOCKED_OUT + elif num == ntstatus.NT_STATUS_WRONG_PASSWORD: + return ConnectionResult.WRONG_PASSWORD + else: + raise AssertionError(f'pwd change raised wrong error code ' + f'({num:08X})') + else: + return ConnectionResult.SUCCESS + + +def connect_samr_aes(pipe, + url, + hostname, + username, + password, + domain, + realm, + workstation, + dn): + # Get the user's NT hash. + user_creds = KerberosCredentials() + user_creds.set_password(password) + nt_hash = user_creds.get_nt_hash() + + # Generate a new UTF-16 password. + new_password = generate_random_password(32, 32) + new_password = new_password.encode('utf-16le') + + # Prepend the 16-bit length of the password.. + new_password_len = int.to_bytes(len(new_password), + length=2, + byteorder='little') + new_password = new_password_len + new_password + + server = lsa.String() + server.string = hostname + + account = lsa.String() + account.string = username + + # Derive a key from the user's NT hash. + iv = generate_random_bytes(16) + iterations = 5555 + cek = sha512_pbkdf2(nt_hash, iv, iterations) + + enc_key_salt = (b'Microsoft SAM encryption key ' + b'AEAD-AES-256-CBC-HMAC-SHA512 16\0') + mac_key_salt = (b'Microsoft SAM MAC key ' + b'AEAD-AES-256-CBC-HMAC-SHA512 16\0') + + # Encrypt the new password. + ciphertext, auth_data = aead_aes_256_cbc_hmac_sha512_blob(new_password, + cek, + enc_key_salt, + mac_key_salt, + iv) + + # Create the new password structure + pwd_buf = samr.EncryptedPasswordAES() + pwd_buf.auth_data = list(auth_data) + pwd_buf.salt = list(iv) + pwd_buf.cipher_len = len(ciphertext) + pwd_buf.cipher = list(ciphertext) + pwd_buf.PBKDF2Iterations = iterations + + conn = samr.samr(f'ncacn_np:{hostname}[krb5,seal,smb2]') + + # Indicate that we're ready. This ensures we hit the right transaction + # lock. + pipe.send_bytes(b'0') + + # Wait for the main process to take out a transaction lock. + if not pipe.poll(timeout=5): + raise AssertionError('main process failed to indicate readiness') + + try: + # Try changing the password. This should fail, either due to our + # account being locked out or due to using the wrong password. + conn.ChangePasswordUser4(server=server, + account=account, + password=pwd_buf) + except NTSTATUSError as err: + num, estr = err.args + + if num == ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT: + return ConnectionResult.LOCKED_OUT + elif num == ntstatus.NT_STATUS_WRONG_PASSWORD: + return ConnectionResult.WRONG_PASSWORD + else: + raise AssertionError(f'pwd change raised wrong error code ' + f'({num:08X})') + else: + return ConnectionResult.SUCCESS + + +def ldap_pwd_change(pipe, + url, + hostname, + username, + password, + domain, + realm, + workstation, + dn): + lp = env_loadparm() + + admin_creds = KerberosCredentials() + admin_creds.guess(lp) + admin_creds.set_username(env_get_var_value('ADMIN_USERNAME')) + admin_creds.set_password(env_get_var_value('ADMIN_PASSWORD')) + admin_creds.set_kerberos_state(MUST_USE_KERBEROS) + + samdb = SamDB(url=url, + credentials=admin_creds, + lp=lp) + + old_utf16pw = f'"{password}"'.encode('utf-16le') + + new_password = generate_random_password(32, 32) + new_utf16pw = f'"{new_password}"'.encode('utf-16le') + + msg = ldb.Message(ldb.Dn(samdb, dn)) + msg['0'] = ldb.MessageElement(old_utf16pw, + ldb.FLAG_MOD_DELETE, + 'unicodePwd') + msg['1'] = ldb.MessageElement(new_utf16pw, + ldb.FLAG_MOD_ADD, + 'unicodePwd') + + # Indicate that we're ready. This ensures we hit the right transaction + # lock. + pipe.send_bytes(b'0') + + # Wait for the main process to take out a transaction lock. + if not pipe.poll(timeout=5): + raise AssertionError('main process failed to indicate readiness') + + # Try changing the user's password. This should fail, either due to the + # user's account being locked out or due to specifying the wrong password. + try: + samdb.modify(msg) + except ldb.LdbError as err: + num, estr = err.args + if num != ldb.ERR_CONSTRAINT_VIOLATION: + raise AssertionError(f'pwd change raised wrong error code ({err})') + + if f'<{werror.WERR_ACCOUNT_LOCKED_OUT:08X}:' in estr: + return ConnectionResult.LOCKED_OUT + elif f'<{werror.WERR_INVALID_PASSWORD:08X}:' in estr: + return ConnectionResult.WRONG_PASSWORD + else: + raise AssertionError(f'pwd change raised wrong error code ' + f'({estr})') + else: + return ConnectionResult.SUCCESS + + +class LockoutTests(KDCBaseTest): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + samdb = self.get_samdb() + base_dn = ldb.Dn(samdb, samdb.domain_dn()) + + def modify_attr(attr, value): + if value is None: + value = [] + flag = ldb.FLAG_MOD_DELETE + else: + value = str(value) + flag = ldb.FLAG_MOD_REPLACE + + msg = ldb.Message(base_dn) + msg[attr] = ldb.MessageElement( + value, flag, attr) + samdb.modify(msg) + + res = samdb.search(base_dn, + scope=ldb.SCOPE_BASE, + attrs=['lockoutDuration', + 'lockoutThreshold', + 'msDS-LogonTimeSyncInterval']) + self.assertEqual(1, len(res)) + + # Reset the lockout duration as it was before. + lockout_duration = res[0].get('lockoutDuration', idx=0) + self.addCleanup(modify_attr, 'lockoutDuration', lockout_duration) + + # Set the new lockout duration: locked out accounts now stay locked + # out. + modify_attr('lockoutDuration', 0) + + # Reset the lockout threshold as it was before. + lockout_threshold = res[0].get('lockoutThreshold', idx=0) + self.addCleanup(modify_attr, 'lockoutThreshold', lockout_threshold) + + # Set the new lockout threshold. + self.lockout_threshold = 3 + modify_attr('lockoutThreshold', self.lockout_threshold) + + # Reset the logon time sync interval as it was before. + sync_interval = res[0].get('msDS-LogonTimeSyncInterval', idx=0) + self.addCleanup(modify_attr, + 'msDS-LogonTimeSyncInterval', + sync_interval) + + # Set the new logon time sync interval. Setting it to 0 eliminates the + # need for this attribute to be updated on logon, and thus the + # requirement to take out a transaction. + modify_attr('msDS-LogonTimeSyncInterval', 0) + + # 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 assertLocalSamDB(self, samdb): + if samdb.url.startswith('tdb://'): + return + if samdb.url.startswith('mdb://'): + return + + self.fail(f'connection to {samdb.url} is not local!') + + def wait_for_ready(self, pipe, future): + if pipe.poll(timeout=5): + return + + # We failed to read a response from the pipe, so see if the test raised + # an exception with more information. + if future.done(): + exception = future.exception(timeout=0) + if exception is not None: + raise exception + + self.fail('test failed to indicate readiness') + + def test_lockout_transaction_kdc(self): + self.do_lockout_transaction(connect_kdc) + + def test_lockout_transaction_kdc_ntstatus(self): + self.do_lockout_transaction(partial(connect_kdc, expect_status=True)) + + def test_lockout_transaction_ntlm(self): + self.do_lockout_transaction(connect_ntlm) + + def test_lockout_transaction_samr(self): + self.do_lockout_transaction(connect_samr) + + def test_lockout_transaction_samr_aes(self): + self.do_lockout_transaction(connect_samr_aes) + + def test_lockout_transaction_ldap_pw_change(self): + self.do_lockout_transaction(ldap_pwd_change) + + # Tests to ensure we can handle the account being renamed. We do not test + # renames with SAMR password changes, because in that case the entire + # process happens inside a transaction, and the password change method only + # receives the account username. By the time it searches for the account, + # it will have already been renamed, and so it will always fail to find the + # account. + + def test_lockout_transaction_rename_kdc(self): + self.do_lockout_transaction(connect_kdc, rename=True) + + def test_lockout_transaction_rename_kdc_ntstatus(self): + self.do_lockout_transaction(partial(connect_kdc, expect_status=True), + rename=True) + + def test_lockout_transaction_rename_ntlm(self): + self.do_lockout_transaction(connect_ntlm, rename=True) + + def test_lockout_transaction_rename_ldap_pw_change(self): + self.do_lockout_transaction(ldap_pwd_change, rename=True) + + def test_lockout_transaction_bad_pwd_kdc(self): + self.do_lockout_transaction(connect_kdc, correct_pw=False) + + def test_lockout_transaction_bad_pwd_kdc_ntstatus(self): + self.do_lockout_transaction(partial(connect_kdc, expect_status=True), + correct_pw=False) + + def test_lockout_transaction_bad_pwd_ntlm(self): + self.do_lockout_transaction(connect_ntlm, correct_pw=False) + + def test_lockout_transaction_bad_pwd_samr(self): + self.do_lockout_transaction(connect_samr, correct_pw=False) + + def test_lockout_transaction_bad_pwd_samr_aes(self): + self.do_lockout_transaction(connect_samr_aes, correct_pw=False) + + def test_lockout_transaction_bad_pwd_ldap_pw_change(self): + self.do_lockout_transaction(ldap_pwd_change, correct_pw=False) + + def test_bad_pwd_count_transaction_kdc(self): + self.do_bad_pwd_count_transaction(connect_kdc) + + def test_bad_pwd_count_transaction_ntlm(self): + self.do_bad_pwd_count_transaction(connect_ntlm) + + def test_bad_pwd_count_transaction_samr(self): + self.do_bad_pwd_count_transaction(connect_samr) + + def test_bad_pwd_count_transaction_samr_aes(self): + self.do_bad_pwd_count_transaction(connect_samr_aes) + + def test_bad_pwd_count_transaction_ldap_pw_change(self): + self.do_bad_pwd_count_transaction(ldap_pwd_change) + + def test_bad_pwd_count_transaction_rename_kdc(self): + self.do_bad_pwd_count_transaction(connect_kdc, rename=True) + + def test_bad_pwd_count_transaction_rename_ntlm(self): + self.do_bad_pwd_count_transaction(connect_ntlm, rename=True) + + def test_bad_pwd_count_transaction_rename_ldap_pw_change(self): + self.do_bad_pwd_count_transaction(ldap_pwd_change, rename=True) + + def test_lockout_race_kdc(self): + self.do_lockout_race(connect_kdc) + + def test_lockout_race_kdc_ntstatus(self): + self.do_lockout_race(partial(connect_kdc, expect_status=True)) + + def test_lockout_race_ntlm(self): + self.do_lockout_race(connect_ntlm) + + def test_lockout_race_samr(self): + self.do_lockout_race(connect_samr) + + def test_lockout_race_samr_aes(self): + self.do_lockout_race(connect_samr_aes) + + def test_lockout_race_ldap_pw_change(self): + self.do_lockout_race(ldap_pwd_change) + + def test_logon_without_transaction_ntlm(self): + self.do_logon_without_transaction(connect_ntlm) + + # Tests to ensure that the connection functions work correctly in the happy + # path. + + def test_logon_kdc(self): + self.do_logon(partial(connect_kdc, expect_error=False)) + + def test_logon_ntlm(self): + self.do_logon(connect_ntlm) + + def test_logon_samr(self): + self.do_logon(connect_samr) + + def test_logon_samr_aes(self): + self.do_logon(connect_samr_aes) + + def test_logon_ldap_pw_change(self): + self.do_logon(ldap_pwd_change) + + # Test that connection without a correct password works. + def do_logon(self, connect_fn): + # Create the user account for testing. + user_creds = self.get_cached_creds(account_type=self.AccountType.USER, + use_cache=False) + user_dn = user_creds.get_dn() + + admin_creds = self.get_admin_creds() + lp = self.get_lp() + + # Get a connection to our local SamDB. + samdb = connect_samdb(samdb_url=lp.samdb_url(), lp=lp, + credentials=admin_creds) + self.assertLocalSamDB(samdb) + + password = user_creds.get_password() + + # Prepare to connect to the server with a valid password. + our_pipe, their_pipe = Pipe(duplex=True) + + # Inform the test function that it may proceed. + our_pipe.send_bytes(b'0') + + result = connect_fn(pipe=their_pipe, + url=f'ldap://{samdb.host_dns_name()}', + hostname=samdb.host_dns_name(), + username=user_creds.get_username(), + password=password, + domain=user_creds.get_domain(), + realm=user_creds.get_realm(), + workstation=user_creds.get_workstation(), + dn=str(user_dn)) + + # The connection should succeed. + self.assertEqual(result, ConnectionResult.SUCCESS) + + # Lock out the account while holding a transaction lock, then release the + # lock. A logon attempt already in progress should reread the account + # details and recognise the account is locked out. The account can + # additionally be renamed within the transaction to ensure that, by using + # the GUID, rereading the account's details still succeeds. + def do_lockout_transaction(self, connect_fn, + rename=False, + correct_pw=True): + # Create the user account for testing. + user_creds = self.get_cached_creds(account_type=self.AccountType.USER, + use_cache=False) + user_dn = user_creds.get_dn() + + admin_creds = self.get_admin_creds() + lp = self.get_lp() + + # Get a connection to our local SamDB. + samdb = connect_samdb(samdb_url=lp.samdb_url(), lp=lp, + credentials=admin_creds) + self.assertLocalSamDB(samdb) + + password = user_creds.get_password() + if not correct_pw: + password = password[:-1] + + # Prepare to connect to the server. + with futures.ProcessPoolExecutor(max_workers=1) as executor: + our_pipe, their_pipe = Pipe(duplex=True) + connect_future = executor.submit( + connect_fn, + pipe=their_pipe, + url=f'ldap://{samdb.host_dns_name()}', + hostname=samdb.host_dns_name(), + username=user_creds.get_username(), + password=password, + domain=user_creds.get_domain(), + realm=user_creds.get_realm(), + workstation=user_creds.get_workstation(), + dn=str(user_dn)) + + # Wait until the test process indicates it's ready. + self.wait_for_ready(our_pipe, connect_future) + + # Take out a transaction. + samdb.transaction_start() + try: + # Lock out the account. We must do it using an actual password + # check like so, rather than directly with a database + # modification, so that the account is also added to the + # auxiliary bad password database. + + old_utf16pw = '"Secret007"'.encode('utf-16le') # invalid pwd + new_utf16pw = '"Secret008"'.encode('utf-16le') + + msg = ldb.Message(user_dn) + msg['0'] = ldb.MessageElement(old_utf16pw, + ldb.FLAG_MOD_DELETE, + 'unicodePwd') + msg['1'] = ldb.MessageElement(new_utf16pw, + ldb.FLAG_MOD_ADD, + 'unicodePwd') + + for i in range(self.lockout_threshold): + try: + samdb.modify(msg) + except ldb.LdbError as err: + num, estr = err.args + + # We get an error, but the bad password count should + # still be updated. + self.assertEqual(num, ldb.ERR_OPERATIONS_ERROR) + self.assertEqual('Failed to obtain remote address for ' + 'the LDAP client while changing the ' + 'password', + estr) + else: + self.fail('pwd change should have failed') + + # Ensure the account is locked out. + + res = samdb.search( + user_dn, scope=ldb.SCOPE_BASE, + attrs=['msDS-User-Account-Control-Computed']) + self.assertEqual(1, len(res)) + + uac = int(res[0].get('msDS-User-Account-Control-Computed', + idx=0)) + self.assertTrue(uac & dsdb.UF_LOCKOUT) + + # Now the bad password database has been updated, inform the + # test process that it may proceed. + our_pipe.send_bytes(b'0') + + # Wait one second to ensure the test process hits the + # transaction lock. + time.sleep(1) + + if rename: + # While we're at it, rename the account to ensure that is + # also safe if a race occurs. + msg = ldb.Message(user_dn) + new_username = self.get_new_username() + msg['sAMAccountName'] = ldb.MessageElement( + new_username, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + + except Exception: + samdb.transaction_cancel() + raise + + # Commit the local transaction. + samdb.transaction_commit() + + result = connect_future.result(timeout=5) + self.assertEqual(result, ConnectionResult.LOCKED_OUT) + + # Update the bad password count while holding a transaction lock, then + # release the lock. A logon attempt already in progress should reread the + # account details and ensure the bad password count is atomically + # updated. The account can additionally be renamed within the transaction + # to ensure that, by using the GUID, rereading the account's details still + # succeeds. + def do_bad_pwd_count_transaction(self, connect_fn, rename=False): + # Create the user account for testing. + user_creds = self.get_cached_creds(account_type=self.AccountType.USER, + use_cache=False) + user_dn = user_creds.get_dn() + + admin_creds = self.get_admin_creds() + lp = self.get_lp() + + # Get a connection to our local SamDB. + samdb = connect_samdb(samdb_url=lp.samdb_url(), lp=lp, + credentials=admin_creds) + self.assertLocalSamDB(samdb) + + # Prepare to connect to the server with an invalid password. + with futures.ProcessPoolExecutor(max_workers=1) as executor: + our_pipe, their_pipe = Pipe(duplex=True) + connect_future = executor.submit( + connect_fn, + pipe=their_pipe, + url=f'ldap://{samdb.host_dns_name()}', + hostname=samdb.host_dns_name(), + username=user_creds.get_username(), + password=user_creds.get_password()[:-1], # invalid password + domain=user_creds.get_domain(), + realm=user_creds.get_realm(), + workstation=user_creds.get_workstation(), + dn=str(user_dn)) + + # Wait until the test process indicates it's ready. + self.wait_for_ready(our_pipe, connect_future) + + # Take out a transaction. + samdb.transaction_start() + try: + # Inform the test process that it may proceed. + our_pipe.send_bytes(b'0') + + # Wait one second to ensure the test process hits the + # transaction lock. + time.sleep(1) + + # Set badPwdCount to 1. + msg = ldb.Message(user_dn) + now = int(time.time()) + bad_pwd_time = unix2nttime(now) + msg['badPwdCount'] = ldb.MessageElement( + '1', + ldb.FLAG_MOD_REPLACE, + 'badPwdCount') + msg['badPasswordTime'] = ldb.MessageElement( + str(bad_pwd_time), + ldb.FLAG_MOD_REPLACE, + 'badPasswordTime') + if rename: + # While we're at it, rename the account to ensure that is + # also safe if a race occurs. + new_username = self.get_new_username() + msg['sAMAccountName'] = ldb.MessageElement( + new_username, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + + # Ensure the account is not yet locked out. + + res = samdb.search( + user_dn, scope=ldb.SCOPE_BASE, + attrs=['msDS-User-Account-Control-Computed']) + self.assertEqual(1, len(res)) + + uac = int(res[0].get('msDS-User-Account-Control-Computed', + idx=0)) + self.assertFalse(uac & dsdb.UF_LOCKOUT) + except Exception: + samdb.transaction_cancel() + raise + + # Commit the local transaction. + samdb.transaction_commit() + + result = connect_future.result(timeout=5) + self.assertEqual(result, ConnectionResult.WRONG_PASSWORD, result) + + # Check that badPwdCount has now increased to 2. + + res = samdb.search(user_dn, + scope=ldb.SCOPE_BASE, + attrs=['badPwdCount']) + self.assertEqual(1, len(res)) + + bad_pwd_count = int(res[0].get('badPwdCount', idx=0)) + self.assertEqual(2, bad_pwd_count) + + # Attempt to log in to the account with an incorrect password, using + # lockoutThreshold+1 simultaneous attempts. We should get three 'wrong + # password' errors and one 'locked out' error, showing that the bad + # password count is checked and incremented atomically. + def do_lockout_race(self, connect_fn): + # Create the user account for testing. + user_creds = self.get_cached_creds(account_type=self.AccountType.USER, + use_cache=False) + user_dn = user_creds.get_dn() + + admin_creds = self.get_admin_creds() + lp = self.get_lp() + + # Get a connection to our local SamDB. + samdb = connect_samdb(samdb_url=lp.samdb_url(), lp=lp, + credentials=admin_creds) + self.assertLocalSamDB(samdb) + + # Prepare to connect to the server with an invalid password, using four + # simultaneous requests. Only three of those attempts should get + # through before the account is locked out. + num_attempts = self.lockout_threshold + 1 + with futures.ProcessPoolExecutor(max_workers=num_attempts) as executor: + connect_futures = [] + our_pipes = [] + for i in range(num_attempts): + our_pipe, their_pipe = Pipe(duplex=True) + our_pipes.append(our_pipe) + + connect_future = executor.submit( + connect_fn, + pipe=their_pipe, + url=f'ldap://{samdb.host_dns_name()}', + hostname=samdb.host_dns_name(), + username=user_creds.get_username(), + password=user_creds.get_password()[:-1], # invalid pw + domain=user_creds.get_domain(), + realm=user_creds.get_realm(), + workstation=user_creds.get_workstation(), + dn=str(user_dn)) + connect_futures.append(connect_future) + + # Wait until the test process indicates it's ready. + self.wait_for_ready(our_pipe, connect_future) + + # Take out a transaction. + samdb.transaction_start() + try: + # Inform the test processes that they may proceed. + for our_pipe in our_pipes: + our_pipe.send_bytes(b'0') + + # Wait one second to ensure the test processes hit the + # transaction lock. + time.sleep(1) + except Exception: + samdb.transaction_cancel() + raise + + # Commit the local transaction. + samdb.transaction_commit() + + lockouts = 0 + wrong_passwords = 0 + for i, connect_future in enumerate(connect_futures): + result = connect_future.result(timeout=5) + if result == ConnectionResult.LOCKED_OUT: + lockouts += 1 + elif result == ConnectionResult.WRONG_PASSWORD: + wrong_passwords += 1 + else: + self.fail(f'process {i} gave an unexpected result ' + f'{result}') + + self.assertEqual(wrong_passwords, self.lockout_threshold) + self.assertEqual(lockouts, num_attempts - self.lockout_threshold) + + # Ensure the account is now locked out. + + res = samdb.search( + user_dn, scope=ldb.SCOPE_BASE, + attrs=['badPwdCount', + 'msDS-User-Account-Control-Computed']) + self.assertEqual(1, len(res)) + + bad_pwd_count = int(res[0].get('badPwdCount', idx=0)) + self.assertEqual(self.lockout_threshold, bad_pwd_count) + + uac = int(res[0].get('msDS-User-Account-Control-Computed', + idx=0)) + self.assertTrue(uac & dsdb.UF_LOCKOUT) + + # Test that logon is possible even while we locally hold a transaction + # lock. This test only works with NTLM authentication; Kerberos + # authentication must take out a transaction to update the logonCount + # attribute, and LDAP and SAMR password changes both take out a transaction + # to effect the password change. NTLM is the only logon method that does + # not require a transaction, and can thus be performed while we're holding + # the lock. + def do_logon_without_transaction(self, connect_fn): + # Create the user account for testing. + user_creds = self.get_cached_creds(account_type=self.AccountType.USER, + use_cache=False) + user_dn = user_creds.get_dn() + + admin_creds = self.get_admin_creds() + lp = self.get_lp() + + # Get a connection to our local SamDB. + samdb = connect_samdb(samdb_url=lp.samdb_url(), lp=lp, + credentials=admin_creds) + self.assertLocalSamDB(samdb) + + password = user_creds.get_password() + + # Prepare to connect to the server with a valid password. + with futures.ProcessPoolExecutor(max_workers=1) as executor: + our_pipe, their_pipe = Pipe(duplex=True) + connect_future = executor.submit( + connect_fn, + pipe=their_pipe, + url=f'ldap://{samdb.host_dns_name()}', + hostname=samdb.host_dns_name(), + username=user_creds.get_username(), + password=password, + domain=user_creds.get_domain(), + realm=user_creds.get_realm(), + workstation=user_creds.get_workstation(), + dn=str(user_dn)) + + # Wait until the test process indicates it's ready. + self.wait_for_ready(our_pipe, connect_future) + + # Take out a transaction. + samdb.transaction_start() + try: + # Inform the test process that it may proceed. + our_pipe.send_bytes(b'0') + + # The connection should succeed, despite our holding a + # transaction. + result = connect_future.result(timeout=5) + self.assertEqual(result, ConnectionResult.SUCCESS) + except Exception: + samdb.transaction_cancel() + raise + + # Commit the local transaction. + samdb.transaction_commit() + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/ms_kile_client_principal_lookup_tests.py b/python/samba/tests/krb5/ms_kile_client_principal_lookup_tests.py new file mode 100755 index 0000000..4feb3bb --- /dev/null +++ b/python/samba/tests/krb5/ms_kile_client_principal_lookup_tests.py @@ -0,0 +1,818 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2020 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.dsdb import UF_DONT_REQUIRE_PREAUTH +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + NT_ENTERPRISE_PRINCIPAL, + NT_PRINCIPAL, + NT_SRV_INST, + KDC_ERR_C_PRINCIPAL_UNKNOWN, + KDC_ERR_TGT_REVOKED, +) + +global_asn1_print = False +global_hexdump = False + + +class MS_Kile_Client_Principal_Lookup_Tests(KDCBaseTest): + """ Tests for MS-KILE client principal look-up + See [MS-KILE]: Kerberos Protocol Extensions + section 3.3.5.6.1 Client Principal Lookup + """ + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def check_pac(self, samdb, auth_data, uc, name, upn=None): + + pac_data = self.get_pac_data(auth_data) + if upn is None: + upn = "%s@%s" % (name, uc.get_realm().lower()) + if name.endswith('$'): + name = name[:-1] + + self.assertEqual( + uc.get_username(), + str(pac_data.account_name), + "pac_data = {%s}" % str(pac_data)) + self.assertEqual( + name, + pac_data.logon_name, + "pac_data = {%s}" % str(pac_data)) + self.assertEqual( + uc.get_realm(), + pac_data.domain_name, + "pac_data = {%s}" % str(pac_data)) + self.assertEqual( + upn, + pac_data.upn, + "pac_data = {%s}" % str(pac_data)) + self.assertEqual( + uc.get_sid(), + pac_data.account_sid, + "pac_data = {%s}" % str(pac_data)) + + def test_nt_principal_step_1(self): + """ Step 1 + For an NT_PRINCIPAL cname with no realm or the realm matches the + DC's domain + search for an account with the + sAMAccountName matching the cname. + """ + + # Create user and machine accounts for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + (uc, dn) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[user_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the pac, and the ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + self.check_pac(samdb, enc_part['authorization-data'], uc, user_name) + # check the crealm and cname + cname = enc_part['cname'] + self.assertEqual(NT_PRINCIPAL, cname['name-type']) + self.assertEqual(user_name.encode('UTF8'), cname['name-string'][0]) + self.assertEqual(realm.upper().encode('UTF8'), enc_part['crealm']) + + def test_nt_principal_step_2(self): + """ Step 2 + If not found + search for sAMAccountName equal to the cname + "$" + + """ + + # Create a machine account for the test. + # + samdb = self.get_samdb() + mach_name = "mskilemac" + (mc, dn) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + realm = mc.get_realm().lower() + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[mach_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(mc, rep) + key = self.get_as_rep_key(mc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mach_name]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, mc.get_realm(), ticket, key, etype, + creds=mc, service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the pac, and the ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + self.check_pac(samdb, enc_part['authorization-data'], mc, mach_name + '$') + # check the crealm and cname + cname = enc_part['cname'] + self.assertEqual(NT_PRINCIPAL, cname['name-type']) + self.assertEqual(mach_name.encode('UTF8'), cname['name-string'][0]) + self.assertEqual(realm.upper().encode('UTF8'), enc_part['crealm']) + + def test_nt_principal_step_3(self): + """ Step 3 + + If not found + search for a matching UPN name where the UPN is set to + cname@realm or cname@DC's domain name + + """ + # Create a user account for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + upn_name = "mskileupn" + upn = upn_name + "@" + self.get_user_creds().get_realm().lower() + (uc, dn) = self.create_account(samdb, user_name, upn=upn) + realm = uc.get_realm().lower() + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[upn_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[upn_name]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the service ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + self.check_pac(samdb, enc_part['authorization-data'], uc, upn_name) + # check the crealm and cname + cname = enc_part['cname'] + self.assertEqual(NT_PRINCIPAL, cname['name-type']) + self.assertEqual(upn_name.encode('UTF8'), cname['name-string'][0]) + self.assertEqual(realm.upper().encode('UTF8'), enc_part['crealm']) + + def test_nt_principal_step_4_a(self): + """ Step 4, no pre-authentication + If not found and no pre-authentication + search for a matching altSecurityIdentity + """ + # Create a user account for the test. + # with an altSecurityIdentity, and with UF_DONT_REQUIRE_PREAUTH + # set. + # + # note that in this case IDL_DRSCrackNames is called with + # pmsgIn.formatOffered set to + # DS_USER_PRINCIPAL_NAME_AND_ALTSECID + # + # setting UF_DONT_REQUIRE_PREAUTH seems to be the only way + # to trigger the no pre-auth step + + samdb = self.get_samdb() + user_name = "mskileusr" + alt_name = "mskilealtsec" + (uc, dn) = self.create_account(samdb, user_name, + account_control=UF_DONT_REQUIRE_PREAUTH) + realm = uc.get_realm().lower() + alt_sec = "Kerberos:%s@%s" % (alt_name, realm) + self.add_attribute(samdb, dn, "altSecurityIdentities", alt_sec) + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, as we've set UF_DONT_REQUIRE_PREAUTH + # we should get a valid AS-RESP + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[alt_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_as_reply(rep) + salt = "%s%s" % (realm.upper(), user_name) + key = self.PasswordKey_create( + rep['enc-part']['etype'], + uc.get_password(), + salt.encode('UTF8'), + rep['enc-part']['kvno']) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[alt_name]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc, expect_pac=False, + expect_edata=False, + expected_error_mode=KDC_ERR_TGT_REVOKED) + self.check_error_rep(rep, KDC_ERR_TGT_REVOKED) + + def test_nt_principal_step_4_b(self): + """ Step 4, pre-authentication + If not found and pre-authentication + search for a matching user principal name + """ + + # Create user and machine accounts for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + alt_name = "mskilealtsec" + (uc, dn) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + alt_sec = "Kerberos:%s@%s" % (alt_name, realm) + self.add_attribute(samdb, dn, "altSecurityIdentities", alt_sec) + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[alt_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + # Note: although we used the alt security id for the pre-auth + # we need to use the username for the auth + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[user_name]) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[user_name]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the pac, and the ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + self.check_pac(samdb, + enc_part['authorization-data'], uc, user_name) + # check the crealm and cname + cname = enc_part['cname'] + self.assertEqual(NT_PRINCIPAL, cname['name-type']) + self.assertEqual(user_name.encode('UTF8'), cname['name-string'][0]) + self.assertEqual(realm.upper().encode('UTF8'), enc_part['crealm']) + + def test_nt_principal_step_4_c(self): + """ Step 4, pre-authentication + If not found and pre-authentication + search for a matching user principal name + + This test uses the altsecid, so the AS-REQ should fail. + """ + + # Create user and machine accounts for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + alt_name = "mskilealtsec" + (uc, dn) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + alt_sec = "Kerberos:%s@%s" % (alt_name, realm) + self.add_attribute(samdb, dn, "altSecurityIdentities", alt_sec) + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[alt_name]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + # Use the alternate security identifier + # this should fail + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[alt_sec]) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_error_rep(rep, KDC_ERR_C_PRINCIPAL_UNKNOWN) + + def test_enterprise_principal_step_1_3(self): + """ Steps 1-3 + For an NT_ENTERPRISE_PRINCIPAL cname + search for a user principal name matching the cname + + """ + + # Create a user account for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + upn_name = "mskileupn" + upn = upn_name + "@" + self.get_user_creds().get_realm().lower() + (uc, dn) = self.create_account(samdb, user_name, upn=upn) + realm = uc.get_realm().lower() + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[upn]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[upn]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the pac, and the ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + self.check_pac( + samdb, enc_part['authorization-data'], uc, upn, upn=upn) + # check the crealm and cname + cname = enc_part['cname'] + crealm = enc_part['crealm'] + self.assertEqual(NT_ENTERPRISE_PRINCIPAL, cname['name-type']) + self.assertEqual(upn.encode('UTF8'), cname['name-string'][0]) + self.assertEqual(realm.upper().encode('UTF8'), crealm) + + def test_enterprise_principal_step_4(self): + """ Step 4 + + If that fails + search for an account where the sAMAccountName matches + the name before the @ + + """ + + # Create a user account for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + (uc, dn) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + ename = user_name + "@" + realm + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the pac, and the ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + self.check_pac( + samdb, enc_part['authorization-data'], uc, ename, upn=ename) + # check the crealm and cname + cname = enc_part['cname'] + crealm = enc_part['crealm'] + self.assertEqual(NT_ENTERPRISE_PRINCIPAL, cname['name-type']) + self.assertEqual(ename.encode('UTF8'), cname['name-string'][0]) + self.assertEqual(realm.upper().encode('UTF8'), crealm) + + def test_enterprise_principal_step_5(self): + """ Step 5 + + If that fails + search for an account where the sAMAccountName matches + the name before the @ with a $ appended. + + """ + + # Create a user account for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + (uc, _) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + + mach_name = "mskilemac" + (mc, dn) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + ename = mach_name + "@" + realm + uname = mach_name + "$@" + realm + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(mc, rep) + key = self.get_as_rep_key(mc, rep) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the pac, and the ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + self.check_pac( + samdb, enc_part['authorization-data'], mc, ename, upn=uname) + # check the crealm and cname + cname = enc_part['cname'] + crealm = enc_part['crealm'] + self.assertEqual(NT_ENTERPRISE_PRINCIPAL, cname['name-type']) + self.assertEqual(ename.encode('UTF8'), cname['name-string'][0]) + self.assertEqual(realm.upper().encode('UTF8'), crealm) + + def test_enterprise_principal_step_6_a(self): + """ Step 6, no pre-authentication + If not found and no pre-authentication + search for a matching altSecurityIdentity + """ + # Create a user account for the test. + # with an altSecurityIdentity, and with UF_DONT_REQUIRE_PREAUTH + # set. + # + # note that in this case IDL_DRSCrackNames is called with + # pmsgIn.formatOffered set to + # DS_USER_PRINCIPAL_NAME_AND_ALTSECID + # + # setting UF_DONT_REQUIRE_PREAUTH seems to be the only way + # to trigger the no pre-auth step + + samdb = self.get_samdb() + user_name = "mskileusr" + alt_name = "mskilealtsec" + (uc, dn) = self.create_account(samdb, user_name, + account_control=UF_DONT_REQUIRE_PREAUTH) + realm = uc.get_realm().lower() + alt_sec = "Kerberos:%s@%s" % (alt_name, realm) + self.add_attribute(samdb, dn, "altSecurityIdentities", alt_sec) + ename = alt_name + "@" + realm + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, as we've set UF_DONT_REQUIRE_PREAUTH + # we should get a valid AS-RESP + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_as_reply(rep) + salt = "%s%s" % (realm.upper(), user_name) + key = self.PasswordKey_create( + rep['enc-part']['etype'], + uc.get_password(), + salt.encode('UTF8'), + rep['enc-part']['kvno']) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc, expect_pac=False, + expect_edata=False, + expected_error_mode=KDC_ERR_TGT_REVOKED) + self.check_error_rep(rep, KDC_ERR_TGT_REVOKED) + + def test_nt_enterprise_principal_step_6_b(self): + """ Step 4, pre-authentication + If not found and pre-authentication + search for a matching user principal name + """ + + # Create user and machine accounts for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + alt_name = "mskilealtsec" + (uc, dn) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + alt_sec = "Kerberos:%s@%s" % (alt_name, realm) + self.add_attribute(samdb, dn, "altSecurityIdentities", alt_sec) + ename = alt_name + "@" + realm + uname = user_name + "@" + realm + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + key = self.get_as_rep_key(uc, rep) + # Note: although we used the alt security id for the pre-auth + # we need to use the username for the auth + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[uname]) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_as_reply(rep) + + # Request a ticket to the host service on the machine account + ticket = rep['ticket'] + enc_part2 = self.get_as_rep_enc_data(key, rep) + key = self.EncryptionKey_import(enc_part2['key']) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, + names=[uname]) + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=[mc.get_username()]) + + (rep, enc_part) = self.tgs_req( + cname, sname, uc.get_realm(), ticket, key, etype, + creds=uc, service_creds=mc) + self.check_tgs_reply(rep) + + # Check the contents of the pac, and the ticket + ticket = rep['ticket'] + enc_part = self.decode_service_ticket(mc, ticket) + self.check_pac( + samdb, enc_part['authorization-data'], uc, uname, upn=uname) + # check the crealm and cname + cname = enc_part['cname'] + self.assertEqual(NT_ENTERPRISE_PRINCIPAL, cname['name-type']) + self.assertEqual(uname.encode('UTF8'), cname['name-string'][0]) + self.assertEqual(realm.upper().encode('UTF8'), enc_part['crealm']) + + def test_nt_principal_step_6_c(self): + """ Step 4, pre-authentication + If not found and pre-authentication + search for a matching user principal name + + This test uses the altsecid, so the AS-REQ should fail. + """ + + # Create user and machine accounts for the test. + # + samdb = self.get_samdb() + user_name = "mskileusr" + alt_name = "mskilealtsec" + (uc, dn) = self.create_account(samdb, user_name) + realm = uc.get_realm().lower() + alt_sec = "Kerberos:%s@%s" % (alt_name, realm) + self.add_attribute(samdb, dn, "altSecurityIdentities", alt_sec) + ename = alt_name + "@" + realm + + mach_name = "mskilemac" + (mc, _) = self.create_account(samdb, mach_name, + account_type=self.AccountType.COMPUTER) + + # Do the initial AS-REQ, should get a pre-authentication required + # response + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=["krbtgt", realm]) + + rep = self.as_req(cname, sname, realm, etype) + self.check_pre_authentication(rep) + + # Do the next AS-REQ + padata = self.get_enc_timestamp_pa_data(uc, rep) + # Use the alternate security identifier + # this should fail + cname = self.PrincipalName_create( + name_type=NT_ENTERPRISE_PRINCIPAL, names=[ename]) + rep = self.as_req(cname, sname, realm, etype, padata=[padata]) + self.check_error_rep(rep, KDC_ERR_C_PRINCIPAL_UNKNOWN) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/nt_hash_tests.py b/python/samba/tests/krb5/nt_hash_tests.py new file mode 100755 index 0000000..82d9c09 --- /dev/null +++ b/python/samba/tests/krb5/nt_hash_tests.py @@ -0,0 +1,142 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +import ldb + +from samba import generate_random_password, net +from samba.dcerpc import drsuapi, misc + +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +class NtHashTests(KDCBaseTest): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _check_nt_hash(self, dn, history_len): + expect_nt_hash = bool(int(os.environ.get('EXPECT_NT_HASH', '1'))) + + samdb = self.get_samdb() + admin_creds = self.get_admin_creds() + + bind, identifier, attributes = self.get_secrets( + dn, + destination_dsa_guid=misc.GUID(samdb.get_ntds_GUID()), + source_dsa_invocation_id=misc.GUID()) + + rid = identifier.sid.split()[1] + + net_ctx = net.Net(admin_creds) + + def num_hashes(attr): + if attr.value_ctr.values is None: + return 0 + + net_ctx.replicate_decrypt(bind, attr, rid) + + length = sum(len(value.blob) for value in attr.value_ctr.values) + self.assertEqual(0, length & 0xf) + return length // 16 + + def is_unicodePwd(attr): + return attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd + + def is_ntPwdHistory(attr): + return attr.attid == drsuapi.DRSUAPI_ATTID_ntPwdHistory + + unicode_pwd_count = sum(attr.value_ctr.num_values + for attr in filter(is_unicodePwd, attributes)) + + nt_history_count = sum(num_hashes(attr) + for attr in filter(is_ntPwdHistory, attributes)) + + if expect_nt_hash: + self.assertEqual(1, unicode_pwd_count, + 'expected to find NT hash') + else: + self.assertEqual(0, unicode_pwd_count, + 'got unexpected NT hash') + + if expect_nt_hash: + self.assertEqual(history_len, nt_history_count, + 'expected to find NT password history') + else: + self.assertEqual(0, nt_history_count, + 'got unexpected NT password history') + + # Test that the NT hash and its history is not generated or stored for an + # account when we disable NTLM authentication. + def test_nt_hash(self): + samdb = self.get_samdb() + user_name = self.get_new_username() + + client_creds, client_dn = self.create_account( + samdb, user_name, + account_type=KDCBaseTest.AccountType.USER) + + self._check_nt_hash(client_dn, history_len=1) + + # Change the password and check that the NT hash is still not present. + + # 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') + + old_utf16pw = f'"{client_creds.get_password()}"'.encode('utf-16-le') + + history_len = 3 + for _ in range(history_len - 1): + password = generate_random_password(32, 32) + utf16pw = f'"{password}"'.encode('utf-16-le') + + msg = ldb.Message(ldb.Dn(samdb, client_dn)) + msg['0'] = ldb.MessageElement(old_utf16pw, + ldb.FLAG_MOD_DELETE, + 'unicodePwd') + msg['1'] = ldb.MessageElement(utf16pw, + ldb.FLAG_MOD_ADD, + 'unicodePwd') + samdb.modify(msg) + + old_utf16pw = utf16pw + + self._check_nt_hash(client_dn, history_len) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/pac_align_tests.py b/python/samba/tests/krb5/pac_align_tests.py new file mode 100755 index 0000000..ae63596 --- /dev/null +++ b/python/samba/tests/krb5/pac_align_tests.py @@ -0,0 +1,93 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from samba.dcerpc import krb5pac +from samba.ndr import ndr_unpack +from samba.tests import DynamicTestCase +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +@DynamicTestCase +class PacAlignTests(KDCBaseTest): + + base_name = 'krbpac' + + @classmethod + def setUpDynamicTestCases(cls): + for length in range(len(cls.base_name), 21): + cls.generate_dynamic_test('test_pac_align', + f'{length}_chars', + length) + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _test_pac_align_with_args(self, length): + samdb = self.get_samdb() + + account_name = self.base_name + 'a' * (length - len(self.base_name)) + creds, _ = self.create_account(samdb, account_name) + + tgt = self.get_tgt(creds, expect_pac=True) + + pac_data = self.get_ticket_pac(tgt) + self.assertIsNotNone(pac_data) + + self.assertEqual(0, len(pac_data) & 7) + + pac = ndr_unpack(krb5pac.PAC_DATA_RAW, pac_data) + for pac_buffer in pac.buffers: + buffer_type = pac_buffer.type + buffer_size = pac_buffer.ndr_size + + with self.subTest(buffer_type=buffer_type): + if buffer_type == krb5pac.PAC_TYPE_LOGON_NAME: + self.assertEqual(length * 2 + 10, buffer_size) + elif buffer_type == krb5pac.PAC_TYPE_REQUESTER_SID: + self.assertEqual(28, buffer_size) + elif buffer_type in {krb5pac.PAC_TYPE_SRV_CHECKSUM, + krb5pac.PAC_TYPE_KDC_CHECKSUM, + krb5pac.PAC_TYPE_TICKET_CHECKSUM}: + self.assertEqual(0, buffer_size & 3, + f'buffer type was: {buffer_type}, ' + f'buffer size was: {buffer_size}') + else: + self.assertEqual(0, buffer_size & 7, + f'buffer type was: {buffer_type}, ' + f'buffer size was: {buffer_size}') + + rounded_len = (buffer_size + 7) & ~7 + self.assertEqual(rounded_len, len(pac_buffer.info.remaining)) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/pkinit_tests.py b/python/samba/tests/krb5/pkinit_tests.py new file mode 100755 index 0000000..3d47c79 --- /dev/null +++ b/python/samba/tests/krb5/pkinit_tests.py @@ -0,0 +1,1211 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) Catalyst.Net Ltd 2023 +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from datetime import datetime, timedelta + +from pyasn1.type import univ + +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import pkcs12 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import dh, padding +from cryptography.x509.oid import NameOID + +import samba.tests +from samba.tests.krb5 import kcrypto +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.raw_testcase import PkInit +from samba.tests.krb5.rfc4120_constants import ( + DES_EDE3_CBC, + KDC_ERR_CLIENT_NOT_TRUSTED, + KDC_ERR_ETYPE_NOSUPP, + KDC_ERR_MODIFIED, + KDC_ERR_PREAUTH_EXPIRED, + KDC_ERR_PREAUTH_FAILED, + KDC_ERR_PREAUTH_REQUIRED, + KU_PA_ENC_TIMESTAMP, + NT_PRINCIPAL, + PADATA_AS_FRESHNESS, + PADATA_ENC_TIMESTAMP, + PADATA_PK_AS_REP_19, + PADATA_PK_AS_REQ, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +global_asn1_print = False +global_hexdump = False + + +class PkInitTests(KDCBaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _get_creds(self, account_type=KDCBaseTest.AccountType.USER): + """Return credentials with an account having a UPN for performing + PK-INIT.""" + samdb = self.get_samdb() + realm = samdb.domain_dns_name().upper() + + return self.get_cached_creds( + account_type=account_type, + opts={'upn': f'{{account}}.{realm}@{realm}'}) + + def test_pkinit_no_des3(self): + """Test public-key PK-INIT without specifying the DES3 encryption + type. It should fail.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + etypes=(kcrypto.Enctype.AES256, kcrypto.Enctype.RC4), + expect_error=KDC_ERR_ETYPE_NOSUPP) + + def test_pkinit_no_des3_dh(self): + """Test Diffie-Hellman PK-INIT without specifying the DES3 encryption + type. This time, it should succeed.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + etypes=(kcrypto.Enctype.AES256, kcrypto.Enctype.RC4)) + + def test_pkinit_aes128(self): + """Test public-key PK-INIT, specifying the AES128 encryption type + first.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + etypes=( + kcrypto.Enctype.AES128, + kcrypto.Enctype.AES256, + DES_EDE3_CBC, + )) + + def test_pkinit_rc4(self): + """Test public-key PK-INIT, specifying the RC4 encryption type first. + """ + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + etypes=( + kcrypto.Enctype.RC4, + kcrypto.Enctype.AES256, + DES_EDE3_CBC, + )) + + def test_pkinit_zero_nonce(self): + """Test public-key PK-INIT with a nonce of zero. The nonce in the + request body should take precedence.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, pk_nonce=0) + + def test_pkinit_zero_nonce_dh(self): + """Test Diffie-Hellman PK-INIT with a nonce of zero. The nonce in the + request body should take precedence. + """ + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + pk_nonce=0) + + def test_pkinit_computer(self): + """Test public-key PK-INIT with a computer account.""" + client_creds = self._get_creds(self.AccountType.COMPUTER) + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds) + + def test_pkinit_computer_dh(self): + """Test Diffie-Hellman PK-INIT with a computer account.""" + client_creds = self._get_creds(self.AccountType.COMPUTER) + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN) + + def test_pkinit_computer_win2k(self): + """Test public-key Windows 2000 PK-INIT with a computer account.""" + client_creds = self._get_creds(self.AccountType.COMPUTER) + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, win2k_variant=True) + + def test_pkinit_service(self): + """Test public-key PK-INIT with a service account.""" + client_creds = self._get_creds(self.AccountType.MANAGED_SERVICE) + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds) + + def test_pkinit_service_dh(self): + """Test Diffie-Hellman PK-INIT with a service account.""" + client_creds = self._get_creds(self.AccountType.MANAGED_SERVICE) + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN) + + def test_pkinit_service_win2k(self): + """Test public-key Windows 2000 PK-INIT with a service account.""" + client_creds = self._get_creds(self.AccountType.MANAGED_SERVICE) + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, win2k_variant=True) + + def test_pkinit_no_supported_cms_types(self): + """Test public-key PK-INIT, excluding the supportedCmsTypes field. This + causes Windows to reply with differently-encoded ASN.1.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + supported_cms_types=False) + + def test_pkinit_no_supported_cms_types_dh(self): + """Test Diffie-Hellman PK-INIT, excluding the supportedCmsTypes field. + """ + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + supported_cms_types=False) + + def test_pkinit_empty_supported_cms_types(self): + """Test public-key PK-INIT with an empty supportedCmsTypes field.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + supported_cms_types=[]) + + def test_pkinit_empty_supported_cms_types_dh(self): + """Test Diffie-Hellman PK-INIT with an empty supportedCmsTypes field. + """ + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + supported_cms_types=[]) + + def test_pkinit_sha256_signature(self): + """Test public-key PK-INIT with a SHA256 signature.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req( + client_creds, target_creds, + signature_algorithm=krb5_asn1.id_pkcs1_sha256WithRSAEncryption) + + def test_pkinit_sha256_signature_dh(self): + """Test Diffie-Hellman PK-INIT with a SHA256 signature.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req( + client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + signature_algorithm=krb5_asn1.id_pkcs1_sha256WithRSAEncryption) + + def test_pkinit_sha256_signature_win2k(self): + """Test public-key Windows 2000 PK-INIT with a SHA256 signature.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req( + client_creds, target_creds, + signature_algorithm=krb5_asn1.id_pkcs1_sha256WithRSAEncryption, + win2k_variant=True) + + def test_pkinit_sha256_certificate_signature(self): + """Test public-key PK-INIT with a SHA256 certificate signature.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req( + client_creds, target_creds, + certificate_signature=hashes.SHA256) + + def test_pkinit_sha256_certificate_signature_dh(self): + """Test Diffie-Hellman PK-INIT with a SHA256 certificate signature.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req( + client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + certificate_signature=hashes.SHA256) + + def test_pkinit_sha256_certificate_signature_win2k(self): + """Test public-key Windows 2000 PK-INIT with a SHA256 certificate + signature.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + self._pkinit_req( + client_creds, target_creds, + certificate_signature=hashes.SHA256, + win2k_variant=True) + + def test_pkinit_freshness(self): + """Test public-key PK-INIT with the PKINIT Freshness Extension.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + # Perform the AS-REQ to get the freshness token. + kdc_exchange_dict = self._as_req(client_creds, target_creds, + freshness=b'', + expect_error=KDC_ERR_PREAUTH_REQUIRED, + expect_edata=True) + freshness_token = kdc_exchange_dict.get('freshness_token') + self.assertIsNotNone(freshness_token) + + # Include the freshness token in the PK-INIT request. + self._pkinit_req(client_creds, target_creds, + freshness_token=freshness_token) + + def test_pkinit_freshness_dh(self): + """Test Diffie-Hellman PK-INIT with the PKINIT Freshness Extension.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + kdc_exchange_dict = self._as_req(client_creds, target_creds, + freshness=b'', + expect_error=KDC_ERR_PREAUTH_REQUIRED, + expect_edata=True) + freshness_token = kdc_exchange_dict.get('freshness_token') + self.assertIsNotNone(freshness_token) + + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + freshness_token=freshness_token) + + def test_pkinit_freshness_non_empty(self): + """Test sending a non-empty freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + kdc_exchange_dict = self._as_req( + client_creds, target_creds, + freshness=b'A genuine freshness token', + expect_error=KDC_ERR_PREAUTH_REQUIRED, + expect_edata=True) + freshness_token = kdc_exchange_dict.get('freshness_token') + self.assertIsNotNone(freshness_token) + + def test_pkinit_freshness_with_enc_ts(self): + """Test sending a freshness token and ENC-TS in the same request.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + kdc_exchange_dict = self._as_req(client_creds, target_creds, + freshness=b'', + send_enc_ts=True) + + # There should be no freshness token in the reply. + freshness_token = kdc_exchange_dict.get('freshness_token') + self.assertIsNone(freshness_token) + + def test_pkinit_freshness_current(self): + """Test public-key PK-INIT with an up-to-date freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + freshness_token = self.create_freshness_token() + + self._pkinit_req(client_creds, target_creds, + freshness_token=freshness_token) + + def test_pkinit_freshness_current_dh(self): + """Test Diffie-Hellman PK-INIT with an up-to-date freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + freshness_token = self.create_freshness_token() + + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + freshness_token=freshness_token) + + def test_pkinit_freshness_old(self): + """Test public-key PK-INIT with an old freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + # Present a freshness token from fifteen minutes in the past. + fifteen_minutes = timedelta(minutes=15).total_seconds() + freshness_token = self.create_freshness_token(offset=-fifteen_minutes) + + # The request should be rejected. + self._pkinit_req(client_creds, target_creds, + freshness_token=freshness_token, + expect_error=KDC_ERR_PREAUTH_EXPIRED) + + def test_pkinit_freshness_old_dh(self): + """Test Diffie-Hellman PK-INIT with an old freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + # Present a freshness token from fifteen minutes in the past. + fifteen_minutes = timedelta(minutes=15).total_seconds() + freshness_token = self.create_freshness_token(offset=-fifteen_minutes) + + # The request should be rejected. + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + freshness_token=freshness_token, + expect_error=KDC_ERR_PREAUTH_EXPIRED) + + def test_pkinit_freshness_future(self): + """Test public-key PK-INIT with a freshness token from the future.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + # Present a freshness token from fifteen minutes in the future. + fifteen_minutes = timedelta(minutes=15).total_seconds() + freshness_token = self.create_freshness_token(offset=fifteen_minutes) + + # The request should be rejected. + self._pkinit_req(client_creds, target_creds, + freshness_token=freshness_token, + expect_error=KDC_ERR_PREAUTH_EXPIRED) + + def test_pkinit_freshness_future_dh(self): + """Test Diffie-Hellman PK-INIT with a freshness token from the future. + """ + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + # Present a freshness token from fifteen minutes in the future. + fifteen_minutes = timedelta(minutes=15).total_seconds() + freshness_token = self.create_freshness_token(offset=fifteen_minutes) + + # The request should be rejected. + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + freshness_token=freshness_token, + expect_error=KDC_ERR_PREAUTH_EXPIRED) + + def test_pkinit_freshness_invalid(self): + """Test public-key PK-INIT with an invalid freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + freshness_token = b'A genuine freshness token' + + # The request should be rejected. + self._pkinit_req(client_creds, target_creds, + freshness_token=freshness_token, + expect_error=KDC_ERR_MODIFIED) + + def test_pkinit_freshness_invalid_dh(self): + """Test Diffie-Hellman PK-INIT with an invalid freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + freshness_token = b'A genuine freshness token' + + # The request should be rejected. + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + freshness_token=freshness_token, + expect_error=KDC_ERR_MODIFIED) + + def test_pkinit_freshness_rodc_ts(self): + """Test public-key PK-INIT with an RODC-issued freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + rodc_krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + freshness_token = self.create_freshness_token( + krbtgt_creds=rodc_krbtgt_creds) + + # The token should be rejected. + self._pkinit_req(client_creds, target_creds, + freshness_token=freshness_token, + expect_error=KDC_ERR_PREAUTH_FAILED) + + def test_pkinit_freshness_rodc_dh(self): + """Test Diffie-Hellman PK-INIT with an RODC-issued freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + rodc_krbtgt_creds = self.get_mock_rodc_krbtgt_creds() + freshness_token = self.create_freshness_token( + krbtgt_creds=rodc_krbtgt_creds) + + # The token should be rejected. + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + freshness_token=freshness_token, + expect_error=KDC_ERR_PREAUTH_FAILED) + + def test_pkinit_freshness_wrong_header(self): + """Test public-key PK-INIT with a modified freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + freshness_token = self.create_freshness_token() + + # Modify the leading two bytes of the freshness token. + freshness_token = b'@@' + freshness_token[2:] + + # Expect to get an error. + self._pkinit_req(client_creds, target_creds, + freshness_token=freshness_token, + expect_error=KDC_ERR_MODIFIED) + + def test_pkinit_freshness_wrong_header_dh(self): + """Test Diffie-Hellman PK-INIT with a modified freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + freshness_token = self.create_freshness_token() + + # Modify the leading two bytes of the freshness token. + freshness_token = b'@@' + freshness_token[2:] + + # Expect to get an error. + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + freshness_token=freshness_token, + expect_error=KDC_ERR_MODIFIED) + + def test_pkinit_freshness_empty(self): + """Test public-key PK-INIT with an empty freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + # Expect to get an error. + self._pkinit_req(client_creds, target_creds, + freshness_token=b'', + expect_error=KDC_ERR_MODIFIED) + + def test_pkinit_freshness_empty_dh(self): + """Test Diffie-Hellman PK-INIT with an empty freshness token.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + # Expect to get an error. + self._pkinit_req(client_creds, target_creds, + using_pkinit=PkInit.DIFFIE_HELLMAN, + freshness_token=b'', + expect_error=KDC_ERR_MODIFIED) + + def test_pkinit_revoked(self): + """Test PK-INIT with a revoked certificate.""" + client_creds = self._get_creds() + target_creds = self.get_service_creds() + + ca_cert, ca_private_key = self.get_ca_cert_and_private_key() + + certificate = self.create_certificate(client_creds, + ca_cert, + ca_private_key) + + # The initial public-key PK-INIT request should succeed. + self._pkinit_req(client_creds, target_creds, + certificate=certificate) + + # The initial Diffie-Hellman PK-INIT request should succeed. + self._pkinit_req(client_creds, target_creds, + certificate=certificate, + using_pkinit=PkInit.DIFFIE_HELLMAN) + + # Revoke the client’s certificate. + self.revoke_certificate(certificate, ca_cert, ca_private_key) + + # The subsequent public-key PK-INIT request should fail. + self._pkinit_req(client_creds, target_creds, + certificate=certificate, + expect_error=KDC_ERR_CLIENT_NOT_TRUSTED) + + # The subsequent Diffie-Hellman PK-INIT request should also fail. + self._pkinit_req(client_creds, target_creds, + certificate=certificate, + using_pkinit=PkInit.DIFFIE_HELLMAN, + expect_error=KDC_ERR_CLIENT_NOT_TRUSTED) + + def _as_req(self, + creds, + target_creds, + *, + expect_error=0, + expect_edata=False, + etypes=None, + freshness=None, + send_enc_ts=False, + ): + if send_enc_ts: + preauth_key = self.PasswordKey_from_creds(creds, kcrypto.Enctype.AES256) + else: + preauth_key = None + + if freshness is not None or send_enc_ts: + def generate_padata_fn(_kdc_exchange_dict, + _callback_dict, + req_body): + padata = [] + + if freshness is not None: + freshness_padata = self.PA_DATA_create(PADATA_AS_FRESHNESS, + freshness) + padata.append(freshness_padata) + + if send_enc_ts: + patime, pausec = self.get_KerberosTimeWithUsec() + enc_ts = self.PA_ENC_TS_ENC_create(patime, pausec) + enc_ts = self.der_encode( + enc_ts, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + enc_ts = self.EncryptedData_create(preauth_key, + KU_PA_ENC_TIMESTAMP, + enc_ts) + enc_ts = self.der_encode( + enc_ts, asn1Spec=krb5_asn1.EncryptedData()) + + enc_ts = self.PA_DATA_create(PADATA_ENC_TIMESTAMP, enc_ts) + + padata.append(enc_ts) + + return padata, req_body + else: + generate_padata_fn = None + + user_name = creds.get_username() + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=user_name.split('/')) + + target_name = target_creds.get_username() + target_realm = target_creds.get_realm() + + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', target_name[:-1]]) + + if expect_error: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + + expected_sname = sname + else: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + expected_sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[target_name]) + + kdc_options = ('forwardable,' + 'renewable,' + 'canonicalize,' + 'renewable-ok') + kdc_options = krb5_asn1.KDCOptions(kdc_options) + + ticket_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + kdc_exchange_dict = self.as_exchange_dict( + creds=creds, + expected_crealm=creds.get_realm(), + expected_cname=cname, + expected_srealm=target_realm, + expected_sname=expected_sname, + expected_supported_etypes=target_creds.tgs_supported_enctypes, + ticket_decryption_key=ticket_decryption_key, + generate_padata_fn=generate_padata_fn, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expect_error, + expected_salt=creds.get_salt(), + preauth_key=preauth_key, + kdc_options=str(kdc_options), + expect_edata=expect_edata) + + till = self.get_KerberosTime(offset=36000) + + if etypes is None: + etypes = kcrypto.Enctype.AES256, kcrypto.Enctype.RC4, + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=cname, + realm=target_realm, + sname=sname, + till_time=till, + etypes=etypes) + if expect_error: + self.check_error_rep(rep, expect_error) + else: + self.check_as_reply(rep) + + return kdc_exchange_dict + + def get_ca_cert_and_private_key(self): + # The password with which to try to encrypt the certificate or private + # key specified on the command line. + ca_pass = samba.tests.env_get_var_value('CA_PASS', allow_missing=True) + if ca_pass is not None: + ca_pass = ca_pass.encode('utf-8') + + # The root certificate of the CA, with which we can issue new + # certificates. + ca_cert_path = samba.tests.env_get_var_value('CA_CERT') + with open(ca_cert_path, mode='rb') as f: + ca_cert_data = f.read() + + try: + # If the certificate file is in the PKCS#12 format (such as is + # found in a .pfx file) try to get the private key and the + # certificate all in one go. + ca_private_key, ca_cert, _additional_ca_certs = ( + pkcs12.load_key_and_certificates( + ca_cert_data, ca_pass, default_backend())) + except ValueError: + # Fall back to loading a PEM-encoded certificate. + ca_private_key = None + ca_cert = x509.load_pem_x509_certificate( + ca_cert_data, default_backend()) + + # If we didn’t get the private key, do that now. + if ca_private_key is None: + ca_private_key_path = samba.tests.env_get_var_value( + 'CA_PRIVATE_KEY') + with open(ca_private_key_path, mode='rb') as f: + ca_private_key = serialization.load_pem_private_key( + f.read(), password=ca_pass, backend=default_backend()) + + return ca_cert, ca_private_key + + def create_certificate(self, + creds, + ca_cert, + ca_private_key, + certificate_signature=None): + if certificate_signature is None: + certificate_signature = hashes.SHA256 + + user_name = creds.get_username() + + builder = x509.CertificateBuilder() + + # Add the subject name. + cert_name = f'{user_name}@{creds.get_realm().lower()}' + builder = builder.subject_name(x509.Name([ + # This name can be anything; it isn’t needed to authorize the + # user. The SubjectAlternativeName is used for that instead. + x509.NameAttribute(NameOID.COUNTRY_NAME, 'US'), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, 'SambaState'), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'SambaSelfTesting'), + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'Users'), + x509.NameAttribute(NameOID.COMMON_NAME, + f'{cert_name}/emailAddress={cert_name}'), + ])) + + # The new certificate must be issued by the root CA. + builder = builder.issuer_name(ca_cert.issuer) + + one_day = timedelta(1, 0, 0) + + # Put the certificate start time in the past to avoid issues where the + # KDC considers the certificate to be invalid due to clock skew. Note + # that if the certificate predates the existence of the account in AD, + # Windows will refuse authentication unless a strong mapping is + # present (in the certificate, or in AD). + # See https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16#ID0EFR + builder = builder.not_valid_before(datetime.today() - one_day) + + builder = builder.not_valid_after(datetime.today() + (one_day * 30)) + + builder = builder.serial_number(x509.random_serial_number()) + + public_key = creds.get_public_key() + builder = builder.public_key(public_key) + + # Add the SubjectAlternativeName. Windows uses this to map the account + # to the certificate. + id_pkinit_ms_san = x509.ObjectIdentifier( + str(krb5_asn1.id_pkinit_ms_san)) + encoded_upn = self.der_encode(creds.get_upn(), + asn1Spec=krb5_asn1.MS_UPN_SAN()) + ms_upn_san = x509.OtherName(id_pkinit_ms_san, encoded_upn) + builder = builder.add_extension( + x509.SubjectAlternativeName([ms_upn_san]), + critical=False, + ) + + builder = builder.add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True, + ) + + # The key identifier is used to identify the certificate. + subject_key_id = x509.SubjectKeyIdentifier.from_public_key(public_key) + builder = builder.add_extension( + subject_key_id, critical=True, + ) + + # Add the key usages for which this certificate is valid. Windows + # doesn’t actually require this extension to be present. + builder = builder.add_extension( + # Heimdal requires that the certificate be valid for digital + # signatures. + x509.KeyUsage(digital_signature=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False), + critical=True, + ) + + # Windows doesn’t require this extension to be present either; but if + # it is, Windows will not accept the certificate unless either client + # authentication or smartcard logon is specified, returning + # KDC_ERR_INCONSISTENT_KEY_PURPOSE otherwise. + builder = builder.add_extension( + x509.ExtendedKeyUsage([ + x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH, + ]), + critical=False, + ) + + # If the certificate predates (as ours does) the existence of the + # account that presents it Windows will refuse to accept it unless + # there exists a strong mapping from one to the other. This strong + # mapping will in this case take the form of a certificate extension + # described in [MS-WCCE] 2.2.2.7.7.4 (szOID_NTDS_CA_SECURITY_EXT) and + # containing the account’s SID. + + # Encode this structure manually until we are able to produce the same + # ASN.1 encoding that Windows does. + + encoded_sid = creds.get_sid().encode('utf-8') + + # The OCTET STRING tag, followed by length and encoded SID… + security_ext = bytes([0x04]) + self.asn1_length(encoded_sid) + ( + encoded_sid) + + # …enclosed in a construct tagged with the application-specific value + # 0… + security_ext = bytes([0xa0]) + self.asn1_length(security_ext) + ( + security_ext) + + # …preceded by the extension OID… + encoded_oid = self.der_encode(krb5_asn1.szOID_NTDS_OBJECTSID, + univ.ObjectIdentifier()) + security_ext = encoded_oid + security_ext + + # …and another application-specific tag 0… + # (This is the part about which I’m unsure. This length is not just of + # the OID, but of the entire structure so far, as if there’s some + # nesting going on. So far I haven’t been able to replicate this with + # pyasn1.) + security_ext = bytes([0xa0]) + self.asn1_length(security_ext) + ( + security_ext) + + # …all enclosed in a structure with a SEQUENCE tag. + security_ext = bytes([0x30]) + self.asn1_length(security_ext) + ( + security_ext) + + # Add the security extension to the certificate. + builder = builder.add_extension( + x509.UnrecognizedExtension( + x509.ObjectIdentifier( + str(krb5_asn1.szOID_NTDS_CA_SECURITY_EXT)), + security_ext, + ), + critical=False, + ) + + # Sign the certificate with the CA’s private key. Windows accepts both + # SHA1 and SHA256 hashes. + certificate = builder.sign( + private_key=ca_private_key, algorithm=certificate_signature(), + backend=default_backend() + ) + + return certificate + + def revoke_certificate(self, certificate, + ca_cert, + ca_private_key, + crl_signature=None): + if crl_signature is None: + crl_signature = hashes.SHA256 + + # Read the existing certificate revocation list. + crl_path = samba.tests.env_get_var_value('KRB5_CRL_FILE') + with open(crl_path, 'rb') as crl_file: + crl_data = crl_file.read() + + try: + # Get the list of existing revoked certificates. + revoked_certs = x509.load_pem_x509_crl(crl_data, default_backend()) + extensions = revoked_certs.extensions + except ValueError: + # We couldn’t parse the file. Let’s just create a new CRL from + # scratch. + revoked_certs = [] + extensions = [] + + # Create a new CRL. + builder = x509.CertificateRevocationListBuilder() + builder = builder.issuer_name(ca_cert.issuer) + builder = builder.last_update(datetime.today()) + one_day = timedelta(1, 0, 0) + builder = builder.next_update(datetime.today() + one_day) + + # Add the existing revoked certificates. + for revoked_cert in revoked_certs: + builder = builder.add_revoked_certificate(revoked_cert) + + # Add the serial number of the certificate that we’re revoking. + revoked_cert = x509.RevokedCertificateBuilder().serial_number( + certificate.serial_number + ).revocation_date( + datetime.today() + ).build(default_backend()) + builder = builder.add_revoked_certificate(revoked_cert) + + # Copy over any extensions from the existing certificate. + for extension in extensions: + builder = builder.add_extension(extension.value, + extension.critical) + + # Sign the CRL with the CA’s private key. + crl = builder.sign( + private_key=ca_private_key, algorithm=crl_signature(), + backend=default_backend(), + ) + + # Write the CRL back out to the file. + crl_data = crl.public_bytes(serialization.Encoding.PEM) + with open(crl_path, 'wb') as crl_file: + crl_file.write(crl_data) + + def _pkinit_req(self, + creds, + target_creds, + *, + certificate=None, + expect_error=0, + using_pkinit=PkInit.PUBLIC_KEY, + etypes=None, + pk_nonce=None, + supported_cms_types=None, + signature_algorithm=None, + certificate_signature=None, + freshness_token=None, + win2k_variant=False, + ): + self.assertIsNot(using_pkinit, PkInit.NOT_USED) + + if signature_algorithm is None: + # This algorithm must be one of ‘sig_algs’ for it to be supported + # by Heimdal. + signature_algorithm = krb5_asn1.sha1WithRSAEncryption + + signature_algorithm_id = self.AlgorithmIdentifier_create( + signature_algorithm) + + if certificate is None: + ca_cert, ca_private_key = self.get_ca_cert_and_private_key() + + # Create a certificate for the client signed by the CA. + certificate = self.create_certificate(creds, + ca_cert, + ca_private_key, + certificate_signature) + + private_key = creds.get_private_key() + + if using_pkinit is PkInit.DIFFIE_HELLMAN: + # This is the 2048-bit MODP Group from RFC 3526. Heimdal refers to + # it as “rfc3526-MODP-group14”. + p, g = 32317006071311007300338913926423828248817941241140239112842009751400741706634354222619689417363569347117901737909704191754605873209195028853758986185622153212175412514901774520270235796078236248884246189477587641105928646099411723245426622522193230540919037680524235519125679715870117001058055877651038861847280257976054903569732561526167081339361799541336476559160368317896729073178384589680639671900977202194168647225871031411336429319536193471636533209717077448227988588565369208645296636077250268955505928362751121174096972998068410554359584866583291642136218231078990999448652468262416972035911852507045361090559, 2 + + numbers = dh.DHParameterNumbers(p, g) + dh_params = numbers.parameters(default_backend()) + + dh_private_key = dh_params.generate_private_key() + + preauth_key = dh_private_key + else: + preauth_key = private_key + + if pk_nonce is None: + pk_nonce = self.get_Nonce() + + def generate_pk_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + if win2k_variant: + digest = None + else: + checksum_blob = self.der_encode( + req_body, + asn1Spec=krb5_asn1.KDC_REQ_BODY()) + + # Calculate the SHA1 checksum over the KDC-REQ-BODY. This checksum + # is required to be present in the authenticator, and must be SHA1. + digest = hashes.Hash(hashes.SHA1(), default_backend()) + digest.update(checksum_blob) + digest = digest.finalize() + + ctime, cusec = self.get_KerberosTimeWithUsec() + + if win2k_variant: + krbtgt_sname = self.get_krbtgt_sname() + krbtgt_realm = self.get_krbtgt_creds().get_realm() + else: + krbtgt_sname = None + krbtgt_realm = None + + # Create the authenticator, which shows that we had possession of + # the private key at some point. + authenticator_obj = self.PKAuthenticator_create( + cusec, + ctime, + pk_nonce, + pa_checksum=digest, + freshness_token=freshness_token, + kdc_name=krbtgt_sname, + kdc_realm=krbtgt_realm, + win2k_variant=win2k_variant) + + if using_pkinit is PkInit.DIFFIE_HELLMAN: + dh_public_key = dh_private_key.public_key() + + encoded_dh_public_key = dh_public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo) + decoded_dh_public_key = self.der_decode( + encoded_dh_public_key, + asn1Spec=krb5_asn1.SubjectPublicKeyInfo()) + dh_public_key_bitstring = decoded_dh_public_key[ + 'subjectPublicKey'] + + # Encode the Diffie-Hellman parameters. + params = dh_params.parameter_bytes( + serialization.Encoding.DER, + serialization.ParameterFormat.PKCS3) + + pk_algorithm = self.AlgorithmIdentifier_create( + krb5_asn1.dhpublicnumber, + parameters=params) + + # Create the structure containing information about the public + # key of the certificate that we shall present. + client_public_value = self.SubjectPublicKeyInfo_create( + pk_algorithm, + dh_public_key_bitstring) + else: + client_public_value = None + + # An optional set of algorithms supported by the client in + # decreasing order of preference. For whatever reason, if this + # field is missing or empty, Windows will respond with a slightly + # differently encoded ReplyKeyPack, wrapping it first in a + # ContentInfo structure. + nonlocal supported_cms_types + if win2k_variant: + self.assertIsNone(supported_cms_types) + elif supported_cms_types is False: + # Exclude this field. + supported_cms_types = None + elif supported_cms_types is None: + supported_cms_types = [ + self.AlgorithmIdentifier_create( + krb5_asn1.id_pkcs1_sha256WithRSAEncryption), + ] + + # The client may include this field if it wishes to reuse DH keys + # or allow the KDC to do so. + client_dh_nonce = None + + auth_pack_obj = self.AuthPack_create( + authenticator_obj, + client_public_value=client_public_value, + supported_cms_types=supported_cms_types, + client_dh_nonce=client_dh_nonce, + win2k_variant=win2k_variant) + + asn1_spec = (krb5_asn1.AuthPack_Win2k + if win2k_variant + else krb5_asn1.AuthPack) + auth_pack = self.der_encode(auth_pack_obj, asn1Spec=asn1_spec()) + + signature_hash = self.hash_from_algorithm(signature_algorithm) + + pad = padding.PKCS1v15() + signed = private_key.sign(auth_pack, + padding=pad, + algorithm=signature_hash()) + + encap_content_info_obj = self.EncapsulatedContentInfo_create( + krb5_asn1.id_pkinit_authData, auth_pack) + + subject_key_id = certificate.extensions.get_extension_for_oid( + x509.ExtensionOID.SUBJECT_KEY_IDENTIFIER) + signer_identifier = self.SignerIdentifier_create( + subject_key_id=subject_key_id.value.digest) + + signer_info = self.SignerInfo_create( + signer_identifier, + signature_algorithm_id, + signature_algorithm_id, + signed, + signed_attrs=[ + # Note: these attributes are optional. + krb5_asn1.id_pkinit_authData, + krb5_asn1.id_messageDigest, + ]) + + encoded_cert = certificate.public_bytes(serialization.Encoding.DER) + decoded_cert = self.der_decode( + encoded_cert, asn1Spec=krb5_asn1.CertificateChoices()) + + signed_auth_pack = self.SignedData_create( + [signature_algorithm_id], + encap_content_info_obj, + signer_infos=[signer_info], + certificates=[decoded_cert], + crls=None) + + signed_auth_pack = self.der_encode(signed_auth_pack, + asn1Spec=krb5_asn1.SignedData()) + + pk_as_req = self.PK_AS_REQ_create(signed_auth_pack, + # This contains a list of CAs, + # trusted by the client, that can + # be used to certify the KDC. + trusted_certifiers=None, + kdc_pk_id=None, + win2k_variant=win2k_variant) + + pa_type = (PADATA_PK_AS_REP_19 + if win2k_variant + else PADATA_PK_AS_REQ) + padata = [self.PA_DATA_create(pa_type, pk_as_req)] + + return padata, req_body + + user_name = creds.get_username() + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=user_name.split('/')) + + target_name = target_creds.get_username() + target_realm = target_creds.get_realm() + + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', target_name[:-1]]) + + if expect_error: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + + expected_sname = sname + else: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + expected_sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[target_name]) + + kdc_options = ('forwardable,' + 'renewable,' + 'canonicalize,' + 'renewable-ok') + kdc_options = krb5_asn1.KDCOptions(kdc_options) + + ticket_decryption_key = self.TicketDecryptionKey_from_creds( + target_creds) + + kdc_exchange_dict = self.as_exchange_dict( + creds=creds, + client_cert=certificate, + expected_crealm=creds.get_realm(), + expected_cname=cname, + expected_srealm=target_realm, + expected_sname=expected_sname, + expected_supported_etypes=target_creds.tgs_supported_enctypes, + ticket_decryption_key=ticket_decryption_key, + generate_padata_fn=generate_pk_padata, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expect_error, + expected_salt=creds.get_salt(), + preauth_key=preauth_key, + kdc_options=str(kdc_options), + using_pkinit=using_pkinit, + pk_nonce=pk_nonce, + expect_edata=False) + + till = self.get_KerberosTime(offset=36000) + + if etypes is None: + etypes = kcrypto.Enctype.AES256, kcrypto.Enctype.RC4, + + if using_pkinit is PkInit.PUBLIC_KEY: + # DES-EDE3-CBC is required for public-key PK-INIT to work on + # Windows. + etypes += DES_EDE3_CBC, + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=cname, + realm=target_realm, + sname=sname, + till_time=till, + etypes=etypes) + if expect_error: + self.check_error_rep(rep, expect_error) + return None + + self.check_as_reply(rep) + return kdc_exchange_dict['rep_ticket_creds'] + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/protected_users_tests.py b/python/samba/tests/krb5/protected_users_tests.py new file mode 100755 index 0000000..fee78ab --- /dev/null +++ b/python/samba/tests/krb5/protected_users_tests.py @@ -0,0 +1,1053 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from functools import partial + +import ldb + +from samba import generate_random_password, ntstatus +from samba.dcerpc import netlogon, security +from samba.hresult import HRES_SEC_E_LOGON_DENIED + +import samba.tests.krb5.kcrypto as kcrypto +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.rfc4120_constants import ( + AES128_CTS_HMAC_SHA1_96, + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + DES3_CBC_MD5, + DES3_CBC_SHA1, + DES_CBC_CRC, + DES_CBC_MD5, + KDC_ERR_ETYPE_NOSUPP, + KDC_ERR_POLICY, + KDC_ERR_PREAUTH_REQUIRED, + KRB_ERROR, + NT_PRINCIPAL, + NT_SRV_INST, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +global_asn1_print = False +global_hexdump = False + + +class ProtectedUsersTests(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 ‘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') + + # Get account credentials for testing. + def _get_creds(self, + protected, + account_type=KDCBaseTest.AccountType.USER, + ntlm=False, + member_of=None, + supported_enctypes=None, + cached=True): + opts = { + 'kerberos_enabled': not ntlm, + } + members = () + if protected: + samdb = self.get_samdb() + protected_users_group = (f'<SID={samdb.get_domain_sid()}-' + f'{security.DOMAIN_RID_PROTECTED_USERS}>') + members += (protected_users_group,) + if member_of is not None: + members += (member_of,) + + if members: + opts['member_of'] = members + if supported_enctypes is not None: + opts['supported_enctypes'] = supported_enctypes + + return self.get_cached_creds(account_type=account_type, + opts=opts, + use_cache=cached) + + # Test NTLM authentication with a normal account. Authentication should + # succeed. + def test_ntlm_not_protected(self): + client_creds = self._get_creds(protected=False, + ntlm=True, + cached=False) + + self._connect(client_creds, simple_bind=False) + + # Test NTLM authentication with a protected account. Authentication should + # fail, as Protected User accounts cannot use NTLM authentication. + def test_ntlm_protected(self): + client_creds = self._get_creds(protected=True, + ntlm=True, + cached=False) + + self._connect(client_creds, simple_bind=False, + expect_error=f'{HRES_SEC_E_LOGON_DENIED:08X}') + + # Test that the Protected Users restrictions still apply when the user is a + # member of a group that is itself a member of Protected Users. + def test_ntlm_protected_nested(self): + samdb = self.get_samdb() + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + + protected_users_group = (f'<SID={samdb.get_domain_sid()}-' + f'{security.DOMAIN_RID_PROTECTED_USERS}>') + self.add_to_group(group_dn, ldb.Dn(samdb, protected_users_group), + 'member', expect_attr=False) + + client_creds = self._get_creds(protected=False, + ntlm=True, + member_of=group_dn) + + self._connect(client_creds, simple_bind=False, + expect_error=f'{HRES_SEC_E_LOGON_DENIED:08X}') + + # Test SAMR password changes for unprotected and protected accounts. + def test_samr_change_password_not_protected(self): + # Use a non-cached account so that it is not locked out for other + # tests. + client_creds = self._get_creds(protected=False, + cached=False) + + self._test_samr_change_password( + client_creds, + expect_error=None) + + def test_samr_change_password_protected(self): + # Use a non-cached account so that it is not locked out for other + # tests. + client_creds = self._get_creds(protected=True, + cached=False) + + self._test_samr_change_password( + client_creds, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + # Test interactive SamLogon with an unprotected account. + def test_samlogon_interactive_not_protected(self): + client_creds = self._get_creds(protected=False, + ntlm=True) + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation) + + # Test interactive SamLogon with a protected account. + def test_samlogon_interactive_protected(self): + client_creds = self._get_creds(protected=True, + ntlm=True) + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonInteractiveInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + # Test network SamLogon with an unprotected account. + def test_samlogon_network_not_protected(self): + client_creds = self._get_creds(protected=False, + ntlm=True) + self._test_samlogon(creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation) + + # Test network SamLogon with a protected account. + def test_samlogon_network_protected(self): + client_creds = self._get_creds(protected=True, + ntlm=True) + self._test_samlogon( + creds=client_creds, + logon_type=netlogon.NetlogonNetworkInformation, + expect_error=ntstatus.NT_STATUS_ACCOUNT_RESTRICTION) + + # Test that changing the password of an account in the Protected Users + # group still generates an NT hash. + def test_protected_nt_hash(self): + # Use a non-cached account, as we are changing the password. + client_creds = self._get_creds(protected=True, + cached=False) + client_dn = client_creds.get_dn() + + new_password = generate_random_password(32, 32) + utf16pw = f'"{new_password}"'.encode('utf-16-le') + + samdb = self.get_samdb() + msg = ldb.Message(client_dn) + msg['unicodePwd'] = ldb.MessageElement(utf16pw, + ldb.FLAG_MOD_REPLACE, + 'unicodePwd') + samdb.modify(msg) + + client_creds.set_password(new_password) + + expected_etypes = { + kcrypto.Enctype.AES256, + kcrypto.Enctype.AES128, + } + if self.expect_nt_hash: + expected_etypes.add(kcrypto.Enctype.RC4) + + self.get_keys(client_creds, + expected_etypes=expected_etypes) + + # Test that DES-CBC-CRC cannot be used whether or not the user is + # protected. + def test_des_cbc_crc_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=DES_CBC_CRC, + expect_error=True) + + def test_des_cbc_crc_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=DES_CBC_CRC, + expect_error=True, rc4_support=False) + + # Test that DES-CBC-MD5 cannot be used whether or not the user is + # protected. + def test_des_cbc_md5_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=DES_CBC_MD5, + expect_error=True) + + def test_des_cbc_md5_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=DES_CBC_MD5, + expect_error=True, rc4_support=False) + + # Test that DES3-CBC-MD5 cannot be used whether or not the user is + # protected. + def test_des3_cbc_md5_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=DES3_CBC_MD5, + expect_error=True) + + def test_des3_cbc_md5_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=DES3_CBC_MD5, + expect_error=True, rc4_support=False) + + # Test that DES3-CBC-SHA1 cannot be used whether or not the user is + # protected. + def test_des3_cbc_sha1_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=DES3_CBC_SHA1, + expect_error=True) + + def test_des3_cbc_sha1_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=DES3_CBC_SHA1, + expect_error=True, rc4_support=False) + + # Test that RC4 may only be used if the user is not protected. + def test_rc4_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=ARCFOUR_HMAC_MD5) + + def test_rc4_protected_aes256_preauth(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=ARCFOUR_HMAC_MD5, + preauth_etype=AES256_CTS_HMAC_SHA1_96, + rc4_support=False) + + def test_rc4_protected_rc4_preauth(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=ARCFOUR_HMAC_MD5, + preauth_etype=ARCFOUR_HMAC_MD5, + expect_error=True, rc4_support=False, + expect_edata=False) + + # Test that AES256 can always be used. + def test_aes256_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=AES256_CTS_HMAC_SHA1_96) + + def test_aes256_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=AES256_CTS_HMAC_SHA1_96, + rc4_support=False) + + def test_aes256_rc4_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=(AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5)) + + def test_aes256_rc4_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=(AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5), + rc4_support=False) + + def test_rc4_aes256_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=(ARCFOUR_HMAC_MD5, + AES256_CTS_HMAC_SHA1_96)) + + def test_rc4_aes256_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=(ARCFOUR_HMAC_MD5, + AES256_CTS_HMAC_SHA1_96), + rc4_support=False) + + # Test that AES128 can always be used. + def test_aes128_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=AES128_CTS_HMAC_SHA1_96) + + def test_aes128_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=AES128_CTS_HMAC_SHA1_96, + rc4_support=False) + + def test_aes128_rc4_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=(AES128_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5)) + + def test_aes128_rc4_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=(AES128_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5), + rc4_support=False) + + def test_rc4_aes128_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, etype=(ARCFOUR_HMAC_MD5, + AES128_CTS_HMAC_SHA1_96)) + + def test_rc4_aes128_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, etype=(ARCFOUR_HMAC_MD5, + AES128_CTS_HMAC_SHA1_96), + rc4_support=False) + + # Test also with computer accounts. + def test_rc4_mac_not_protected(self): + client_creds = self._get_creds( + protected=False, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=ARCFOUR_HMAC_MD5) + + def test_rc4_mac_protected_aes256_preauth(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=ARCFOUR_HMAC_MD5, + preauth_etype=AES256_CTS_HMAC_SHA1_96, + rc4_support=False) + + def test_rc4_mac_protected_rc4_preauth(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=ARCFOUR_HMAC_MD5, + preauth_etype=ARCFOUR_HMAC_MD5, + expect_error=True, rc4_support=False, + expect_edata=False) + + def test_aes256_rc4_mac_not_protected(self): + client_creds = self._get_creds( + protected=False, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=(AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5)) + + def test_aes256_rc4_mac_protected(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=(AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5), + rc4_support=False) + + def test_rc4_aes256_mac_not_protected(self): + client_creds = self._get_creds( + protected=False, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=(ARCFOUR_HMAC_MD5, + AES256_CTS_HMAC_SHA1_96)) + + def test_rc4_aes256_mac_protected(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=(ARCFOUR_HMAC_MD5, + AES256_CTS_HMAC_SHA1_96), + rc4_support=False) + + def test_aes128_rc4_mac_not_protected(self): + client_creds = self._get_creds( + protected=False, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=(AES128_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5)) + + def test_aes128_rc4_mac_protected(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=(AES128_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5), + rc4_support=False) + + def test_rc4_aes128_mac_not_protected(self): + client_creds = self._get_creds( + protected=False, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=(ARCFOUR_HMAC_MD5, + AES128_CTS_HMAC_SHA1_96)) + + def test_rc4_aes128_mac_protected(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, etype=(ARCFOUR_HMAC_MD5, + AES128_CTS_HMAC_SHA1_96), + rc4_support=False) + + # Test that RC4 can only be used as a preauth etype if the user is not + # protected. + def test_ts_rc4_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, preauth_etype=ARCFOUR_HMAC_MD5) + + def test_ts_rc4_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, preauth_etype=ARCFOUR_HMAC_MD5, + expect_error=True, rc4_support=False, + expect_edata=False) + + # Test that the etype restrictions still apply if the user is a member of a + # group that is itself in the Protected Users group. + def test_ts_rc4_protected_nested(self): + samdb = self.get_samdb() + group_name = self.get_new_username() + group_dn = self.create_group(samdb, group_name) + + protected_users_group = (f'<SID={samdb.get_domain_sid()}-' + f'{security.DOMAIN_RID_PROTECTED_USERS}>') + self.add_to_group(group_dn, ldb.Dn(samdb, protected_users_group), + 'member', expect_attr=False) + + client_creds = self._get_creds(protected=False, + member_of=group_dn) + + self._test_etype(client_creds, preauth_etype=ARCFOUR_HMAC_MD5, + expect_error=True, rc4_support=False, + expect_edata=False) + + # Test that AES256 can always be used as a preauth etype. + def test_ts_aes256_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, preauth_etype=AES256_CTS_HMAC_SHA1_96) + + def test_ts_aes256_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, preauth_etype=AES256_CTS_HMAC_SHA1_96, + rc4_support=False) + + # Test that AES128 can always be used as a preauth etype. + def test_ts_aes128_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._test_etype(client_creds, preauth_etype=AES128_CTS_HMAC_SHA1_96) + + def test_ts_aes128_protected(self): + client_creds = self._get_creds(protected=True) + + self._test_etype(client_creds, preauth_etype=AES128_CTS_HMAC_SHA1_96, + rc4_support=False) + + # Test also with machine accounts. + def test_ts_rc4_mac_not_protected(self): + client_creds = self._get_creds( + protected=False, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, preauth_etype=ARCFOUR_HMAC_MD5) + + def test_ts_rc4_mac_protected(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, preauth_etype=ARCFOUR_HMAC_MD5, + expect_error=True, rc4_support=False, + expect_edata=False) + + def test_ts_aes256_mac_not_protected(self): + client_creds = self._get_creds( + protected=False, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, preauth_etype=AES256_CTS_HMAC_SHA1_96) + + def test_ts_aes256_mac_protected(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, preauth_etype=AES256_CTS_HMAC_SHA1_96, + rc4_support=False) + + def test_ts_aes128_mac_not_protected(self): + client_creds = self._get_creds( + protected=False, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, preauth_etype=AES128_CTS_HMAC_SHA1_96) + + def test_ts_aes128_mac_protected(self): + client_creds = self._get_creds( + protected=True, + account_type=self.AccountType.COMPUTER) + + self._test_etype(client_creds, preauth_etype=AES128_CTS_HMAC_SHA1_96, + rc4_support=False) + + # Test that the restrictions do not apply to accounts acting as services, + # and that RC4 service tickets can still be obtained. + def test_service_rc4_only_not_protected(self): + client_creds = self.get_client_creds() + service_creds = self._get_creds(protected=False, + account_type=self.AccountType.COMPUTER, + supported_enctypes=kcrypto.Enctype.RC4) + tgt = self.get_tgt(client_creds) + self.get_service_ticket(tgt, service_creds) + + def test_service_rc4_only_protected(self): + client_creds = self.get_client_creds() + service_creds = self._get_creds(protected=True, + account_type=self.AccountType.COMPUTER, + supported_enctypes=kcrypto.Enctype.RC4) + tgt = self.get_tgt(client_creds) + self.get_service_ticket(tgt, service_creds) + + # Test that requesting a ticket with a short lifetime results in a ticket + # with that lifetime. + def test_tgt_lifetime_shorter_not_protected(self): + client_creds = self._get_creds(protected=False) + + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._test_etype(client_creds, + preauth_etype=AES256_CTS_HMAC_SHA1_96, + till=till) + self.check_ticket_times(tgt, expected_end=till) + + def test_tgt_lifetime_shorter_protected(self): + client_creds = self._get_creds(protected=True) + + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._test_etype(client_creds, + preauth_etype=AES256_CTS_HMAC_SHA1_96, + till=till, rc4_support=False) + + self.check_ticket_times(tgt, expected_end=till, + expected_renew_time=till) + + # Test that requesting a ticket with a long lifetime produces a ticket with + # that lifetime, unless the user is protected, whereupon the lifetime will + # be capped at four hours. + def test_tgt_lifetime_longer_not_protected(self): + client_creds = self._get_creds(protected=False) + + till = self.get_KerberosTime(offset=6 * 60 * 60) # 6 hours + tgt = self._test_etype(client_creds, + preauth_etype=AES256_CTS_HMAC_SHA1_96, + till=till) + self.check_ticket_times(tgt, expected_end=till) + + def test_tgt_lifetime_longer_protected(self): + client_creds = self._get_creds(protected=True) + + till = self.get_KerberosTime(offset=6 * 60 * 60) # 6 hours + tgt = self._test_etype(client_creds, + preauth_etype=AES256_CTS_HMAC_SHA1_96, + till=till, rc4_support=False) + + expected_life = 4 * 60 * 60 # 4 hours + self.check_ticket_times(tgt, expected_life=expected_life, + expected_renew_life=expected_life) + + # Test that the lifetime of a service ticket is capped to the lifetime of + # the TGT. + def test_ticket_lifetime_not_protected(self): + client_creds = self._get_creds(protected=False) + + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._test_etype( + client_creds, preauth_etype=AES256_CTS_HMAC_SHA1_96, till=till) + self.check_ticket_times(tgt, expected_end=till) + + service_creds = self.get_service_creds() + till2 = self.get_KerberosTime(offset=10 * 60 * 60) # 10 hours + ticket = self.get_service_ticket(tgt, service_creds, till=till2) + + self.check_ticket_times(ticket, expected_end=till) + + def test_ticket_lifetime_protected(self): + client_creds = self._get_creds(protected=True) + + till = self.get_KerberosTime(offset=2 * 60 * 60) # 2 hours + tgt = self._test_etype( + client_creds, preauth_etype=AES256_CTS_HMAC_SHA1_96, till=till, + rc4_support=False) + + self.check_ticket_times(tgt, expected_end=till, + expected_renew_time=till) + + service_creds = self.get_service_creds() + till2 = self.get_KerberosTime(offset=10 * 60 * 60) # 10 hours + ticket = self.get_service_ticket(tgt, service_creds, till=till2) + + self.check_ticket_times(ticket, expected_end=till) + + # Test that a request for a forwardable ticket will only be fulfilled if + # the user is not protected. + def test_forwardable_as_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._get_tgt_check_flags(client_creds, kdc_options='forwardable', + expected_flags='forwardable') + + def test_forwardable_as_protected(self): + client_creds = self._get_creds(protected=True) + + self._get_tgt_check_flags(client_creds, kdc_options='forwardable', + unexpected_flags='forwardable', + rc4_support=False) + + # Test that a request for a proxiable ticket will only be fulfilled if the + # user is not protected. + def test_proxiable_as_not_protected(self): + client_creds = self._get_creds(protected=False) + + self._get_tgt_check_flags(client_creds, kdc_options='proxiable', + expected_flags='proxiable') + + def test_proxiable_as_protected(self): + client_creds = self._get_creds(protected=True) + + self._get_tgt_check_flags(client_creds, kdc_options='proxiable', + unexpected_flags='proxiable', + rc4_support=False) + + # An alternate test for Protected Users that passes if we get a policy + # error rather than a ticket that is not proxiable. + def test_proxiable_as_protected_policy_error(self): + client_creds = self._get_creds(protected=True) + + self._get_tgt_check_flags(client_creds, kdc_options='proxiable', + unexpected_flags='proxiable', + rc4_support=False, expect_error=True) + + # Test that if we have a forwardable TGT, then we can use it to obtain a + # forwardable service ticket, whether or not the account is protected. + def test_forwardable_tgs_not_protected(self): + client_creds = self._get_creds(protected=False) + + tgt = self.get_tgt(client_creds) + tgt = self.modified_ticket( + tgt, + modify_fn=partial(self.modify_ticket_flag, flag='forwardable', + value=True), + checksum_keys=self.get_krbtgt_checksum_key()) + + service_creds = self.get_service_creds() + self.get_service_ticket( + tgt, service_creds, kdc_options='forwardable', + expected_flags=krb5_asn1.TicketFlags('forwardable')) + + def test_forwardable_tgs_protected(self): + client_creds = self._get_creds(protected=True) + + tgt = self.get_tgt(client_creds, rc4_support=False) + tgt = self.modified_ticket( + tgt, + modify_fn=partial(self.modify_ticket_flag, flag='forwardable', + value=True), + checksum_keys=self.get_krbtgt_checksum_key()) + + service_creds = self.get_service_creds() + self.get_service_ticket( + tgt, service_creds, kdc_options='forwardable', + expected_flags=krb5_asn1.TicketFlags('forwardable'), + rc4_support=False) + + # Test that if we have a proxiable TGT, then we can use it to obtain a + # forwardable service ticket, whether or not the account is protected. + def test_proxiable_tgs_not_protected(self): + client_creds = self._get_creds(protected=False) + + tgt = self.get_tgt(client_creds) + tgt = self.modified_ticket( + tgt, + modify_fn=partial(self.modify_ticket_flag, flag='proxiable', + value=True), + checksum_keys=self.get_krbtgt_checksum_key()) + + service_creds = self.get_service_creds() + self.get_service_ticket( + tgt, service_creds, kdc_options='proxiable', + expected_flags=krb5_asn1.TicketFlags('proxiable')) + + def test_proxiable_tgs_protected(self): + client_creds = self._get_creds(protected=True) + + tgt = self.get_tgt(client_creds, rc4_support=False) + tgt = self.modified_ticket( + tgt, + modify_fn=partial(self.modify_ticket_flag, flag='proxiable', + value=True), + checksum_keys=self.get_krbtgt_checksum_key()) + + service_creds = self.get_service_creds() + self.get_service_ticket( + tgt, service_creds, kdc_options='proxiable', + expected_flags=krb5_asn1.TicketFlags('proxiable'), + rc4_support=False) + + def check_ticket_times(self, + ticket_creds, + expected_end=None, + expected_life=None, + expected_renew_time=None, + expected_renew_life=None): + ticket = ticket_creds.ticket_private + + authtime = ticket['authtime'] + starttime = ticket.get('starttime', authtime) + endtime = ticket['endtime'] + renew_till = ticket.get('renew-till', None) + + starttime = self.get_EpochFromKerberosTime(starttime) + + if expected_end is None: + self.assertIsNotNone(expected_life, + 'did not supply expected endtime or lifetime') + + expected_end = self.get_KerberosTime(epoch=starttime, + offset=expected_life) + else: + self.assertIsNone(expected_life, + 'supplied both expected endtime and lifetime') + + self.assertEqual(expected_end, endtime.decode('ascii')) + + if renew_till is None: + self.assertIsNone(expected_renew_time) + self.assertIsNone(expected_renew_life) + else: + if expected_renew_life is not None: + self.assertIsNone( + expected_renew_time, + 'supplied both expected renew time and lifetime') + + expected_renew_time = self.get_KerberosTime( + epoch=starttime, offset=expected_renew_life) + + if expected_renew_time is not None: + self.assertEqual(expected_renew_time, + renew_till.decode('ascii')) + + def _test_etype(self, + creds, + expect_error=False, + etype=None, + preauth_etype=None, + till=None, + rc4_support=True, + expect_edata=None): + if etype is None: + etype = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5) + elif isinstance(etype, int): + etype = (etype,) + + user_name = creds.get_username() + realm = creds.get_realm() + salt = creds.get_salt() + + cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=user_name.split('/')) + sname = self.PrincipalName_create(name_type=NT_SRV_INST, + names=['krbtgt', realm]) + expected_sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=['krbtgt', realm.upper()]) + + expected_cname = cname + + if till is None: + till = self.get_KerberosTime(offset=36000) + + renew_time = till + + krbtgt_creds = self.get_krbtgt_creds() + ticket_decryption_key = ( + self.TicketDecryptionKey_from_creds(krbtgt_creds)) + + expected_etypes = krbtgt_creds.tgs_supported_enctypes + + kdc_options = krb5_asn1.KDCOptions('renewable') + expected_flags = krb5_asn1.TicketFlags('renewable') + + expected_error = KDC_ERR_ETYPE_NOSUPP if expect_error else 0 + + if preauth_etype is None: + if expected_error: + expected_error_mode = KDC_ERR_PREAUTH_REQUIRED, expected_error + else: + expected_error_mode = KDC_ERR_PREAUTH_REQUIRED + + rep, kdc_exchange_dict = self._test_as_exchange( + creds=creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + renew_time=renew_time, + expected_error_mode=expected_error_mode, + expected_crealm=realm, + expected_cname=expected_cname, + expected_srealm=realm, + expected_sname=sname, + expected_salt=salt, + expected_flags=expected_flags, + expected_supported_etypes=expected_etypes, + etypes=etype, + padata=None, + kdc_options=kdc_options, + ticket_decryption_key=ticket_decryption_key, + rc4_support=rc4_support, + expect_edata=expect_edata) + self.assertIsNotNone(rep) + self.assertEqual(KRB_ERROR, rep['msg-type']) + error_code = rep['error-code'] + if expected_error: + self.assertIn(error_code, expected_error_mode) + if error_code == expected_error: + return + else: + self.assertEqual(expected_error_mode, error_code) + + etype_info2 = kdc_exchange_dict['preauth_etype_info2'] + + preauth_key = self.PasswordKey_from_etype_info2(creds, + etype_info2[0], + creds.get_kvno()) + else: + preauth_key = self.PasswordKey_from_creds(creds, preauth_etype) + + ts_enc_padata = self.get_enc_timestamp_pa_data_from_key(preauth_key) + padata = [ts_enc_padata] + + expected_realm = realm.upper() + + rep, kdc_exchange_dict = self._test_as_exchange( + creds=creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + renew_time=renew_time, + expected_error_mode=expected_error, + expected_crealm=expected_realm, + expected_cname=expected_cname, + expected_srealm=expected_realm, + expected_sname=expected_sname, + expected_salt=salt, + expected_flags=expected_flags, + expected_supported_etypes=expected_etypes, + etypes=etype, + padata=padata, + kdc_options=kdc_options, + preauth_key=preauth_key, + ticket_decryption_key=ticket_decryption_key, + rc4_support=rc4_support, + expect_edata=expect_edata) + if expect_error: + self.check_error_rep(rep, expected_error) + + return None + + self.check_as_reply(rep) + + ticket_creds = kdc_exchange_dict['rep_ticket_creds'] + return ticket_creds + + def _get_tgt_check_flags(self, + creds, + kdc_options, + rc4_support=True, + expect_error=False, + expected_flags=None, + unexpected_flags=None): + 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]) + expected_sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=['krbtgt', realm.upper()]) + + expected_cname = cname + + 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 = krb5_asn1.KDCOptions(kdc_options) + + if expected_flags is not None: + expected_flags = krb5_asn1.TicketFlags(expected_flags) + if unexpected_flags is not None: + unexpected_flags = krb5_asn1.TicketFlags(unexpected_flags) + + rep, kdc_exchange_dict = self._test_as_exchange( + creds=creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + expected_error_mode=KDC_ERR_PREAUTH_REQUIRED, + expected_crealm=realm, + expected_cname=expected_cname, + expected_srealm=realm, + expected_sname=sname, + expected_salt=salt, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + expected_supported_etypes=expected_etypes, + etypes=etype, + padata=None, + kdc_options=kdc_options, + ticket_decryption_key=ticket_decryption_key, + rc4_support=rc4_support) + self.check_pre_authentication(rep) + + etype_info2 = kdc_exchange_dict['preauth_etype_info2'] + + preauth_key = self.PasswordKey_from_etype_info2(creds, + etype_info2[0], + creds.get_kvno()) + + ts_enc_padata = self.get_enc_timestamp_pa_data_from_key(preauth_key) + padata = [ts_enc_padata] + + expected_realm = realm.upper() + + expected_error = KDC_ERR_POLICY if expect_error else 0 + + rep, kdc_exchange_dict = self._test_as_exchange( + creds=creds, + cname=cname, + realm=realm, + sname=sname, + till=till, + expected_error_mode=expected_error, + expected_crealm=expected_realm, + expected_cname=expected_cname, + expected_srealm=expected_realm, + expected_sname=expected_sname, + expected_salt=salt, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + expected_supported_etypes=expected_etypes, + etypes=etype, + padata=padata, + kdc_options=kdc_options, + preauth_key=preauth_key, + ticket_decryption_key=ticket_decryption_key, + rc4_support=rc4_support) + if expect_error: + self.check_error_rep(rep, expected_error) + + return None + + self.check_as_reply(rep) + + ticket_creds = kdc_exchange_dict['rep_ticket_creds'] + return ticket_creds + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/pyasn1_regen.sh b/python/samba/tests/krb5/pyasn1_regen.sh new file mode 100755 index 0000000..75b3988 --- /dev/null +++ b/python/samba/tests/krb5/pyasn1_regen.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# + +# +# I used https://github.com/kimgr/asn1ate.git +# to generate pyasn1 bindings for rfc4120.asn1 +# + +PATH_TO_ASN1ATE_CHECKOUT=$1 +PATH_TO_ASN1_INPUT_FILE=$2 + +set -u +set -e + +usage() +{ + echo "usage: $0 PATH_TO_ASN1ATE_CHECKOUT PATH_TO_ASN1_INPUT_FILE > PATH_TO_PYASN1_OUTPUT_FILE" +} + +test -n "${PATH_TO_ASN1ATE_CHECKOUT}" || { + usage + exit 1 +} +test -n "${PATH_TO_ASN1_INPUT_FILE}" || { + usage + exit 1 +} +test -d "${PATH_TO_ASN1ATE_CHECKOUT}" || { + usage + exit 1 +} +test -f "${PATH_TO_ASN1_INPUT_FILE}" || { + usage + exit 1 +} + +PATH_TO_PYASN1GEN_PY="${PATH_TO_ASN1ATE_CHECKOUT}/asn1ate/pyasn1gen.py" + +PYTHONPATH="${PATH_TO_ASN1ATE_CHECKOUT}:${PYTHONPATH-}" +export PYTHONPATH + +python3 "${PATH_TO_PYASN1GEN_PY}" "${PATH_TO_ASN1_INPUT_FILE}" diff --git a/python/samba/tests/krb5/raw_testcase.py b/python/samba/tests/krb5/raw_testcase.py new file mode 100644 index 0000000..90d286a --- /dev/null +++ b/python/samba/tests/krb5/raw_testcase.py @@ -0,0 +1,6221 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Isaac Boukris 2020 +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import socket +import struct +import time +import datetime +import random +import binascii +import itertools +import collections +import math + +from enum import Enum +from pprint import pprint + +from cryptography import x509 +from cryptography.hazmat.primitives import asymmetric, hashes +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend + +from pyasn1.codec.der.decoder import decode as pyasn1_der_decode +from pyasn1.codec.der.encoder import encode as pyasn1_der_encode +from pyasn1.codec.native.decoder import decode as pyasn1_native_decode +from pyasn1.codec.native.encoder import encode as pyasn1_native_encode + +from pyasn1.codec.ber.encoder import BitStringEncoder +import pyasn1.type.univ + +from pyasn1.error import PyAsn1Error + +from samba import unix2nttime +from samba.credentials import Credentials +from samba.dcerpc import claims, krb5pac, netlogon, samr, security +from samba.gensec import FEATURE_SEAL +from samba.ndr import ndr_pack, ndr_unpack +from samba.dcerpc.misc import ( + SEC_CHAN_WKSTA, + SEC_CHAN_BDC, +) + +import samba.tests +from samba.tests import TestCase + +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +from samba.tests.krb5.rfc4120_constants import ( + AD_IF_RELEVANT, + AD_WIN2K_PAC, + FX_FAST_ARMOR_AP_REQUEST, + KDC_ERR_CLIENT_REVOKED, + KDC_ERR_GENERIC, + KDC_ERR_POLICY, + KDC_ERR_PREAUTH_FAILED, + KDC_ERR_SKEW, + KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS, + KERB_ERR_TYPE_EXTENDED, + KRB_AP_REP, + KRB_AP_REQ, + KRB_AS_REP, + KRB_AS_REQ, + KRB_ERROR, + KRB_PRIV, + KRB_TGS_REP, + KRB_TGS_REQ, + KU_AP_REQ_AUTH, + KU_AP_REQ_ENC_PART, + KU_AS_FRESHNESS, + KU_AS_REP_ENC_PART, + KU_AS_REQ, + KU_ENC_CHALLENGE_KDC, + KU_FAST_ENC, + KU_FAST_FINISHED, + KU_FAST_REP, + KU_FAST_REQ_CHKSUM, + KU_KRB_PRIV, + KU_NON_KERB_CKSUM_SALT, + KU_NON_KERB_SALT, + KU_PKINIT_AS_REQ, + KU_TGS_REP_ENC_PART_SESSION, + KU_TGS_REP_ENC_PART_SUB_KEY, + KU_TGS_REQ_AUTH, + KU_TGS_REQ_AUTH_CKSUM, + KU_TGS_REQ_AUTH_DAT_SESSION, + KU_TGS_REQ_AUTH_DAT_SUBKEY, + KU_TICKET, + NT_PRINCIPAL, + NT_SRV_INST, + NT_WELLKNOWN, + PADATA_AS_FRESHNESS, + PADATA_ENCRYPTED_CHALLENGE, + PADATA_ENC_TIMESTAMP, + PADATA_ETYPE_INFO, + PADATA_ETYPE_INFO2, + PADATA_FOR_USER, + PADATA_FX_COOKIE, + PADATA_FX_ERROR, + PADATA_FX_FAST, + PADATA_GSS, + PADATA_KDC_REQ, + PADATA_PAC_OPTIONS, + PADATA_PAC_REQUEST, + PADATA_PKINIT_KX, + PADATA_PK_AS_REP, + PADATA_PK_AS_REP_19, + PADATA_PK_AS_REQ, + PADATA_PW_SALT, + PADATA_REQ_ENC_PA_REP, + PADATA_SUPPORTED_ETYPES, +) +import samba.tests.krb5.kcrypto as kcrypto + + +def BitStringEncoder_encodeValue32( + self, value, asn1Spec, encodeFun, **options): + # + # BitStrings like KDCOptions or TicketFlags should at least + # be 32-Bit on the wire + # + if asn1Spec is not None: + # TODO: try to avoid ASN.1 schema instantiation + value = asn1Spec.clone(value) + + valueLength = len(value) + if valueLength % 8: + alignedValue = value << (8 - valueLength % 8) + else: + alignedValue = value + + substrate = alignedValue.asOctets() + length = len(substrate) + # We need at least 32-Bit / 4-Bytes + if length < 4: + padding = 4 - length + else: + padding = 0 + ret = b'\x00' + substrate + (b'\x00' * padding) + return ret, False, True + + +BitStringEncoder.encodeValue = BitStringEncoder_encodeValue32 + + +def BitString_NamedValues_prettyPrint(self, scope=0): + ret = "%s" % self.asBinary() + bits = [] + highest_bit = 32 + for byte in self.asNumbers(): + for bit in [7, 6, 5, 4, 3, 2, 1, 0]: + mask = 1 << bit + if byte & mask: + val = 1 + else: + val = 0 + bits.append(val) + if len(bits) < highest_bit: + for bitPosition in range(len(bits), highest_bit): + bits.append(0) + indent = " " * scope + delim = ": (\n%s " % indent + for bitPosition in range(highest_bit): + if bitPosition in self.prettyPrintNamedValues: + name = self.prettyPrintNamedValues[bitPosition] + elif bits[bitPosition] != 0: + name = "unknown-bit-%u" % bitPosition + else: + continue + ret += "%s%s:%u" % (delim, name, bits[bitPosition]) + delim = ",\n%s " % indent + ret += "\n%s)" % indent + return ret + + +krb5_asn1.TicketFlags.prettyPrintNamedValues =\ + krb5_asn1.TicketFlagsValues.namedValues +krb5_asn1.TicketFlags.namedValues =\ + krb5_asn1.TicketFlagsValues.namedValues +krb5_asn1.TicketFlags.prettyPrint =\ + BitString_NamedValues_prettyPrint +krb5_asn1.KDCOptions.prettyPrintNamedValues =\ + krb5_asn1.KDCOptionsValues.namedValues +krb5_asn1.KDCOptions.namedValues =\ + krb5_asn1.KDCOptionsValues.namedValues +krb5_asn1.KDCOptions.prettyPrint =\ + BitString_NamedValues_prettyPrint +krb5_asn1.APOptions.prettyPrintNamedValues =\ + krb5_asn1.APOptionsValues.namedValues +krb5_asn1.APOptions.namedValues =\ + krb5_asn1.APOptionsValues.namedValues +krb5_asn1.APOptions.prettyPrint =\ + BitString_NamedValues_prettyPrint +krb5_asn1.PACOptionFlags.prettyPrintNamedValues =\ + krb5_asn1.PACOptionFlagsValues.namedValues +krb5_asn1.PACOptionFlags.namedValues =\ + krb5_asn1.PACOptionFlagsValues.namedValues +krb5_asn1.PACOptionFlags.prettyPrint =\ + BitString_NamedValues_prettyPrint + + +def Integer_NamedValues_prettyPrint(self, scope=0): + intval = int(self) + if intval in self.prettyPrintNamedValues: + name = self.prettyPrintNamedValues[intval] + else: + name = "<__unknown__>" + ret = "%d (0x%x) %s" % (intval, intval, name) + return ret + + +krb5_asn1.NameType.prettyPrintNamedValues =\ + krb5_asn1.NameTypeValues.namedValues +krb5_asn1.NameType.prettyPrint =\ + Integer_NamedValues_prettyPrint +krb5_asn1.AuthDataType.prettyPrintNamedValues =\ + krb5_asn1.AuthDataTypeValues.namedValues +krb5_asn1.AuthDataType.prettyPrint =\ + Integer_NamedValues_prettyPrint +krb5_asn1.PADataType.prettyPrintNamedValues =\ + krb5_asn1.PADataTypeValues.namedValues +krb5_asn1.PADataType.prettyPrint =\ + Integer_NamedValues_prettyPrint +krb5_asn1.EncryptionType.prettyPrintNamedValues =\ + krb5_asn1.EncryptionTypeValues.namedValues +krb5_asn1.EncryptionType.prettyPrint =\ + Integer_NamedValues_prettyPrint +krb5_asn1.ChecksumType.prettyPrintNamedValues =\ + krb5_asn1.ChecksumTypeValues.namedValues +krb5_asn1.ChecksumType.prettyPrint =\ + Integer_NamedValues_prettyPrint +krb5_asn1.KerbErrorDataType.prettyPrintNamedValues =\ + krb5_asn1.KerbErrorDataTypeValues.namedValues +krb5_asn1.KerbErrorDataType.prettyPrint =\ + Integer_NamedValues_prettyPrint + + +class Krb5EncryptionKey: + __slots__ = [ + 'ctype', + 'etype', + 'key', + 'kvno', + ] + + def __init__(self, key, kvno): + EncTypeChecksum = { + kcrypto.Enctype.AES256: kcrypto.Cksumtype.SHA1_AES256, + kcrypto.Enctype.AES128: kcrypto.Cksumtype.SHA1_AES128, + kcrypto.Enctype.RC4: kcrypto.Cksumtype.HMAC_MD5, + } + self.key = key + self.etype = key.enctype + self.ctype = EncTypeChecksum[self.etype] + self.kvno = kvno + + def __str__(self): + return "etype=%d ctype=%d kvno=%d key=%s" % ( + self.etype, self.ctype, self.kvno, self.key) + + def encrypt(self, usage, plaintext): + ciphertext = kcrypto.encrypt(self.key, usage, plaintext) + return ciphertext + + def decrypt(self, usage, ciphertext): + plaintext = kcrypto.decrypt(self.key, usage, ciphertext) + return plaintext + + def make_zeroed_checksum(self, ctype=None): + if ctype is None: + ctype = self.ctype + + checksum_len = kcrypto.checksum_len(ctype) + return bytes(checksum_len) + + def make_checksum(self, usage, plaintext, ctype=None): + if ctype is None: + ctype = self.ctype + cksum = kcrypto.make_checksum(ctype, self.key, usage, plaintext) + return cksum + + def verify_checksum(self, usage, plaintext, ctype, cksum): + if self.ctype != ctype: + raise AssertionError(f'key checksum type ({self.ctype}) != ' + f'checksum type ({ctype})') + + kcrypto.verify_checksum(ctype, + self.key, + usage, + plaintext, + cksum) + + def export_obj(self): + EncryptionKey_obj = { + 'keytype': self.etype, + 'keyvalue': self.key.contents, + } + return EncryptionKey_obj + + +class RodcPacEncryptionKey(Krb5EncryptionKey): + __slots__ = ['rodc_id'] + + def __init__(self, key, kvno, rodc_id=None): + super().__init__(key, kvno) + + if rodc_id is None: + kvno = self.kvno + if kvno is not None: + kvno >>= 16 + kvno &= (1 << 16) - 1 + + rodc_id = kvno or None + + if rodc_id is not None: + self.rodc_id = rodc_id.to_bytes(2, byteorder='little') + else: + self.rodc_id = b'' + + def make_rodc_zeroed_checksum(self, ctype=None): + checksum = super().make_zeroed_checksum(ctype) + return checksum + bytes(len(self.rodc_id)) + + def make_rodc_checksum(self, usage, plaintext, ctype=None): + checksum = super().make_checksum(usage, plaintext, ctype) + return checksum + self.rodc_id + + def verify_rodc_checksum(self, usage, plaintext, ctype, cksum): + if self.rodc_id: + cksum, cksum_rodc_id = cksum[:-2], cksum[-2:] + + if self.rodc_id != cksum_rodc_id: + raise AssertionError(f'{self.rodc_id.hex()} != ' + f'{cksum_rodc_id.hex()}') + + super().verify_checksum(usage, + plaintext, + ctype, + cksum) + + +class ZeroedChecksumKey(RodcPacEncryptionKey): + def make_checksum(self, usage, plaintext, ctype=None): + return self.make_zeroed_checksum(ctype) + + def make_rodc_checksum(self, usage, plaintext, ctype=None): + return self.make_rodc_zeroed_checksum(ctype) + + +class WrongLengthChecksumKey(RodcPacEncryptionKey): + __slots__ = ['_length'] + + def __init__(self, key, kvno, length): + super().__init__(key, kvno) + + self._length = length + + @classmethod + def _adjust_to_length(cls, checksum, length): + diff = length - len(checksum) + if diff > 0: + checksum += bytes(diff) + elif diff < 0: + checksum = checksum[:length] + + return checksum + + def make_zeroed_checksum(self, ctype=None): + return bytes(self._length) + + def make_checksum(self, usage, plaintext, ctype=None): + checksum = super().make_checksum(usage, plaintext, ctype) + return self._adjust_to_length(checksum, self._length) + + def make_rodc_zeroed_checksum(self, ctype=None): + return bytes(self._length) + + def make_rodc_checksum(self, usage, plaintext, ctype=None): + checksum = super().make_rodc_checksum(usage, plaintext, ctype) + return self._adjust_to_length(checksum, self._length) + + +class KerberosCredentials(Credentials): + __slots__ = [ + '_private_key', + 'account_type', + 'ap_supported_enctypes', + 'as_supported_enctypes', + 'dn', + 'forced_keys', + 'forced_salt', + 'kvno', + 'sid', + 'spn', + 'tgs_supported_enctypes', + 'upn', + ] + + non_etype_bits = ( + security.KERB_ENCTYPE_FAST_SUPPORTED) | ( + security.KERB_ENCTYPE_COMPOUND_IDENTITY_SUPPORTED) | ( + security.KERB_ENCTYPE_CLAIMS_SUPPORTED) | ( + security.KERB_ENCTYPE_RESOURCE_SID_COMPRESSION_DISABLED) | ( + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK) + + def __init__(self): + super().__init__() + all_enc_types = 0 + all_enc_types |= security.KERB_ENCTYPE_RC4_HMAC_MD5 + all_enc_types |= security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96 + all_enc_types |= security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96 + + self.as_supported_enctypes = all_enc_types + self.tgs_supported_enctypes = all_enc_types + self.ap_supported_enctypes = all_enc_types + + self.kvno = None + self.forced_keys = {} + + self.forced_salt = None + + self.dn = None + self.upn = None + self.spn = None + self.sid = None + self.account_type = None + + self._private_key = None + + def set_as_supported_enctypes(self, value): + self.as_supported_enctypes = int(value) + + def set_tgs_supported_enctypes(self, value): + self.tgs_supported_enctypes = int(value) + + def set_ap_supported_enctypes(self, value): + self.ap_supported_enctypes = int(value) + + etype_map = collections.OrderedDict([ + (kcrypto.Enctype.AES256, + security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96), + (kcrypto.Enctype.AES128, + security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96), + (kcrypto.Enctype.RC4, + security.KERB_ENCTYPE_RC4_HMAC_MD5), + (kcrypto.Enctype.DES_MD5, + security.KERB_ENCTYPE_DES_CBC_MD5), + (kcrypto.Enctype.DES_CRC, + security.KERB_ENCTYPE_DES_CBC_CRC) + ]) + + @classmethod + def etypes_to_bits(cls, etypes): + bits = 0 + for etype in etypes: + bit = cls.etype_map[etype] + if bits & bit: + raise ValueError(f'Got duplicate etype: {etype}') + bits |= bit + + return bits + + @classmethod + def bits_to_etypes(cls, bits): + etypes = () + for etype, bit in cls.etype_map.items(): + if bit & bits: + bits &= ~bit + etypes += (etype,) + + bits &= ~cls.non_etype_bits + if bits != 0: + raise ValueError(f'Unsupported etype bits: {bits}') + + return etypes + + def get_as_krb5_etypes(self): + return self.bits_to_etypes(self.as_supported_enctypes) + + def get_tgs_krb5_etypes(self): + return self.bits_to_etypes(self.tgs_supported_enctypes) + + def get_ap_krb5_etypes(self): + return self.bits_to_etypes(self.ap_supported_enctypes) + + def set_kvno(self, kvno): + # Sign-extend from 32 bits. + if kvno & 1 << 31: + kvno |= -1 << 31 + self.kvno = kvno + + def get_kvno(self): + return self.kvno + + def set_forced_key(self, etype, hexkey): + etype = int(etype) + contents = binascii.a2b_hex(hexkey) + key = kcrypto.Key(etype, contents) + self.forced_keys[etype] = RodcPacEncryptionKey(key, self.kvno) + + # Also set the NT hash of computer accounts for which we don’t know the + # password. + if etype == kcrypto.Enctype.RC4 and self.get_password() is None: + nt_hash = samr.Password() + nt_hash.hash = list(contents) + + self.set_nt_hash(nt_hash) + + def get_forced_key(self, etype): + etype = int(etype) + return self.forced_keys.get(etype) + + def set_forced_salt(self, salt): + self.forced_salt = bytes(salt) + + def get_forced_salt(self): + return self.forced_salt + + def get_salt(self): + if self.forced_salt is not None: + return self.forced_salt + + upn = self.get_upn() + if upn is not None: + salt_name = upn.rsplit('@', 1)[0].replace('/', '') + else: + salt_name = self.get_username() + + secure_schannel_type = self.get_secure_channel_type() + if secure_schannel_type in [SEC_CHAN_WKSTA,SEC_CHAN_BDC]: + salt_name = self.get_username().lower() + if salt_name[-1] == '$': + salt_name = salt_name[:-1] + salt_string = '%shost%s.%s' % ( + self.get_realm().upper(), + salt_name, + self.get_realm().lower()) + else: + salt_string = self.get_realm().upper() + salt_name + + return salt_string.encode('utf-8') + + def set_dn(self, dn): + self.dn = dn + + def get_dn(self): + return self.dn + + def set_spn(self, spn): + self.spn = spn + + def get_spn(self): + return self.spn + + def set_upn(self, upn): + self.upn = upn + + def get_upn(self): + return self.upn + + def set_sid(self, sid): + self.sid = sid + + def get_sid(self): + return self.sid + + def get_rid(self): + sid = self.get_sid() + if sid is None: + return None + + _, rid = sid.rsplit('-', 1) + return int(rid) + + def set_type(self, account_type): + self.account_type = account_type + + def get_type(self): + return self.account_type + + def update_password(self, password): + self.set_password(password) + self.set_kvno(self.get_kvno() + 1) + + def get_private_key(self): + if self._private_key is None: + # Generate a new keypair. + self._private_key = asymmetric.rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend() + ) + + return self._private_key + + def get_public_key(self): + return self.get_private_key().public_key() + + +class KerberosTicketCreds: + __slots__ = [ + 'cname', + 'crealm', + 'decryption_key', + 'encpart_private', + 'session_key', + 'sname', + 'srealm', + 'ticket_private', + 'ticket', + ] + + def __init__(self, ticket, session_key, + crealm=None, cname=None, + srealm=None, sname=None, + decryption_key=None, + ticket_private=None, + encpart_private=None): + self.ticket = ticket + self.session_key = session_key + self.crealm = crealm + self.cname = cname + self.srealm = srealm + self.sname = sname + self.decryption_key = decryption_key + self.ticket_private = ticket_private + self.encpart_private = encpart_private + + def set_sname(self, sname): + self.ticket['sname'] = sname + self.sname = sname + + +class PkInit(Enum): + NOT_USED = object() + PUBLIC_KEY = object() + DIFFIE_HELLMAN = object() + + +class RawKerberosTest(TestCase): + """A raw Kerberos Test case.""" + + class KpasswdMode(Enum): + SET = object() + CHANGE = object() + + # The location of a SID within the PAC + class SidType(Enum): + BASE_SID = object() # in info3.base.groups + EXTRA_SID = object() # in info3.sids + RESOURCE_SID = object() # in resource_groups + PRIMARY_GID = object() # the (sole) primary group + + def __repr__(self): + return self.__str__() + + pac_checksum_types = {krb5pac.PAC_TYPE_SRV_CHECKSUM, + krb5pac.PAC_TYPE_KDC_CHECKSUM, + krb5pac.PAC_TYPE_TICKET_CHECKSUM, + krb5pac.PAC_TYPE_FULL_CHECKSUM} + + etypes_to_test = ( + {"value": -1111, "name": "dummy", }, + {"value": kcrypto.Enctype.AES256, "name": "aes128", }, + {"value": kcrypto.Enctype.AES128, "name": "aes256", }, + {"value": kcrypto.Enctype.RC4, "name": "rc4", }, + ) + + expect_padata_outer = object() + + setup_etype_test_permutations_done = False + + @classmethod + def setup_etype_test_permutations(cls): + if cls.setup_etype_test_permutations_done: + return + + res = [] + + num_idxs = len(cls.etypes_to_test) + permutations = [] + for num in range(1, num_idxs + 1): + chunk = list(itertools.permutations(range(num_idxs), num)) + for e in chunk: + el = list(e) + permutations.append(el) + + for p in permutations: + name = None + etypes = () + for idx in p: + n = cls.etypes_to_test[idx]["name"] + if name is None: + name = n + else: + name += "_%s" % n + etypes += (cls.etypes_to_test[idx]["value"],) + + r = {"name": name, "etypes": etypes, } + res.append(r) + + cls.etype_test_permutations = res + cls.setup_etype_test_permutations_done = True + + @classmethod + def etype_test_permutation_name_idx(cls): + cls.setup_etype_test_permutations() + res = [] + idx = 0 + for e in cls.etype_test_permutations: + r = (e['name'], idx) + idx += 1 + res.append(r) + return res + + def etype_test_permutation_by_idx(self, idx): + e = self.etype_test_permutations[idx] + return (e['name'], e['etypes']) + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.host = samba.tests.env_get_var_value('SERVER') + cls.dc_host = samba.tests.env_get_var_value('DC_SERVER') + + # A dictionary containing credentials that have already been + # obtained. + cls.creds_dict = {} + + kdc_fast_support = samba.tests.env_get_var_value('FAST_SUPPORT', + allow_missing=True) + if kdc_fast_support is None: + kdc_fast_support = '0' + cls.kdc_fast_support = bool(int(kdc_fast_support)) + + kdc_claims_support = samba.tests.env_get_var_value('CLAIMS_SUPPORT', + allow_missing=True) + if kdc_claims_support is None: + kdc_claims_support = '0' + cls.kdc_claims_support = bool(int(kdc_claims_support)) + + kdc_compound_id_support = samba.tests.env_get_var_value( + 'COMPOUND_ID_SUPPORT', + allow_missing=True) + if kdc_compound_id_support is None: + kdc_compound_id_support = '0' + cls.kdc_compound_id_support = bool(int(kdc_compound_id_support)) + + tkt_sig_support = samba.tests.env_get_var_value('TKT_SIG_SUPPORT', + allow_missing=True) + if tkt_sig_support is None: + tkt_sig_support = '1' + cls.tkt_sig_support = bool(int(tkt_sig_support)) + + full_sig_support = samba.tests.env_get_var_value('FULL_SIG_SUPPORT', + allow_missing=True) + if full_sig_support is None: + full_sig_support = '1' + cls.full_sig_support = bool(int(full_sig_support)) + + expect_pac = samba.tests.env_get_var_value('EXPECT_PAC', + allow_missing=True) + if expect_pac is None: + expect_pac = '1' + cls.expect_pac = bool(int(expect_pac)) + + expect_extra_pac_buffers = samba.tests.env_get_var_value( + 'EXPECT_EXTRA_PAC_BUFFERS', + allow_missing=True) + if expect_extra_pac_buffers is None: + expect_extra_pac_buffers = '1' + cls.expect_extra_pac_buffers = bool(int(expect_extra_pac_buffers)) + + cname_checking = samba.tests.env_get_var_value('CHECK_CNAME', + allow_missing=True) + if cname_checking is None: + cname_checking = '1' + cls.cname_checking = bool(int(cname_checking)) + + padata_checking = samba.tests.env_get_var_value('CHECK_PADATA', + allow_missing=True) + if padata_checking is None: + padata_checking = '1' + cls.padata_checking = bool(int(padata_checking)) + + kadmin_is_tgs = samba.tests.env_get_var_value('KADMIN_IS_TGS', + allow_missing=True) + if kadmin_is_tgs is None: + kadmin_is_tgs = '0' + cls.kadmin_is_tgs = bool(int(kadmin_is_tgs)) + + default_etypes = samba.tests.env_get_var_value('DEFAULT_ETYPES', + allow_missing=True) + if default_etypes is not None: + default_etypes = int(default_etypes) + cls.default_etypes = default_etypes + + forced_rc4 = samba.tests.env_get_var_value('FORCED_RC4', + allow_missing=True) + if forced_rc4 is None: + forced_rc4 = '0' + cls.forced_rc4 = bool(int(forced_rc4)) + + expect_nt_hash = samba.tests.env_get_var_value('EXPECT_NT_HASH', + allow_missing=True) + if expect_nt_hash is None: + expect_nt_hash = '1' + cls.expect_nt_hash = bool(int(expect_nt_hash)) + + expect_nt_status = samba.tests.env_get_var_value('EXPECT_NT_STATUS', + allow_missing=True) + if expect_nt_status is None: + expect_nt_status = '1' + cls.expect_nt_status = bool(int(expect_nt_status)) + + crash_windows = samba.tests.env_get_var_value('CRASH_WINDOWS', + allow_missing=True) + if crash_windows is None: + crash_windows = '1' + cls.crash_windows = bool(int(crash_windows)) + + def setUp(self): + super().setUp() + self.do_asn1_print = False + self.do_hexdump = False + + strict_checking = samba.tests.env_get_var_value('STRICT_CHECKING', + allow_missing=True) + if strict_checking is None: + strict_checking = '1' + self.strict_checking = bool(int(strict_checking)) + + self.s = None + + self.unspecified_kvno = object() + + def tearDown(self): + self._disconnect("tearDown") + super().tearDown() + + def _disconnect(self, reason): + if self.s is None: + return + self.s.close() + self.s = None + if self.do_hexdump: + sys.stderr.write("disconnect[%s]\n" % reason) + + def _connect_tcp(self, host, port=None): + if port is None: + port = 88 + try: + self.a = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, socket.SOL_TCP, + 0) + self.s = socket.socket(self.a[0][0], self.a[0][1], self.a[0][2]) + self.s.settimeout(10) + self.s.connect(self.a[0][4]) + except socket.error: + self.s.close() + raise + + def connect(self, host, port=None): + self.assertNotConnected() + self._connect_tcp(host, port) + if self.do_hexdump: + sys.stderr.write("connected[%s]\n" % host) + + def env_get_var(self, varname, prefix, + fallback_default=True, + allow_missing=False): + val = None + if prefix is not None: + allow_missing_prefix = allow_missing or fallback_default + val = samba.tests.env_get_var_value( + '%s_%s' % (prefix, varname), + allow_missing=allow_missing_prefix) + else: + fallback_default = True + if val is None and fallback_default: + val = samba.tests.env_get_var_value(varname, + allow_missing=allow_missing) + return val + + def _get_krb5_creds_from_env(self, prefix, + default_username=None, + allow_missing_password=False, + allow_missing_keys=True, + require_strongest_key=False): + c = KerberosCredentials() + c.guess() + + domain = self.env_get_var('DOMAIN', prefix) + realm = self.env_get_var('REALM', prefix) + allow_missing_username = default_username is not None + username = self.env_get_var('USERNAME', prefix, + fallback_default=False, + allow_missing=allow_missing_username) + if username is None: + username = default_username + password = self.env_get_var('PASSWORD', prefix, + fallback_default=False, + allow_missing=allow_missing_password) + c.set_domain(domain) + c.set_realm(realm) + c.set_username(username) + if password is not None: + c.set_password(password) + as_supported_enctypes = self.env_get_var('AS_SUPPORTED_ENCTYPES', + prefix, allow_missing=True) + if as_supported_enctypes is not None: + c.set_as_supported_enctypes(as_supported_enctypes) + tgs_supported_enctypes = self.env_get_var('TGS_SUPPORTED_ENCTYPES', + prefix, allow_missing=True) + if tgs_supported_enctypes is not None: + c.set_tgs_supported_enctypes(tgs_supported_enctypes) + ap_supported_enctypes = self.env_get_var('AP_SUPPORTED_ENCTYPES', + prefix, allow_missing=True) + if ap_supported_enctypes is not None: + c.set_ap_supported_enctypes(ap_supported_enctypes) + + if require_strongest_key: + kvno_allow_missing = False + if password is None: + aes256_allow_missing = False + else: + aes256_allow_missing = True + else: + kvno_allow_missing = allow_missing_keys + aes256_allow_missing = allow_missing_keys + kvno = self.env_get_var('KVNO', prefix, + fallback_default=False, + allow_missing=kvno_allow_missing) + if kvno is not None: + c.set_kvno(int(kvno)) + aes256_key = self.env_get_var('AES256_KEY_HEX', prefix, + fallback_default=False, + allow_missing=aes256_allow_missing) + if aes256_key is not None: + c.set_forced_key(kcrypto.Enctype.AES256, aes256_key) + aes128_key = self.env_get_var('AES128_KEY_HEX', prefix, + fallback_default=False, + allow_missing=True) + if aes128_key is not None: + c.set_forced_key(kcrypto.Enctype.AES128, aes128_key) + rc4_key = self.env_get_var('RC4_KEY_HEX', prefix, + fallback_default=False, allow_missing=True) + if rc4_key is not None: + c.set_forced_key(kcrypto.Enctype.RC4, rc4_key) + + if not allow_missing_keys: + self.assertTrue(c.forced_keys, + 'Please supply %s encryption keys ' + 'in environment' % prefix) + + return c + + def _get_krb5_creds(self, + prefix, + default_username=None, + allow_missing_password=False, + allow_missing_keys=True, + require_strongest_key=False, + fallback_creds_fn=None): + if prefix in self.creds_dict: + return self.creds_dict[prefix] + + # We don't have the credentials already + creds = None + env_err = None + try: + # Try to obtain them from the environment + creds = self._get_krb5_creds_from_env( + prefix, + default_username=default_username, + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys, + require_strongest_key=require_strongest_key) + except Exception as err: + # An error occurred, so save it for later + env_err = err + else: + self.assertIsNotNone(creds) + # Save the obtained credentials + self.creds_dict[prefix] = creds + return creds + + if fallback_creds_fn is not None: + try: + # Try to use the fallback method + creds = fallback_creds_fn() + except Exception as err: + print("ERROR FROM ENV: %r" % (env_err)) + print("FALLBACK-FN: %s" % (fallback_creds_fn)) + print("FALLBACK-ERROR: %r" % (err)) + else: + self.assertIsNotNone(creds) + # Save the obtained credentials + self.creds_dict[prefix] = creds + return creds + + # Both methods failed, so raise the exception from the + # environment method + raise env_err + + def get_user_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + c = self._get_krb5_creds(prefix=None, + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys) + return c + + def get_service_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + c = self._get_krb5_creds(prefix='SERVICE', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys) + return c + + def get_client_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + c = self._get_krb5_creds(prefix='CLIENT', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys) + return c + + def get_server_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + c = self._get_krb5_creds(prefix='SERVER', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys) + return c + + def get_admin_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + c = self._get_krb5_creds(prefix='ADMIN', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys) + c.set_gensec_features(c.get_gensec_features() | FEATURE_SEAL) + c.set_workstation('') + return c + + def get_rodc_krbtgt_creds(self, + require_keys=True, + require_strongest_key=False): + if require_strongest_key: + self.assertTrue(require_keys) + c = self._get_krb5_creds(prefix='RODC_KRBTGT', + allow_missing_password=True, + allow_missing_keys=not require_keys, + require_strongest_key=require_strongest_key) + return c + + def get_krbtgt_creds(self, + require_keys=True, + require_strongest_key=False): + if require_strongest_key: + self.assertTrue(require_keys) + c = self._get_krb5_creds(prefix='KRBTGT', + default_username='krbtgt', + allow_missing_password=True, + allow_missing_keys=not require_keys, + require_strongest_key=require_strongest_key) + return c + + def get_anon_creds(self): + c = Credentials() + c.set_anonymous() + return c + + # Overridden by KDCBaseTest. At this level we don't know what actual + # enctypes are supported, so the best we can do is go by whether NT hashes + # are expected and whether the account is a workstation or not. This + # matches the behaviour that tests expect by default. + def get_default_enctypes(self, creds): + self.assertIsNotNone(creds) + + default_enctypes = [ + kcrypto.Enctype.AES256, + kcrypto.Enctype.AES128, + ] + + if self.expect_nt_hash or creds.get_workstation(): + default_enctypes.append(kcrypto.Enctype.RC4) + + return default_enctypes + + def asn1_dump(self, name, obj, asn1_print=None): + if asn1_print is None: + asn1_print = self.do_asn1_print + if asn1_print: + if name is not None: + sys.stderr.write("%s:\n%s" % (name, obj)) + else: + sys.stderr.write("%s" % (obj)) + + def hex_dump(self, name, blob, hexdump=None): + if hexdump is None: + hexdump = self.do_hexdump + if hexdump: + sys.stderr.write( + "%s: %d\n%s" % (name, len(blob), self.hexdump(blob))) + + def der_decode( + self, + blob, + asn1Spec=None, + native_encode=True, + asn1_print=None, + hexdump=None): + if asn1Spec is not None: + class_name = type(asn1Spec).__name__.split(':')[0] + else: + class_name = "<None-asn1Spec>" + self.hex_dump(class_name, blob, hexdump=hexdump) + obj, _ = pyasn1_der_decode(blob, asn1Spec=asn1Spec) + self.asn1_dump(None, obj, asn1_print=asn1_print) + if native_encode: + obj = pyasn1_native_encode(obj) + return obj + + def der_encode( + self, + obj, + asn1Spec=None, + native_decode=True, + asn1_print=None, + hexdump=None): + if native_decode: + obj = pyasn1_native_decode(obj, asn1Spec=asn1Spec) + class_name = type(obj).__name__.split(':')[0] + if class_name is not None: + self.asn1_dump(None, obj, asn1_print=asn1_print) + blob = pyasn1_der_encode(obj) + if class_name is not None: + self.hex_dump(class_name, blob, hexdump=hexdump) + return blob + + def send_pdu(self, req, asn1_print=None, hexdump=None): + k5_pdu = self.der_encode( + req, native_decode=False, asn1_print=asn1_print, hexdump=False) + self.send_msg(k5_pdu, hexdump=hexdump) + + def send_msg(self, msg, hexdump=None): + header = struct.pack('>I', len(msg)) + req_pdu = header + req_pdu += msg + self.hex_dump("send_msg", header, hexdump=hexdump) + self.hex_dump("send_msg", msg, hexdump=hexdump) + + try: + while True: + sent = self.s.send(req_pdu, 0) + if sent == len(req_pdu): + return + req_pdu = req_pdu[sent:] + except socket.error as e: + self._disconnect("send_msg: %s" % e) + raise + + def recv_raw(self, num_recv=0xffff, hexdump=None, timeout=None): + rep_pdu = None + try: + if timeout is not None: + self.s.settimeout(timeout) + rep_pdu = self.s.recv(num_recv, 0) + self.s.settimeout(10) + if len(rep_pdu) == 0: + self._disconnect("recv_raw: EOF") + return None + self.hex_dump("recv_raw", rep_pdu, hexdump=hexdump) + except socket.timeout: + self.s.settimeout(10) + sys.stderr.write("recv_raw: TIMEOUT\n") + except socket.error as e: + self._disconnect("recv_raw: %s" % e) + raise + return rep_pdu + + def recv_pdu_raw(self, asn1_print=None, hexdump=None, timeout=None): + raw_pdu = self.recv_raw( + num_recv=4, hexdump=hexdump, timeout=timeout) + if raw_pdu is None: + return None + header = struct.unpack(">I", raw_pdu[0:4]) + k5_len = header[0] + if k5_len == 0: + return "" + missing = k5_len + rep_pdu = b'' + while missing > 0: + raw_pdu = self.recv_raw( + num_recv=missing, hexdump=hexdump, timeout=timeout) + self.assertGreaterEqual(len(raw_pdu), 1) + rep_pdu += raw_pdu + missing = k5_len - len(rep_pdu) + return rep_pdu + + def recv_reply(self, asn1_print=None, hexdump=None, timeout=None): + rep_pdu = self.recv_pdu_raw(asn1_print=asn1_print, + hexdump=hexdump, + timeout=timeout) + if not rep_pdu: + return None, rep_pdu + k5_raw = self.der_decode( + rep_pdu, + asn1Spec=None, + native_encode=False, + asn1_print=False, + hexdump=False) + pvno = k5_raw['field-0'] + self.assertEqual(pvno, 5) + msg_type = k5_raw['field-1'] + self.assertIn(msg_type, [KRB_AS_REP, KRB_TGS_REP, KRB_ERROR]) + if msg_type == KRB_AS_REP: + asn1Spec = krb5_asn1.AS_REP() + elif msg_type == KRB_TGS_REP: + asn1Spec = krb5_asn1.TGS_REP() + elif msg_type == KRB_ERROR: + asn1Spec = krb5_asn1.KRB_ERROR() + rep = self.der_decode(rep_pdu, asn1Spec=asn1Spec, + asn1_print=asn1_print, hexdump=False) + return (rep, rep_pdu) + + def recv_pdu(self, asn1_print=None, hexdump=None, timeout=None): + (rep, rep_pdu) = self.recv_reply(asn1_print=asn1_print, + hexdump=hexdump, + timeout=timeout) + return rep + + def assertIsConnected(self): + self.assertIsNotNone(self.s, msg="Not connected") + + def assertNotConnected(self): + self.assertIsNone(self.s, msg="Is connected") + + def send_recv_transaction( + self, + req, + asn1_print=None, + hexdump=None, + timeout=None, + to_rodc=False): + host = self.host if to_rodc else self.dc_host + self.connect(host) + try: + self.send_pdu(req, asn1_print=asn1_print, hexdump=hexdump) + rep = self.recv_pdu( + asn1_print=asn1_print, hexdump=hexdump, timeout=timeout) + except Exception: + self._disconnect("transaction failed") + raise + self._disconnect("transaction done") + return rep + + def getElementValue(self, obj, elem): + return obj.get(elem) + + def assertElementMissing(self, obj, elem): + v = self.getElementValue(obj, elem) + self.assertIsNone(v) + + def assertElementPresent(self, obj, elem, expect_empty=False): + v = self.getElementValue(obj, elem) + self.assertIsNotNone(v) + if self.strict_checking: + if isinstance(v, collections.abc.Container): + if expect_empty: + self.assertEqual(0, len(v)) + else: + self.assertNotEqual(0, len(v)) + + def assertElementEqual(self, obj, elem, value): + v = self.getElementValue(obj, elem) + self.assertIsNotNone(v) + self.assertEqual(v, value) + + def assertElementEqualUTF8(self, obj, elem, value): + v = self.getElementValue(obj, elem) + self.assertIsNotNone(v) + self.assertEqual(v, bytes(value, 'utf8')) + + def assertPrincipalEqual(self, princ1, princ2): + self.assertEqual(princ1['name-type'], princ2['name-type']) + self.assertEqual( + len(princ1['name-string']), + len(princ2['name-string']), + msg="princ1=%s != princ2=%s" % (princ1, princ2)) + for idx in range(len(princ1['name-string'])): + self.assertEqual( + princ1['name-string'][idx], + princ2['name-string'][idx], + msg="princ1=%s != princ2=%s" % (princ1, princ2)) + + def assertElementEqualPrincipal(self, obj, elem, value): + v = self.getElementValue(obj, elem) + self.assertIsNotNone(v) + v = pyasn1_native_decode(v, asn1Spec=krb5_asn1.PrincipalName()) + self.assertPrincipalEqual(v, value) + + def assertElementKVNO(self, obj, elem, value): + v = self.getElementValue(obj, elem) + if value == "autodetect": + value = v + if value is not None: + self.assertIsNotNone(v) + # The value on the wire should never be 0 + self.assertNotEqual(v, 0) + # unspecified_kvno means we don't know the kvno, + # but want to enforce its presence + if value is not self.unspecified_kvno: + value = int(value) + self.assertNotEqual(value, 0) + self.assertEqual(v, value) + else: + self.assertIsNone(v) + + def assertElementFlags(self, obj, elem, expected, unexpected): + v = self.getElementValue(obj, elem) + self.assertIsNotNone(v) + if expected is not None: + self.assertIsInstance(expected, krb5_asn1.TicketFlags) + for i, flag in enumerate(expected): + if flag == 1: + self.assertEqual('1', v[i], + f"'{expected.namedValues[i]}' " + f"expected in {v}") + if unexpected is not None: + self.assertIsInstance(unexpected, krb5_asn1.TicketFlags) + for i, flag in enumerate(unexpected): + if flag == 1: + self.assertEqual('0', v[i], + f"'{unexpected.namedValues[i]}' " + f"unexpected in {v}") + + def assertSequenceElementsEqual(self, expected, got, *, + require_strict=None, + unchecked=None, + require_ordered=True): + if self.strict_checking and require_ordered and not unchecked: + self.assertEqual(expected, got) + else: + fail_msg = f'expected: {expected} got: {got}' + + ignored = set() + if unchecked: + ignored.update(unchecked) + if require_strict and not self.strict_checking: + ignored.update(require_strict) + + if ignored: + fail_msg += f' (ignoring: {ignored})' + expected = (x for x in expected if x not in ignored) + got = (x for x in got if x not in ignored) + + self.assertCountEqual(expected, got, fail_msg) + + def get_KerberosTimeWithUsec(self, epoch=None, offset=None): + if epoch is None: + epoch = time.time() + if offset is not None: + epoch = epoch + int(offset) + dt = datetime.datetime.fromtimestamp(epoch, tz=datetime.timezone.utc) + return (dt.strftime("%Y%m%d%H%M%SZ"), dt.microsecond) + + def get_KerberosTime(self, epoch=None, offset=None): + (s, _) = self.get_KerberosTimeWithUsec(epoch=epoch, offset=offset) + return s + + def get_EpochFromKerberosTime(self, kerberos_time): + if isinstance(kerberos_time, bytes): + kerberos_time = kerberos_time.decode() + + epoch = datetime.datetime.strptime(kerberos_time, + '%Y%m%d%H%M%SZ') + epoch = epoch.replace(tzinfo=datetime.timezone.utc) + epoch = int(epoch.timestamp()) + + return epoch + + def get_Nonce(self): + nonce_min = 0x7f000000 + nonce_max = 0x7fffffff + v = random.randint(nonce_min, nonce_max) + return v + + def get_pa_dict(self, pa_data): + pa_dict = {} + + if pa_data is not None: + for pa in pa_data: + pa_type = pa['padata-type'] + if pa_type in pa_dict: + raise RuntimeError(f'Duplicate type {pa_type}') + pa_dict[pa_type] = pa['padata-value'] + + return pa_dict + + def SessionKey_create(self, etype, contents, kvno=None): + key = kcrypto.Key(etype, contents) + return RodcPacEncryptionKey(key, kvno) + + def PasswordKey_create(self, etype=None, pwd=None, salt=None, kvno=None, + params=None): + self.assertIsNotNone(pwd) + self.assertIsNotNone(salt) + key = kcrypto.string_to_key(etype, pwd, salt, params=params) + return RodcPacEncryptionKey(key, kvno) + + def PasswordKey_from_etype_info2(self, creds, etype_info2, kvno=None): + e = etype_info2['etype'] + salt = etype_info2.get('salt') + _params = etype_info2.get('s2kparams') + return self.PasswordKey_from_etype(creds, e, + kvno=kvno, + salt=salt) + + def PasswordKey_from_creds(self, creds, etype): + kvno = creds.get_kvno() + salt = creds.get_salt() + return self.PasswordKey_from_etype(creds, etype, + kvno=kvno, + salt=salt) + + def PasswordKey_from_etype(self, creds, etype, kvno=None, salt=None): + if etype == kcrypto.Enctype.RC4: + nthash = creds.get_nt_hash() + return self.SessionKey_create(etype=etype, contents=nthash, kvno=kvno) + + password = creds.get_password().encode('utf-8') + return self.PasswordKey_create( + etype=etype, pwd=password, salt=salt, kvno=kvno) + + def TicketDecryptionKey_from_creds(self, creds, etype=None): + + if etype is None: + etypes = creds.get_tgs_krb5_etypes() + if etypes and etypes[0] not in (kcrypto.Enctype.DES_CRC, + kcrypto.Enctype.DES_MD5): + etype = etypes[0] + else: + etype = kcrypto.Enctype.RC4 + + forced_key = creds.get_forced_key(etype) + if forced_key is not None: + return forced_key + + kvno = creds.get_kvno() + + fail_msg = ("%s has no fixed key for etype[%s] kvno[%s] " + "nor a password specified, " % ( + creds.get_username(), etype, kvno)) + + if etype == kcrypto.Enctype.RC4: + nthash = creds.get_nt_hash() + self.assertIsNotNone(nthash, msg=fail_msg) + return self.SessionKey_create(etype=etype, + contents=nthash, + kvno=kvno) + + password = creds.get_password() + self.assertIsNotNone(password, msg=fail_msg) + salt = creds.get_salt() + return self.PasswordKey_create(etype=etype, + pwd=password, + salt=salt, + kvno=kvno) + + def RandomKey(self, etype): + e = kcrypto._get_enctype_profile(etype) + contents = samba.generate_random_bytes(e.keysize) + return self.SessionKey_create(etype=etype, contents=contents) + + def EncryptionKey_import(self, EncryptionKey_obj): + return self.SessionKey_create(EncryptionKey_obj['keytype'], + EncryptionKey_obj['keyvalue']) + + def EncryptedData_create(self, key, usage, plaintext): + # EncryptedData ::= SEQUENCE { + # etype [0] Int32 -- EncryptionType --, + # kvno [1] Int32 OPTIONAL, + # cipher [2] OCTET STRING -- ciphertext + # } + ciphertext = key.encrypt(usage, plaintext) + EncryptedData_obj = { + 'etype': key.etype, + 'cipher': ciphertext + } + if key.kvno is not None: + EncryptedData_obj['kvno'] = key.kvno + return EncryptedData_obj + + def Checksum_create(self, key, usage, plaintext, ctype=None): + # Checksum ::= SEQUENCE { + # cksumtype [0] Int32, + # checksum [1] OCTET STRING + # } + if ctype is None: + ctype = key.ctype + checksum = key.make_checksum(usage, plaintext, ctype=ctype) + Checksum_obj = { + 'cksumtype': ctype, + 'checksum': checksum, + } + return Checksum_obj + + @classmethod + def PrincipalName_create(cls, name_type, names): + # PrincipalName ::= SEQUENCE { + # name-type [0] Int32, + # name-string [1] SEQUENCE OF KerberosString + # } + PrincipalName_obj = { + 'name-type': name_type, + 'name-string': names, + } + return PrincipalName_obj + + def AuthorizationData_create(self, ad_type, ad_data): + # AuthorizationData ::= SEQUENCE { + # ad-type [0] Int32, + # ad-data [1] OCTET STRING + # } + AUTH_DATA_obj = { + 'ad-type': ad_type, + 'ad-data': ad_data + } + return AUTH_DATA_obj + + def PA_DATA_create(self, padata_type, padata_value): + # PA-DATA ::= SEQUENCE { + # -- NOTE: first tag is [1], not [0] + # padata-type [1] Int32, + # padata-value [2] OCTET STRING -- might be encoded AP-REQ + # } + PA_DATA_obj = { + 'padata-type': padata_type, + 'padata-value': padata_value, + } + return PA_DATA_obj + + def PA_ENC_TS_ENC_create(self, ts, usec): + # PA-ENC-TS-ENC ::= SEQUENCE { + # patimestamp[0] KerberosTime, -- client's time + # pausec[1] krb5int32 OPTIONAL + # } + PA_ENC_TS_ENC_obj = { + 'patimestamp': ts, + 'pausec': usec, + } + return PA_ENC_TS_ENC_obj + + def PA_PAC_OPTIONS_create(self, options): + # PA-PAC-OPTIONS ::= SEQUENCE { + # options [0] PACOptionFlags + # } + PA_PAC_OPTIONS_obj = { + 'options': options + } + return PA_PAC_OPTIONS_obj + + def KRB_FAST_ARMOR_create(self, armor_type, armor_value): + # KrbFastArmor ::= SEQUENCE { + # armor-type [0] Int32, + # armor-value [1] OCTET STRING, + # ... + # } + KRB_FAST_ARMOR_obj = { + 'armor-type': armor_type, + 'armor-value': armor_value + } + return KRB_FAST_ARMOR_obj + + def KRB_FAST_REQ_create(self, fast_options, padata, req_body): + # KrbFastReq ::= SEQUENCE { + # fast-options [0] FastOptions, + # padata [1] SEQUENCE OF PA-DATA, + # req-body [2] KDC-REQ-BODY, + # ... + # } + KRB_FAST_REQ_obj = { + 'fast-options': fast_options, + 'padata': padata, + 'req-body': req_body + } + return KRB_FAST_REQ_obj + + def KRB_FAST_ARMORED_REQ_create(self, armor, req_checksum, enc_fast_req): + # KrbFastArmoredReq ::= SEQUENCE { + # armor [0] KrbFastArmor OPTIONAL, + # req-checksum [1] Checksum, + # enc-fast-req [2] EncryptedData -- KrbFastReq -- + # } + KRB_FAST_ARMORED_REQ_obj = { + 'req-checksum': req_checksum, + 'enc-fast-req': enc_fast_req + } + if armor is not None: + KRB_FAST_ARMORED_REQ_obj['armor'] = armor + return KRB_FAST_ARMORED_REQ_obj + + def PA_FX_FAST_REQUEST_create(self, armored_data): + # PA-FX-FAST-REQUEST ::= CHOICE { + # armored-data [0] KrbFastArmoredReq, + # ... + # } + PA_FX_FAST_REQUEST_obj = { + 'armored-data': armored_data + } + return PA_FX_FAST_REQUEST_obj + + def KERB_PA_PAC_REQUEST_create(self, include_pac, pa_data_create=True): + # KERB-PA-PAC-REQUEST ::= SEQUENCE { + # include-pac[0] BOOLEAN --If TRUE, and no pac present, + # -- include PAC. + # --If FALSE, and PAC present, + # -- remove PAC. + # } + KERB_PA_PAC_REQUEST_obj = { + 'include-pac': include_pac, + } + if not pa_data_create: + return KERB_PA_PAC_REQUEST_obj + pa_pac = self.der_encode(KERB_PA_PAC_REQUEST_obj, + asn1Spec=krb5_asn1.KERB_PA_PAC_REQUEST()) + pa_data = self.PA_DATA_create(PADATA_PAC_REQUEST, pa_pac) + return pa_data + + def get_pa_pac_options(self, options): + pac_options = self.PA_PAC_OPTIONS_create(options) + pac_options = self.der_encode(pac_options, + asn1Spec=krb5_asn1.PA_PAC_OPTIONS()) + pac_options = self.PA_DATA_create(PADATA_PAC_OPTIONS, pac_options) + + return pac_options + + def KDC_REQ_BODY_create(self, + kdc_options, + cname, + realm, + sname, + from_time, + till_time, + renew_time, + nonce, + etypes, + addresses, + additional_tickets, + EncAuthorizationData, + EncAuthorizationData_key, + EncAuthorizationData_usage, + asn1_print=None, + hexdump=None): + # KDC-REQ-BODY ::= SEQUENCE { + # kdc-options [0] KDCOptions, + # cname [1] PrincipalName OPTIONAL + # -- Used only in AS-REQ --, + # realm [2] Realm + # -- Server's realm + # -- Also client's in AS-REQ --, + # sname [3] PrincipalName OPTIONAL, + # from [4] KerberosTime OPTIONAL, + # till [5] KerberosTime, + # rtime [6] KerberosTime OPTIONAL, + # nonce [7] UInt32, + # etype [8] SEQUENCE OF Int32 + # -- EncryptionType + # -- in preference order --, + # addresses [9] HostAddresses OPTIONAL, + # enc-authorization-data [10] EncryptedData OPTIONAL + # -- AuthorizationData --, + # additional-tickets [11] SEQUENCE OF Ticket OPTIONAL + # -- NOTE: not empty + # } + if EncAuthorizationData is not None: + enc_ad_plain = self.der_encode( + EncAuthorizationData, + asn1Spec=krb5_asn1.AuthorizationData(), + asn1_print=asn1_print, + hexdump=hexdump) + enc_ad = self.EncryptedData_create(EncAuthorizationData_key, + EncAuthorizationData_usage, + enc_ad_plain) + else: + enc_ad = None + KDC_REQ_BODY_obj = { + 'kdc-options': kdc_options, + 'realm': realm, + 'till': till_time, + 'nonce': nonce, + 'etype': etypes, + } + if cname is not None: + KDC_REQ_BODY_obj['cname'] = cname + if sname is not None: + KDC_REQ_BODY_obj['sname'] = sname + if from_time is not None: + KDC_REQ_BODY_obj['from'] = from_time + if renew_time is not None: + KDC_REQ_BODY_obj['rtime'] = renew_time + if addresses is not None: + KDC_REQ_BODY_obj['addresses'] = addresses + if enc_ad is not None: + KDC_REQ_BODY_obj['enc-authorization-data'] = enc_ad + if additional_tickets is not None: + KDC_REQ_BODY_obj['additional-tickets'] = additional_tickets + return KDC_REQ_BODY_obj + + def KDC_REQ_create(self, + msg_type, + padata, + req_body, + asn1Spec=None, + asn1_print=None, + hexdump=None): + # KDC-REQ ::= SEQUENCE { + # -- NOTE: first tag is [1], not [0] + # pvno [1] INTEGER (5) , + # msg-type [2] INTEGER (10 -- AS -- | 12 -- TGS --), + # padata [3] SEQUENCE OF PA-DATA OPTIONAL + # -- NOTE: not empty --, + # req-body [4] KDC-REQ-BODY + # } + # + KDC_REQ_obj = { + 'pvno': 5, + 'msg-type': msg_type, + 'req-body': req_body, + } + if padata is not None: + KDC_REQ_obj['padata'] = padata + if asn1Spec is not None: + KDC_REQ_decoded = pyasn1_native_decode( + KDC_REQ_obj, asn1Spec=asn1Spec) + else: + KDC_REQ_decoded = None + return KDC_REQ_obj, KDC_REQ_decoded + + def AS_REQ_create(self, + padata, # optional + kdc_options, # required + cname, # optional + realm, # required + sname, # optional + from_time, # optional + till_time, # required + renew_time, # optional + nonce, # required + etypes, # required + addresses, # optional + additional_tickets, + native_decoded_only=True, + asn1_print=None, + hexdump=None): + # KDC-REQ ::= SEQUENCE { + # -- NOTE: first tag is [1], not [0] + # pvno [1] INTEGER (5) , + # msg-type [2] INTEGER (10 -- AS -- | 12 -- TGS --), + # padata [3] SEQUENCE OF PA-DATA OPTIONAL + # -- NOTE: not empty --, + # req-body [4] KDC-REQ-BODY + # } + # + # KDC-REQ-BODY ::= SEQUENCE { + # kdc-options [0] KDCOptions, + # cname [1] PrincipalName OPTIONAL + # -- Used only in AS-REQ --, + # realm [2] Realm + # -- Server's realm + # -- Also client's in AS-REQ --, + # sname [3] PrincipalName OPTIONAL, + # from [4] KerberosTime OPTIONAL, + # till [5] KerberosTime, + # rtime [6] KerberosTime OPTIONAL, + # nonce [7] UInt32, + # etype [8] SEQUENCE OF Int32 + # -- EncryptionType + # -- in preference order --, + # addresses [9] HostAddresses OPTIONAL, + # enc-authorization-data [10] EncryptedData OPTIONAL + # -- AuthorizationData --, + # additional-tickets [11] SEQUENCE OF Ticket OPTIONAL + # -- NOTE: not empty + # } + KDC_REQ_BODY_obj = self.KDC_REQ_BODY_create( + kdc_options, + cname, + realm, + sname, + from_time, + till_time, + renew_time, + nonce, + etypes, + addresses, + additional_tickets, + EncAuthorizationData=None, + EncAuthorizationData_key=None, + EncAuthorizationData_usage=None, + asn1_print=asn1_print, + hexdump=hexdump) + obj, decoded = self.KDC_REQ_create( + msg_type=KRB_AS_REQ, + padata=padata, + req_body=KDC_REQ_BODY_obj, + asn1Spec=krb5_asn1.AS_REQ(), + asn1_print=asn1_print, + hexdump=hexdump) + if native_decoded_only: + return decoded + return decoded, obj + + def AP_REQ_create(self, ap_options, ticket, authenticator): + # AP-REQ ::= [APPLICATION 14] SEQUENCE { + # pvno [0] INTEGER (5), + # msg-type [1] INTEGER (14), + # ap-options [2] APOptions, + # ticket [3] Ticket, + # authenticator [4] EncryptedData -- Authenticator + # } + AP_REQ_obj = { + 'pvno': 5, + 'msg-type': KRB_AP_REQ, + 'ap-options': ap_options, + 'ticket': ticket, + 'authenticator': authenticator, + } + return AP_REQ_obj + + def Authenticator_create( + self, crealm, cname, cksum, cusec, ctime, subkey, seq_number, + authorization_data): + # -- Unencrypted authenticator + # Authenticator ::= [APPLICATION 2] SEQUENCE { + # authenticator-vno [0] INTEGER (5), + # crealm [1] Realm, + # cname [2] PrincipalName, + # cksum [3] Checksum OPTIONAL, + # cusec [4] Microseconds, + # ctime [5] KerberosTime, + # subkey [6] EncryptionKey OPTIONAL, + # seq-number [7] UInt32 OPTIONAL, + # authorization-data [8] AuthorizationData OPTIONAL + # } + Authenticator_obj = { + 'authenticator-vno': 5, + 'crealm': crealm, + 'cname': cname, + 'cusec': cusec, + 'ctime': ctime, + } + if cksum is not None: + Authenticator_obj['cksum'] = cksum + if subkey is not None: + Authenticator_obj['subkey'] = subkey + if seq_number is not None: + Authenticator_obj['seq-number'] = seq_number + if authorization_data is not None: + Authenticator_obj['authorization-data'] = authorization_data + return Authenticator_obj + + def PKAuthenticator_create(self, + cusec, + ctime, + nonce, + *, + pa_checksum=None, + freshness_token=None, + kdc_name=None, + kdc_realm=None, + win2k_variant=False): + if win2k_variant: + self.assertIsNone(pa_checksum) + self.assertIsNone(freshness_token) + self.assertIsNotNone(kdc_name) + self.assertIsNotNone(kdc_realm) + else: + self.assertIsNone(kdc_name) + self.assertIsNone(kdc_realm) + + pk_authenticator_obj = { + 'cusec': cusec, + 'ctime': ctime, + 'nonce': nonce, + } + if pa_checksum is not None: + pk_authenticator_obj['paChecksum'] = pa_checksum + if freshness_token is not None: + pk_authenticator_obj['freshnessToken'] = freshness_token + if kdc_name is not None: + pk_authenticator_obj['kdcName'] = kdc_name + if kdc_realm is not None: + pk_authenticator_obj['kdcRealm'] = kdc_realm + + return pk_authenticator_obj + + def TGS_REQ_create(self, + padata, # optional + cusec, + ctime, + ticket, + kdc_options, # required + cname, # optional + realm, # required + sname, # optional + from_time, # optional + till_time, # required + renew_time, # optional + nonce, # required + etypes, # required + addresses, # optional + EncAuthorizationData, + EncAuthorizationData_key, + additional_tickets, + ticket_session_key, + authenticator_subkey=None, + body_checksum_type=None, + native_decoded_only=True, + asn1_print=None, + hexdump=None): + # KDC-REQ ::= SEQUENCE { + # -- NOTE: first tag is [1], not [0] + # pvno [1] INTEGER (5) , + # msg-type [2] INTEGER (10 -- AS -- | 12 -- TGS --), + # padata [3] SEQUENCE OF PA-DATA OPTIONAL + # -- NOTE: not empty --, + # req-body [4] KDC-REQ-BODY + # } + # + # KDC-REQ-BODY ::= SEQUENCE { + # kdc-options [0] KDCOptions, + # cname [1] PrincipalName OPTIONAL + # -- Used only in AS-REQ --, + # realm [2] Realm + # -- Server's realm + # -- Also client's in AS-REQ --, + # sname [3] PrincipalName OPTIONAL, + # from [4] KerberosTime OPTIONAL, + # till [5] KerberosTime, + # rtime [6] KerberosTime OPTIONAL, + # nonce [7] UInt32, + # etype [8] SEQUENCE OF Int32 + # -- EncryptionType + # -- in preference order --, + # addresses [9] HostAddresses OPTIONAL, + # enc-authorization-data [10] EncryptedData OPTIONAL + # -- AuthorizationData --, + # additional-tickets [11] SEQUENCE OF Ticket OPTIONAL + # -- NOTE: not empty + # } + + if authenticator_subkey is not None: + EncAuthorizationData_usage = KU_TGS_REQ_AUTH_DAT_SUBKEY + else: + EncAuthorizationData_usage = KU_TGS_REQ_AUTH_DAT_SESSION + + req_body = self.KDC_REQ_BODY_create( + kdc_options=kdc_options, + cname=None, + realm=realm, + sname=sname, + from_time=from_time, + till_time=till_time, + renew_time=renew_time, + nonce=nonce, + etypes=etypes, + addresses=addresses, + additional_tickets=additional_tickets, + EncAuthorizationData=EncAuthorizationData, + EncAuthorizationData_key=EncAuthorizationData_key, + EncAuthorizationData_usage=EncAuthorizationData_usage) + req_body_blob = self.der_encode(req_body, + asn1Spec=krb5_asn1.KDC_REQ_BODY(), + asn1_print=asn1_print, hexdump=hexdump) + + req_body_checksum = self.Checksum_create(ticket_session_key, + KU_TGS_REQ_AUTH_CKSUM, + req_body_blob, + ctype=body_checksum_type) + + subkey_obj = None + if authenticator_subkey is not None: + subkey_obj = authenticator_subkey.export_obj() + seq_number = random.randint(0, 0xfffffffe) + authenticator = self.Authenticator_create( + crealm=realm, + cname=cname, + cksum=req_body_checksum, + cusec=cusec, + ctime=ctime, + subkey=subkey_obj, + seq_number=seq_number, + authorization_data=None) + authenticator = self.der_encode( + authenticator, + asn1Spec=krb5_asn1.Authenticator(), + asn1_print=asn1_print, + hexdump=hexdump) + + authenticator = self.EncryptedData_create( + ticket_session_key, KU_TGS_REQ_AUTH, authenticator) + + ap_options = krb5_asn1.APOptions('0') + ap_req = self.AP_REQ_create(ap_options=str(ap_options), + ticket=ticket, + authenticator=authenticator) + ap_req = self.der_encode(ap_req, asn1Spec=krb5_asn1.AP_REQ(), + asn1_print=asn1_print, hexdump=hexdump) + pa_tgs_req = self.PA_DATA_create(PADATA_KDC_REQ, ap_req) + if padata is not None: + padata.append(pa_tgs_req) + else: + padata = [pa_tgs_req] + + obj, decoded = self.KDC_REQ_create( + msg_type=KRB_TGS_REQ, + padata=padata, + req_body=req_body, + asn1Spec=krb5_asn1.TGS_REQ(), + asn1_print=asn1_print, + hexdump=hexdump) + if native_decoded_only: + return decoded + return decoded, obj + + def PA_S4U2Self_create(self, name, realm, tgt_session_key, ctype=None): + # PA-S4U2Self ::= SEQUENCE { + # name [0] PrincipalName, + # realm [1] Realm, + # cksum [2] Checksum, + # auth [3] GeneralString + # } + cksum_data = name['name-type'].to_bytes(4, byteorder='little') + for n in name['name-string']: + cksum_data += n.encode() + cksum_data += realm.encode() + cksum_data += "Kerberos".encode() + cksum = self.Checksum_create(tgt_session_key, + KU_NON_KERB_CKSUM_SALT, + cksum_data, + ctype) + + PA_S4U2Self_obj = { + 'name': name, + 'realm': realm, + 'cksum': cksum, + 'auth': "Kerberos", + } + pa_s4u2self = self.der_encode( + PA_S4U2Self_obj, asn1Spec=krb5_asn1.PA_S4U2Self()) + return self.PA_DATA_create(PADATA_FOR_USER, pa_s4u2self) + + def ChangePasswdDataMS_create(self, + new_password, + target_princ=None, + target_realm=None): + ChangePasswdDataMS_obj = { + 'newpasswd': new_password, + } + if target_princ is not None: + ChangePasswdDataMS_obj['targname'] = target_princ + if target_realm is not None: + ChangePasswdDataMS_obj['targrealm'] = target_realm + + change_password_data = self.der_encode( + ChangePasswdDataMS_obj, asn1Spec=krb5_asn1.ChangePasswdDataMS()) + + return change_password_data + + def KRB_PRIV_create(self, + subkey, + user_data, + s_address, + timestamp=None, + usec=None, + seq_number=None, + r_address=None): + EncKrbPrivPart_obj = { + 'user-data': user_data, + 's-address': s_address, + } + if timestamp is not None: + EncKrbPrivPart_obj['timestamp'] = timestamp + if usec is not None: + EncKrbPrivPart_obj['usec'] = usec + if seq_number is not None: + EncKrbPrivPart_obj['seq-number'] = seq_number + if r_address is not None: + EncKrbPrivPart_obj['r-address'] = r_address + + enc_krb_priv_part = self.der_encode( + EncKrbPrivPart_obj, asn1Spec=krb5_asn1.EncKrbPrivPart()) + + enc_data = self.EncryptedData_create(subkey, + KU_KRB_PRIV, + enc_krb_priv_part) + + KRB_PRIV_obj = { + 'pvno': 5, + 'msg-type': KRB_PRIV, + 'enc-part': enc_data, + } + + krb_priv = self.der_encode( + KRB_PRIV_obj, asn1Spec=krb5_asn1.KRB_PRIV()) + + return krb_priv + + def ContentInfo_create(self, content_type, content): + content_info_obj = { + 'contentType': content_type, + 'content': content, + } + + return content_info_obj + + def EncapsulatedContentInfo_create(self, content_type, content): + encapsulated_content_info_obj = { + 'eContentType': content_type, + 'eContent': content, + } + + return encapsulated_content_info_obj + + def SignedData_create(self, + digest_algorithms, + encap_content_info, + signer_infos, + *, + version=None, + certificates=None, + crls=None): + def is_cert_version_present(version): + return certificates is not None and any( + version in cert for cert in certificates) + + def is_crl_version_present(version): + return crls is not None and any( + version in crl for crl in crls) + + def is_signer_info_version_present(version): + return signer_infos is not None and any( + signer_info['version'] == version + for signer_info in signer_infos) + + def data_version(): + # per RFC5652 5.1: + if is_cert_version_present('other') or ( + is_crl_version_present('other')): + return 5 + + if is_cert_version_present('v2AttrCert'): + return 4 + + if is_cert_version_present('v1AttrCert') or ( + is_signer_info_version_present(3)) or ( + encap_content_info['eContentType'] != krb5_asn1.id_data + ): + return 3 + + return 1 + + if version is None: + version = data_version() + + signed_data_obj = { + 'version': version, + 'digestAlgorithms': digest_algorithms, + 'encapContentInfo': encap_content_info, + 'signerInfos': signer_infos, + } + + if certificates is not None: + signed_data_obj['certificates'] = certificates + if crls is not None: + signed_data_obj['crls'] = crls + + return signed_data_obj + + def AuthPack_create(self, + pk_authenticator, + *, + client_public_value=None, + supported_cms_types=None, + client_dh_nonce=None, + win2k_variant=False): + if win2k_variant: + self.assertIsNone(supported_cms_types) + self.assertIsNone(client_dh_nonce) + + auth_pack_obj = { + 'pkAuthenticator': pk_authenticator, + } + + if client_public_value is not None: + auth_pack_obj['clientPublicValue'] = client_public_value + if supported_cms_types is not None: + auth_pack_obj['supportedCMSTypes'] = supported_cms_types + if client_dh_nonce is not None: + auth_pack_obj['clientDHNonce'] = client_dh_nonce + + return auth_pack_obj + + def PK_AS_REQ_create(self, + signed_auth_pack, + *, + trusted_certifiers=None, + kdc_pk_id=None, + kdc_cert=None, + encryption_cert=None, + win2k_variant=False): + if win2k_variant: + self.assertIsNone(kdc_pk_id) + asn1_spec = krb5_asn1.PA_PK_AS_REQ_Win2k + else: + self.assertIsNone(kdc_cert) + self.assertIsNone(encryption_cert) + asn1_spec = krb5_asn1.PA_PK_AS_REQ + + content_info_obj = self.ContentInfo_create( + krb5_asn1.id_signedData, signed_auth_pack) + content_info = self.der_encode(content_info_obj, + asn1Spec=krb5_asn1.ContentInfo()) + + pk_as_req_obj = { + 'signedAuthPack': content_info, + } + + if trusted_certifiers is not None: + pk_as_req_obj['trustedCertifiers'] = trusted_certifiers + if kdc_pk_id is not None: + pk_as_req_obj['kdcPkId'] = kdc_pk_id + if kdc_cert is not None: + pk_as_req_obj['kdcCert'] = kdc_cert + if encryption_cert is not None: + pk_as_req_obj['encryptionCert'] = encryption_cert + + return self.der_encode(pk_as_req_obj, asn1Spec=asn1_spec()) + + def SignerInfo_create(self, + signer_id, + digest_algorithm, + signature_algorithm, + signature, + *, + version=None, + signed_attrs=None, + unsigned_attrs=None): + if version is None: + # per RFC5652 5.3: + if 'issuerAndSerialNumber' in signer_id: + version = 1 + elif 'subjectKeyIdentifier' in signer_id: + version = 3 + else: + self.fail(f'unknown signer ID version ({signer_id})') + + signer_info_obj = { + 'version': version, + 'sid': signer_id, + 'digestAlgorithm': digest_algorithm, + 'signatureAlgorithm': signature_algorithm, + 'signature': signature, + } + + if signed_attrs is not None: + signer_info_obj['signedAttrs'] = signed_attrs + if unsigned_attrs is not None: + signer_info_obj['unsignedAttrs'] = unsigned_attrs + + return signer_info_obj + + def SignerIdentifier_create(self, *, + issuer_and_serial_number=None, + subject_key_id=None): + if issuer_and_serial_number is not None: + return {'issuerAndSerialNumber': issuer_and_serial_number} + + if subject_key_id is not None: + return {'subjectKeyIdentifier': subject_key_id} + + self.fail('identifier not specified') + + def AlgorithmIdentifier_create(self, + algorithm, + *, + parameters=None): + algorithm_id_obj = { + 'algorithm': algorithm, + } + + if parameters is not None: + algorithm_id_obj['parameters'] = parameters + + return algorithm_id_obj + + def SubjectPublicKeyInfo_create(self, + algorithm, + public_key): + return { + 'algorithm': algorithm, + 'subjectPublicKey': public_key, + } + + def ValidationParms_create(self, + seed, + pgen_counter): + return { + 'seed': seed, + 'pgenCounter': pgen_counter, + } + + def DomainParameters_create(self, + p, + g, + *, + q=None, + j=None, + validation_parms=None): + domain_params_obj = { + 'p': p, + 'g': g, + } + + if q is not None: + domain_params_obj['q'] = q + if j is not None: + domain_params_obj['j'] = j + if validation_parms is not None: + domain_params_obj['validationParms'] = validation_parms + + return domain_params_obj + + def length_in_bytes(self, value): + """Return the length in bytes of an integer once it is encoded as + bytes.""" + + self.assertGreaterEqual(value, 0, 'value must be positive') + self.assertIsInstance(value, int) + + length_in_bits = max(1, math.log2(value + 1)) + length_in_bytes = math.ceil(length_in_bits / 8) + return length_in_bytes + + def bytes_from_int(self, value, *, length=None): + """Return an integer encoded big-endian into bytes of an optionally + specified length. + """ + if length is None: + length = self.length_in_bytes(value) + return value.to_bytes(length, 'big') + + def int_from_bytes(self, data): + """Return an integer decoded from bytes in big-endian format.""" + return int.from_bytes(data, 'big') + + def int_from_bit_string(self, string): + """Return an integer decoded from a bitstring.""" + return int(string, base=2) + + def bit_string_from_int(self, value): + """Return a bitstring encoding of an integer.""" + + string = f'{value:b}' + + # The bitstring must be padded to a multiple of 8 bits in length, or + # pyasn1 will interpret it incorrectly (as if the padding bits were + # present, but on the wrong end). + length = len(string) + padding_len = math.ceil(length / 8) * 8 - length + return '0' * padding_len + string + + def bit_string_from_bytes(self, data): + """Return a bitstring encoding of bytes in big-endian format.""" + value = self.int_from_bytes(data) + return self.bit_string_from_int(value) + + def bytes_from_bit_string(self, string): + """Return big-endian format bytes encoded from a bitstring.""" + value = self.int_from_bit_string(string) + length = math.ceil(len(string) / 8) + return value.to_bytes(length, 'big') + + def asn1_length(self, data): + """Return the ASN.1 encoding of the length of some data.""" + + length = len(data) + + self.assertGreater(length, 0) + if length < 0x80: + return bytes([length]) + + encoding_len = self.length_in_bytes(length) + self.assertLess(encoding_len, 0x80, + 'item is too long to be ASN.1 encoded') + + data = self.bytes_from_int(length, length=encoding_len) + return bytes([0x80 | encoding_len]) + data + + @staticmethod + def octetstring2key(x, enctype): + """This implements the function defined in RFC4556 3.2.3.1 “Using + Diffie-Hellman Key Exchange”.""" + + seedsize = kcrypto.seedsize(enctype) + seed = b'' + + # A counter that cycles through the bytes 0x00–0xff. + counter = itertools.cycle(map(lambda x: bytes([x]), + range(256))) + + while len(seed) < seedsize: + digest = hashes.Hash(hashes.SHA1(), default_backend()) + digest.update(next(counter) + x) + seed += digest.finalize() + + key = kcrypto.random_to_key(enctype, seed[:seedsize]) + return RodcPacEncryptionKey(key, kvno=None) + + def unpad(self, data): + """Return unpadded data.""" + padding_len = data[-1] + expected_padding = bytes([padding_len]) * padding_len + self.assertEqual(expected_padding, data[-padding_len:], + 'invalid padding bytes') + + return data[:-padding_len] + + def try_decode(self, data, module=None): + """Try to decode some data of unknown type with various known ASN.1 + schemata (optionally restricted to those from a particular module) and + print any results that seem promising. For use when debugging. + """ + + if module is None: + # Try a couple of known ASN.1 modules. + self.try_decode(data, krb5_asn1) + self.try_decode(data, pyasn1.type.univ) + + # It’s helpful to stop and give the user a chance to examine the + # results. + self.fail('decoding done') + + names = dir(module) + for name in names: + item = getattr(module, name) + if not callable(item): + continue + + try: + decoded = self.der_decode(data, asn1Spec=item()) + except Exception: + # Initiating the schema or decoding the ASN.1 failed for + # whatever reason. + pass + else: + # Decoding succeeded: print the structure to be examined. + print(f'\t{name}') + pprint(decoded) + + def cipher_from_algorithm(self, algorithm): + if algorithm == str(krb5_asn1.aes256_CBC_PAD): + return algorithms.AES + + if algorithm == str(krb5_asn1.des_EDE3_CBC): + return algorithms.TripleDES + + self.fail(f'unknown cipher algorithm {algorithm}') + + def hash_from_algorithm(self, algorithm): + # Let someone pass in an ObjectIdentifier. + algorithm = str(algorithm) + + if algorithm == str(krb5_asn1.id_sha1): + return hashes.SHA1 + + if algorithm == str(krb5_asn1.sha1WithRSAEncryption): + return hashes.SHA1 + + if algorithm == str(krb5_asn1.rsaEncryption): + return hashes.SHA1 + + if algorithm == str(krb5_asn1.id_pkcs1_sha256WithRSAEncryption): + return hashes.SHA256 + + if algorithm == str(krb5_asn1.id_sha512): + return hashes.SHA512 + + self.fail(f'unknown hash algorithm {algorithm}') + + def hash_from_algorithm_id(self, algorithm_id): + self.assertIsInstance(algorithm_id, dict) + + hash = self.hash_from_algorithm(algorithm_id['algorithm']) + + parameters = algorithm_id.get('parameters') + if self.strict_checking: + self.assertIsNotNone(parameters) + if parameters is not None: + self.assertEqual(b'\x05\x00', parameters) + + return hash + + def create_freshness_token(self, + epoch=None, + *, + offset=None, + krbtgt_creds=None): + timestamp, usec = self.get_KerberosTimeWithUsec(epoch, offset) + + # Encode the freshness token as PA-ENC-TS-ENC. + ts_enc = self.PA_ENC_TS_ENC_create(timestamp, usec) + ts_enc = self.der_encode(ts_enc, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + if krbtgt_creds is None: + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + # Encrypt the freshness token. + freshness = self.EncryptedData_create(krbtgt_key, KU_AS_FRESHNESS, ts_enc) + + freshness_token = self.der_encode(freshness, + asn1Spec=krb5_asn1.EncryptedData()) + + # Prepend a couple of zero bytes. + freshness_token = bytes(2) + freshness_token + + return freshness_token + + def kpasswd_create(self, + subkey, + user_data, + version, + seq_number, + ap_req, + local_address, + remote_address): + self.assertIsNotNone(self.s, 'call self.connect() first') + + timestamp, usec = self.get_KerberosTimeWithUsec() + + krb_priv = self.KRB_PRIV_create(subkey, + user_data, + s_address=local_address, + timestamp=timestamp, + usec=usec, + seq_number=seq_number, + r_address=remote_address) + + size = 6 + len(ap_req) + len(krb_priv) + self.assertLess(size, 0x10000) + + msg = bytearray() + msg.append(size >> 8) + msg.append(size & 0xff) + msg.append(version >> 8) + msg.append(version & 0xff) + msg.append(len(ap_req) >> 8) + msg.append(len(ap_req) & 0xff) + # Note: for sets, there could be a little-endian four-byte length here. + + msg.extend(ap_req) + msg.extend(krb_priv) + + return msg + + def get_enc_part(self, obj, key, usage): + self.assertElementEqual(obj, 'pvno', 5) + + enc_part = obj['enc-part'] + self.assertElementEqual(enc_part, 'etype', key.etype) + self.assertElementKVNO(enc_part, 'kvno', key.kvno) + + enc_part = key.decrypt(usage, enc_part['cipher']) + + return enc_part + + def kpasswd_exchange(self, + ticket, + new_password, + expected_code, + expected_msg, + mode, + target_princ=None, + target_realm=None, + ap_options=None, + send_seq_number=True): + if mode is self.KpasswdMode.SET: + version = 0xff80 + user_data = self.ChangePasswdDataMS_create(new_password, + target_princ, + target_realm) + elif mode is self.KpasswdMode.CHANGE: + self.assertIsNone(target_princ, + 'target_princ only valid for pw set') + self.assertIsNone(target_realm, + 'target_realm only valid for pw set') + + version = 1 + user_data = new_password.encode('utf-8') + else: + self.fail(f'invalid mode {mode}') + + subkey = self.RandomKey(kcrypto.Enctype.AES256) + + if ap_options is None: + ap_options = '0' + ap_options = str(krb5_asn1.APOptions(ap_options)) + + kdc_exchange_dict = { + 'tgt': ticket, + 'authenticator_subkey': subkey, + 'auth_data': None, + 'ap_options': ap_options, + } + + if send_seq_number: + seq_number = random.randint(0, 0xfffffffe) + else: + seq_number = None + + ap_req = self.generate_ap_req(kdc_exchange_dict, + None, + req_body=None, + armor=False, + usage=KU_AP_REQ_AUTH, + seq_number=seq_number) + + self.connect(self.host, port=464) + self.assertIsNotNone(self.s) + + family = self.s.family + + if family == socket.AF_INET: + addr_type = 2 # IPv4 + elif family == socket.AF_INET6: + addr_type = 24 # IPv6 + else: + self.fail(f'unknown family {family}') + + def create_address(ip): + return { + 'addr-type': addr_type, + 'address': socket.inet_pton(family, ip), + } + + local_ip = self.s.getsockname()[0] + local_address = create_address(local_ip) + + # remote_ip = self.s.getpeername()[0] + # remote_address = create_address(remote_ip) + + # TODO: due to a bug (?), MIT Kerberos will not accept the request + # unless r-address is set to our _local_ address. Heimdal, on the other + # hand, requires the r-address is set to the remote address (as + # expected). To avoid problems, avoid sending r-address for now. + remote_address = None + + msg = self.kpasswd_create(subkey, + user_data, + version, + seq_number, + ap_req, + local_address, + remote_address) + + self.send_msg(msg) + rep_pdu = self.recv_pdu_raw() + + self._disconnect('transaction done') + + self.assertIsNotNone(rep_pdu) + + header = rep_pdu[:6] + reply = rep_pdu[6:] + + reply_len = (header[0] << 8) | header[1] + reply_version = (header[2] << 8) | header[3] + ap_rep_len = (header[4] << 8) | header[5] + + self.assertEqual(reply_len, len(rep_pdu)) + self.assertEqual(1, reply_version) # KRB5_KPASSWD_VERS_CHANGEPW + self.assertLess(ap_rep_len, reply_len) + + self.assertNotEqual(0x7e, rep_pdu[1]) + self.assertNotEqual(0x5e, rep_pdu[1]) + + if ap_rep_len: + # We received an AP-REQ and KRB-PRIV as a response. This may or may + # not indicate an error, depending on the status code. + ap_rep = reply[:ap_rep_len] + krb_priv = reply[ap_rep_len:] + + key = ticket.session_key + + ap_rep = self.der_decode(ap_rep, asn1Spec=krb5_asn1.AP_REP()) + self.assertElementEqual(ap_rep, 'msg-type', KRB_AP_REP) + enc_part = self.get_enc_part(ap_rep, key, KU_AP_REQ_ENC_PART) + enc_part = self.der_decode( + enc_part, asn1Spec=krb5_asn1.EncAPRepPart()) + + self.assertElementPresent(enc_part, 'ctime') + self.assertElementPresent(enc_part, 'cusec') + # self.assertElementMissing(enc_part, 'subkey') # TODO + # self.assertElementPresent(enc_part, 'seq-number') # TODO + + try: + krb_priv = self.der_decode(krb_priv, asn1Spec=krb5_asn1.KRB_PRIV()) + except PyAsn1Error: + self.fail() + + self.assertElementEqual(krb_priv, 'msg-type', KRB_PRIV) + priv_enc_part = self.get_enc_part(krb_priv, subkey, KU_KRB_PRIV) + priv_enc_part = self.der_decode( + priv_enc_part, asn1Spec=krb5_asn1.EncKrbPrivPart()) + + self.assertElementMissing(priv_enc_part, 'timestamp') + self.assertElementMissing(priv_enc_part, 'usec') + # self.assertElementPresent(priv_enc_part, 'seq-number') # TODO + # self.assertElementEqual(priv_enc_part, 's-address', remote_address) # TODO + # self.assertElementMissing(priv_enc_part, 'r-address') # TODO + + result_data = priv_enc_part['user-data'] + else: + # We received a KRB-ERROR as a response, indicating an error. + krb_error = self.der_decode(reply, asn1Spec=krb5_asn1.KRB_ERROR()) + + sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, + names=['kadmin', 'changepw']) + realm = self.get_krbtgt_creds().get_realm().upper() + + self.assertElementEqual(krb_error, 'pvno', 5) + self.assertElementEqual(krb_error, 'msg-type', KRB_ERROR) + self.assertElementMissing(krb_error, 'ctime') + self.assertElementMissing(krb_error, 'usec') + self.assertElementPresent(krb_error, 'stime') + self.assertElementPresent(krb_error, 'susec') + + error_code = krb_error['error-code'] + if isinstance(expected_code, int): + self.assertEqual(error_code, expected_code) + else: + self.assertIn(error_code, expected_code) + + self.assertElementMissing(krb_error, 'crealm') + self.assertElementMissing(krb_error, 'cname') + self.assertElementEqual(krb_error, 'realm', realm.encode('utf-8')) + self.assertElementEqualPrincipal(krb_error, 'sname', sname) + self.assertElementMissing(krb_error, 'e-text') + + result_data = krb_error['e-data'] + + status = result_data[:2] + message = result_data[2:] + + status_code = (status[0] << 8) | status[1] + if isinstance(expected_code, int): + self.assertEqual(status_code, expected_code) + else: + self.assertIn(status_code, expected_code) + + if not message: + self.assertEqual(0, status_code, + 'got an error result, but no message') + return + + # Check the first character of the message. + if message[0]: + if isinstance(expected_msg, bytes): + self.assertEqual(message, expected_msg) + else: + self.assertIn(message, expected_msg) + else: + # We got AD password policy information. + self.assertEqual(30, len(message)) + + (empty_bytes, + min_length, + history_length, + properties, + expire_time, + min_age) = struct.unpack('>HIIIQQ', message) + + def _generic_kdc_exchange(self, + kdc_exchange_dict, # required + cname=None, # optional + realm=None, # required + sname=None, # optional + from_time=None, # optional + till_time=None, # required + renew_time=None, # optional + etypes=None, # required + addresses=None, # optional + additional_tickets=None, # optional + EncAuthorizationData=None, # optional + EncAuthorizationData_key=None, # optional + EncAuthorizationData_usage=None): # optional + + check_error_fn = kdc_exchange_dict['check_error_fn'] + check_rep_fn = kdc_exchange_dict['check_rep_fn'] + generate_fast_fn = kdc_exchange_dict['generate_fast_fn'] + generate_fast_armor_fn = kdc_exchange_dict['generate_fast_armor_fn'] + generate_fast_padata_fn = kdc_exchange_dict['generate_fast_padata_fn'] + generate_padata_fn = kdc_exchange_dict['generate_padata_fn'] + callback_dict = kdc_exchange_dict['callback_dict'] + req_msg_type = kdc_exchange_dict['req_msg_type'] + req_asn1Spec = kdc_exchange_dict['req_asn1Spec'] + rep_msg_type = kdc_exchange_dict['rep_msg_type'] + + expected_error_mode = kdc_exchange_dict['expected_error_mode'] + kdc_options = kdc_exchange_dict['kdc_options'] + + pac_request = kdc_exchange_dict['pac_request'] + pac_options = kdc_exchange_dict['pac_options'] + + # Parameters specific to the inner request body + inner_req = kdc_exchange_dict['inner_req'] + + # Parameters specific to the outer request body + outer_req = kdc_exchange_dict['outer_req'] + + if till_time is None: + till_time = self.get_KerberosTime(offset=36000) + + if 'nonce' in kdc_exchange_dict: + nonce = kdc_exchange_dict['nonce'] + else: + nonce = self.get_Nonce() + kdc_exchange_dict['nonce'] = nonce + + req_body = self.KDC_REQ_BODY_create( + kdc_options=kdc_options, + cname=cname, + realm=realm, + sname=sname, + from_time=from_time, + till_time=till_time, + renew_time=renew_time, + nonce=nonce, + etypes=etypes, + addresses=addresses, + additional_tickets=additional_tickets, + EncAuthorizationData=EncAuthorizationData, + EncAuthorizationData_key=EncAuthorizationData_key, + EncAuthorizationData_usage=EncAuthorizationData_usage) + + inner_req_body = dict(req_body) + if inner_req is not None: + for key, value in inner_req.items(): + if value is not None: + inner_req_body[key] = value + else: + del inner_req_body[key] + if outer_req is not None: + for key, value in outer_req.items(): + if value is not None: + req_body[key] = value + else: + del req_body[key] + + additional_padata = [] + if pac_request is not None: + pa_pac_request = self.KERB_PA_PAC_REQUEST_create(pac_request) + additional_padata.append(pa_pac_request) + if pac_options is not None: + pa_pac_options = self.get_pa_pac_options(pac_options) + additional_padata.append(pa_pac_options) + + if req_msg_type == KRB_AS_REQ: + tgs_req = None + tgs_req_padata = None + else: + self.assertEqual(KRB_TGS_REQ, req_msg_type) + + tgs_req = self.generate_ap_req(kdc_exchange_dict, + callback_dict, + req_body, + armor=False) + tgs_req_padata = self.PA_DATA_create(PADATA_KDC_REQ, tgs_req) + + if generate_fast_padata_fn is not None: + self.assertIsNotNone(generate_fast_fn) + # This can alter req_body... + fast_padata, req_body = generate_fast_padata_fn(kdc_exchange_dict, + callback_dict, + req_body) + else: + fast_padata = [] + + if generate_fast_armor_fn is not None: + self.assertIsNotNone(generate_fast_fn) + fast_ap_req = generate_fast_armor_fn(kdc_exchange_dict, + callback_dict, + None, + armor=True) + + fast_armor_type = kdc_exchange_dict['fast_armor_type'] + fast_armor = self.KRB_FAST_ARMOR_create(fast_armor_type, + fast_ap_req) + else: + fast_armor = None + + if generate_padata_fn is not None: + # This can alter req_body... + outer_padata, req_body = generate_padata_fn(kdc_exchange_dict, + callback_dict, + req_body) + self.assertIsNotNone(outer_padata) + self.assertNotIn(PADATA_KDC_REQ, + [pa['padata-type'] for pa in outer_padata], + 'Don\'t create TGS-REQ manually') + else: + outer_padata = None + + if generate_fast_fn is not None: + armor_key = kdc_exchange_dict['armor_key'] + self.assertIsNotNone(armor_key) + + if req_msg_type == KRB_AS_REQ: + checksum_blob = self.der_encode( + req_body, + asn1Spec=krb5_asn1.KDC_REQ_BODY()) + else: + self.assertEqual(KRB_TGS_REQ, req_msg_type) + checksum_blob = tgs_req + + checksum = self.Checksum_create(armor_key, + KU_FAST_REQ_CHKSUM, + checksum_blob) + + fast_padata += additional_padata + fast = generate_fast_fn(kdc_exchange_dict, + callback_dict, + inner_req_body, + fast_padata, + fast_armor, + checksum) + else: + fast = None + + padata = [] + + if tgs_req_padata is not None: + padata.append(tgs_req_padata) + + if fast is not None: + padata.append(fast) + + if outer_padata is not None: + padata += outer_padata + + if fast is None: + padata += additional_padata + + if not padata: + padata = None + + kdc_exchange_dict['req_padata'] = padata + kdc_exchange_dict['fast_padata'] = fast_padata + kdc_exchange_dict['req_body'] = inner_req_body + + req_obj, req_decoded = self.KDC_REQ_create(msg_type=req_msg_type, + padata=padata, + req_body=req_body, + asn1Spec=req_asn1Spec()) + + kdc_exchange_dict['req_obj'] = req_obj + + to_rodc = kdc_exchange_dict['to_rodc'] + + rep = self.send_recv_transaction(req_decoded, to_rodc=to_rodc) + self.assertIsNotNone(rep) + + msg_type = self.getElementValue(rep, 'msg-type') + self.assertIsNotNone(msg_type) + + expected_msg_type = None + if check_error_fn is not None: + expected_msg_type = KRB_ERROR + self.assertIsNone(check_rep_fn) + self.assertNotEqual(0, len(expected_error_mode)) + self.assertNotIn(0, expected_error_mode) + if check_rep_fn is not None: + expected_msg_type = rep_msg_type + self.assertIsNone(check_error_fn) + self.assertEqual(0, len(expected_error_mode)) + self.assertIsNotNone(expected_msg_type) + if msg_type == KRB_ERROR: + error_code = self.getElementValue(rep, 'error-code') + fail_msg = f'Got unexpected error: {error_code}' + else: + fail_msg = f'Expected to fail with error: {expected_error_mode}' + self.assertEqual(msg_type, expected_msg_type, fail_msg) + + if msg_type == KRB_ERROR: + return check_error_fn(kdc_exchange_dict, + callback_dict, + rep) + + return check_rep_fn(kdc_exchange_dict, callback_dict, rep) + + def as_exchange_dict(self, + creds=None, + client_cert=None, + expected_crealm=None, + expected_cname=None, + expected_anon=False, + expected_srealm=None, + expected_sname=None, + expected_account_name=None, + expected_groups=None, + unexpected_groups=None, + expected_upn_name=None, + expected_sid=None, + expected_requester_sid=None, + expected_domain_sid=None, + expected_device_domain_sid=None, + expected_supported_etypes=None, + expected_flags=None, + unexpected_flags=None, + ticket_decryption_key=None, + expect_ticket_checksum=None, + expect_full_checksum=None, + generate_fast_fn=None, + generate_fast_armor_fn=None, + generate_fast_padata_fn=None, + fast_armor_type=FX_FAST_ARMOR_AP_REQUEST, + generate_padata_fn=None, + check_error_fn=None, + check_rep_fn=None, + check_kdc_private_fn=None, + check_patypes=True, + callback_dict=None, + expected_error_mode=0, + expect_status=None, + expected_status=None, + expected_salt=None, + authenticator_subkey=None, + preauth_key=None, + armor_key=None, + armor_tgt=None, + armor_subkey=None, + auth_data=None, + kdc_options='', + inner_req=None, + outer_req=None, + pac_request=None, + pac_options=None, + ap_options=None, + fast_ap_options=None, + strict_edata_checking=True, + using_pkinit=PkInit.NOT_USED, + pk_nonce=None, + expect_edata=None, + expect_pac=True, + expect_client_claims=None, + expect_device_info=None, + expect_device_claims=None, + expect_upn_dns_info_ex=None, + expect_pac_attrs=None, + expect_pac_attrs_pac_request=None, + expect_requester_sid=None, + rc4_support=True, + expected_client_claims=None, + unexpected_client_claims=None, + expected_device_claims=None, + unexpected_device_claims=None, + expect_resource_groups_flag=None, + expected_device_groups=None, + expected_extra_pac_buffers=None, + to_rodc=False): + if expected_error_mode == 0: + expected_error_mode = () + elif not isinstance(expected_error_mode, collections.abc.Container): + expected_error_mode = (expected_error_mode,) + + kdc_exchange_dict = { + 'req_msg_type': KRB_AS_REQ, + 'req_asn1Spec': krb5_asn1.AS_REQ, + 'rep_msg_type': KRB_AS_REP, + 'rep_asn1Spec': krb5_asn1.AS_REP, + 'rep_encpart_asn1Spec': krb5_asn1.EncASRepPart, + 'creds': creds, + 'client_cert': client_cert, + 'expected_crealm': expected_crealm, + 'expected_cname': expected_cname, + 'expected_anon': expected_anon, + 'expected_srealm': expected_srealm, + 'expected_sname': expected_sname, + 'expected_account_name': expected_account_name, + 'expected_groups': expected_groups, + 'unexpected_groups': unexpected_groups, + 'expected_upn_name': expected_upn_name, + 'expected_sid': expected_sid, + 'expected_requester_sid': expected_requester_sid, + 'expected_domain_sid': expected_domain_sid, + 'expected_device_domain_sid': expected_device_domain_sid, + 'expected_supported_etypes': expected_supported_etypes, + 'expected_flags': expected_flags, + 'unexpected_flags': unexpected_flags, + 'ticket_decryption_key': ticket_decryption_key, + 'expect_ticket_checksum': expect_ticket_checksum, + 'expect_full_checksum': expect_full_checksum, + 'generate_fast_fn': generate_fast_fn, + 'generate_fast_armor_fn': generate_fast_armor_fn, + 'generate_fast_padata_fn': generate_fast_padata_fn, + 'fast_armor_type': fast_armor_type, + 'generate_padata_fn': generate_padata_fn, + 'check_error_fn': check_error_fn, + 'check_rep_fn': check_rep_fn, + 'check_kdc_private_fn': check_kdc_private_fn, + 'check_patypes': check_patypes, + 'callback_dict': callback_dict, + 'expected_error_mode': expected_error_mode, + 'expect_status': expect_status, + 'expected_status': expected_status, + 'expected_salt': expected_salt, + 'authenticator_subkey': authenticator_subkey, + 'preauth_key': preauth_key, + 'armor_key': armor_key, + 'armor_tgt': armor_tgt, + 'armor_subkey': armor_subkey, + 'auth_data': auth_data, + 'kdc_options': kdc_options, + 'inner_req': inner_req, + 'outer_req': outer_req, + 'pac_request': pac_request, + 'pac_options': pac_options, + 'ap_options': ap_options, + 'fast_ap_options': fast_ap_options, + 'strict_edata_checking': strict_edata_checking, + 'using_pkinit': using_pkinit, + 'pk_nonce': pk_nonce, + 'expect_edata': expect_edata, + 'expect_pac': expect_pac, + 'expect_client_claims': expect_client_claims, + 'expect_device_info': expect_device_info, + 'expect_device_claims': expect_device_claims, + 'expect_upn_dns_info_ex': expect_upn_dns_info_ex, + 'expect_pac_attrs': expect_pac_attrs, + 'expect_pac_attrs_pac_request': expect_pac_attrs_pac_request, + 'expect_requester_sid': expect_requester_sid, + 'rc4_support': rc4_support, + 'expected_client_claims': expected_client_claims, + 'unexpected_client_claims': unexpected_client_claims, + 'expected_device_claims': expected_device_claims, + 'unexpected_device_claims': unexpected_device_claims, + 'expect_resource_groups_flag': expect_resource_groups_flag, + 'expected_device_groups': expected_device_groups, + 'expected_extra_pac_buffers': expected_extra_pac_buffers, + 'to_rodc': to_rodc + } + if callback_dict is None: + callback_dict = {} + + return kdc_exchange_dict + + def tgs_exchange_dict(self, + creds=None, + expected_crealm=None, + expected_cname=None, + expected_anon=False, + expected_srealm=None, + expected_sname=None, + expected_account_name=None, + expected_groups=None, + unexpected_groups=None, + expected_upn_name=None, + expected_sid=None, + expected_requester_sid=None, + expected_domain_sid=None, + expected_device_domain_sid=None, + expected_supported_etypes=None, + expected_flags=None, + unexpected_flags=None, + ticket_decryption_key=None, + expect_ticket_checksum=None, + expect_full_checksum=None, + generate_fast_fn=None, + generate_fast_armor_fn=None, + generate_fast_padata_fn=None, + fast_armor_type=FX_FAST_ARMOR_AP_REQUEST, + generate_padata_fn=None, + check_error_fn=None, + check_rep_fn=None, + check_kdc_private_fn=None, + check_patypes=True, + expected_error_mode=0, + expect_status=None, + expected_status=None, + callback_dict=None, + tgt=None, + armor_key=None, + armor_tgt=None, + armor_subkey=None, + authenticator_subkey=None, + auth_data=None, + body_checksum_type=None, + kdc_options='', + inner_req=None, + outer_req=None, + pac_request=None, + pac_options=None, + ap_options=None, + fast_ap_options=None, + strict_edata_checking=True, + expect_edata=None, + expect_pac=True, + expect_client_claims=None, + expect_device_info=None, + expect_device_claims=None, + expect_upn_dns_info_ex=None, + expect_pac_attrs=None, + expect_pac_attrs_pac_request=None, + expect_requester_sid=None, + expected_proxy_target=None, + expected_transited_services=None, + rc4_support=True, + expected_client_claims=None, + unexpected_client_claims=None, + expected_device_claims=None, + unexpected_device_claims=None, + expect_resource_groups_flag=None, + expected_device_groups=None, + expected_extra_pac_buffers=None, + to_rodc=False): + if expected_error_mode == 0: + expected_error_mode = () + elif not isinstance(expected_error_mode, collections.abc.Container): + expected_error_mode = (expected_error_mode,) + + kdc_exchange_dict = { + 'req_msg_type': KRB_TGS_REQ, + 'req_asn1Spec': krb5_asn1.TGS_REQ, + 'rep_msg_type': KRB_TGS_REP, + 'rep_asn1Spec': krb5_asn1.TGS_REP, + 'rep_encpart_asn1Spec': krb5_asn1.EncTGSRepPart, + 'creds': creds, + 'expected_crealm': expected_crealm, + 'expected_cname': expected_cname, + 'expected_anon': expected_anon, + 'expected_srealm': expected_srealm, + 'expected_sname': expected_sname, + 'expected_account_name': expected_account_name, + 'expected_groups': expected_groups, + 'unexpected_groups': unexpected_groups, + 'expected_upn_name': expected_upn_name, + 'expected_sid': expected_sid, + 'expected_requester_sid': expected_requester_sid, + 'expected_domain_sid': expected_domain_sid, + 'expected_device_domain_sid': expected_device_domain_sid, + 'expected_supported_etypes': expected_supported_etypes, + 'expected_flags': expected_flags, + 'unexpected_flags': unexpected_flags, + 'ticket_decryption_key': ticket_decryption_key, + 'expect_ticket_checksum': expect_ticket_checksum, + 'expect_full_checksum': expect_full_checksum, + 'generate_fast_fn': generate_fast_fn, + 'generate_fast_armor_fn': generate_fast_armor_fn, + 'generate_fast_padata_fn': generate_fast_padata_fn, + 'fast_armor_type': fast_armor_type, + 'generate_padata_fn': generate_padata_fn, + 'check_error_fn': check_error_fn, + 'check_rep_fn': check_rep_fn, + 'check_kdc_private_fn': check_kdc_private_fn, + 'check_patypes': check_patypes, + 'callback_dict': callback_dict, + 'expected_error_mode': expected_error_mode, + 'expect_status': expect_status, + 'expected_status': expected_status, + 'tgt': tgt, + 'body_checksum_type': body_checksum_type, + 'armor_key': armor_key, + 'armor_tgt': armor_tgt, + 'armor_subkey': armor_subkey, + 'auth_data': auth_data, + 'authenticator_subkey': authenticator_subkey, + 'kdc_options': kdc_options, + 'inner_req': inner_req, + 'outer_req': outer_req, + 'pac_request': pac_request, + 'pac_options': pac_options, + 'ap_options': ap_options, + 'fast_ap_options': fast_ap_options, + 'strict_edata_checking': strict_edata_checking, + 'expect_edata': expect_edata, + 'expect_pac': expect_pac, + 'expect_client_claims': expect_client_claims, + 'expect_device_info': expect_device_info, + 'expect_device_claims': expect_device_claims, + 'expect_upn_dns_info_ex': expect_upn_dns_info_ex, + 'expect_pac_attrs': expect_pac_attrs, + 'expect_pac_attrs_pac_request': expect_pac_attrs_pac_request, + 'expect_requester_sid': expect_requester_sid, + 'expected_proxy_target': expected_proxy_target, + 'expected_transited_services': expected_transited_services, + 'rc4_support': rc4_support, + 'expected_client_claims': expected_client_claims, + 'unexpected_client_claims': unexpected_client_claims, + 'expected_device_claims': expected_device_claims, + 'unexpected_device_claims': unexpected_device_claims, + 'expect_resource_groups_flag': expect_resource_groups_flag, + 'expected_device_groups': expected_device_groups, + 'expected_extra_pac_buffers': expected_extra_pac_buffers, + 'to_rodc': to_rodc + } + if callback_dict is None: + callback_dict = {} + + return kdc_exchange_dict + + def generic_check_kdc_rep(self, + kdc_exchange_dict, + callback_dict, + rep): + + expected_crealm = kdc_exchange_dict['expected_crealm'] + expected_anon = kdc_exchange_dict['expected_anon'] + expected_srealm = kdc_exchange_dict['expected_srealm'] + expected_sname = kdc_exchange_dict['expected_sname'] + ticket_decryption_key = kdc_exchange_dict['ticket_decryption_key'] + check_kdc_private_fn = kdc_exchange_dict['check_kdc_private_fn'] + rep_encpart_asn1Spec = kdc_exchange_dict['rep_encpart_asn1Spec'] + msg_type = kdc_exchange_dict['rep_msg_type'] + armor_key = kdc_exchange_dict['armor_key'] + + self.assertElementEqual(rep, 'msg-type', msg_type) # AS-REP | TGS-REP + padata = self.getElementValue(rep, 'padata') + if self.strict_checking: + self.assertElementEqualUTF8(rep, 'crealm', expected_crealm) + if self.cname_checking: + if expected_anon: + expected_cname = self.PrincipalName_create( + name_type=NT_WELLKNOWN, + names=['WELLKNOWN', 'ANONYMOUS']) + else: + expected_cname = kdc_exchange_dict['expected_cname'] + self.assertElementEqualPrincipal(rep, 'cname', expected_cname) + self.assertElementPresent(rep, 'ticket') + ticket = self.getElementValue(rep, 'ticket') + ticket_encpart = None + ticket_cipher = None + self.assertIsNotNone(ticket) + if ticket is not None: # Never None, but gives indentation + self.assertElementEqual(ticket, 'tkt-vno', 5) + self.assertElementEqualUTF8(ticket, 'realm', expected_srealm) + self.assertElementEqualPrincipal(ticket, 'sname', expected_sname) + self.assertElementPresent(ticket, 'enc-part') + ticket_encpart = self.getElementValue(ticket, 'enc-part') + self.assertIsNotNone(ticket_encpart) + if ticket_encpart is not None: # Never None, but gives indentation + self.assertElementPresent(ticket_encpart, 'etype') + + kdc_options = kdc_exchange_dict['kdc_options'] + pos = len(tuple(krb5_asn1.KDCOptions('enc-tkt-in-skey'))) - 1 + expect_kvno = (pos >= len(kdc_options) + or kdc_options[pos] != '1') + if expect_kvno: + # 'unspecified' means present, with any value != 0 + self.assertElementKVNO(ticket_encpart, 'kvno', + self.unspecified_kvno) + else: + # For user-to-user, don't expect a kvno. + self.assertElementMissing(ticket_encpart, 'kvno') + + self.assertElementPresent(ticket_encpart, 'cipher') + ticket_cipher = self.getElementValue(ticket_encpart, 'cipher') + self.assertElementPresent(rep, 'enc-part') + encpart = self.getElementValue(rep, 'enc-part') + encpart_cipher = None + self.assertIsNotNone(encpart) + if encpart is not None: # Never None, but gives indentation + self.assertElementPresent(encpart, 'etype') + self.assertElementKVNO(ticket_encpart, 'kvno', 'autodetect') + self.assertElementPresent(encpart, 'cipher') + encpart_cipher = self.getElementValue(encpart, 'cipher') + + if self.padata_checking: + self.check_reply_padata(kdc_exchange_dict, + callback_dict, + encpart, + padata) + + ticket_checksum = None + + # Get the decryption key for the encrypted part + encpart_decryption_key, encpart_decryption_usage = ( + self.get_preauth_key(kdc_exchange_dict)) + + pa_dict = self.get_pa_dict(padata) + + pk_as_rep = pa_dict.get(PADATA_PK_AS_REP) + if pk_as_rep is not None: + pk_as_rep_asn1_spec = krb5_asn1.PA_PK_AS_REP + reply_key_pack_asn1_spec = krb5_asn1.ReplyKeyPack + pk_win2k = False + else: + pk_as_rep = pa_dict.get(PADATA_PK_AS_REP_19) + pk_as_rep_asn1_spec = krb5_asn1.PA_PK_AS_REP_Win2k + reply_key_pack_asn1_spec = krb5_asn1.ReplyKeyPack_Win2k + pk_win2k = True + if pk_as_rep is not None: + pk_as_rep = self.der_decode(pk_as_rep, + asn1Spec=pk_as_rep_asn1_spec()) + + using_pkinit = kdc_exchange_dict['using_pkinit'] + if using_pkinit is PkInit.PUBLIC_KEY: + content_info = self.der_decode( + pk_as_rep['encKeyPack'], + asn1Spec=krb5_asn1.ContentInfo()) + self.assertEqual(str(krb5_asn1.id_envelopedData), + content_info['contentType']) + + content = self.der_decode(content_info['content'], + asn1Spec=krb5_asn1.EnvelopedData()) + + self.assertEqual(0, content['version']) + originator_info = content['originatorInfo'] + self.assertFalse(originator_info.get('certs')) + self.assertFalse(originator_info.get('crls')) + self.assertFalse(content.get('unprotectedAttrs')) + + encrypted_content_info = content['encryptedContentInfo'] + recipient_infos = content['recipientInfos'] + + self.assertEqual(1, len(recipient_infos)) + ktri = recipient_infos[0]['ktri'] + + if self.strict_checking: + self.assertEqual(0, ktri['version']) + + private_key = encpart_decryption_key + self.assertIsInstance(private_key, + asymmetric.rsa.RSAPrivateKey) + + client_subject_key_id = ( + x509.SubjectKeyIdentifier.from_public_key( + private_key.public_key())) + + # Check that the client certificate is named as the recipient. + ktri_rid = ktri['rid'] + try: + issuer_and_serial_number = ktri_rid[ + 'issuerAndSerialNumber'] + except KeyError: + subject_key_id = ktri_rid['subjectKeyIdentifier'] + self.assertEqual(subject_key_id, + client_subject_key_id.digest) + else: + client_certificate = kdc_exchange_dict['client_cert'] + + self.assertIsNotNone(issuer_and_serial_number['issuer']) + self.assertEqual(issuer_and_serial_number['serialNumber'], + client_certificate.serial_number) + + key_encryption_algorithm = ktri['keyEncryptionAlgorithm'] + self.assertEqual(str(krb5_asn1.rsaEncryption), + key_encryption_algorithm['algorithm']) + if self.strict_checking: + self.assertEqual( + b'\x05\x00', + key_encryption_algorithm.get('parameters')) + + encrypted_key = ktri['encryptedKey'] + + # Decrypt the key. + pad_len = 256 - len(encrypted_key) + if pad_len: + encrypted_key = bytes(pad_len) + encrypted_key + decrypted_key = private_key.decrypt( + encrypted_key, + padding=asymmetric.padding.PKCS1v15()) + + self.assertEqual(str(krb5_asn1.id_signedData), + encrypted_content_info['contentType']) + + encrypted_content = encrypted_content_info['encryptedContent'] + encryption_algorithm = encrypted_content_info[ + 'contentEncryptionAlgorithm'] + + cipher_algorithm = self.cipher_from_algorithm(encryption_algorithm['algorithm']) + + # This will serve as the IV. + parameters = self.der_decode( + encryption_algorithm['parameters'], + asn1Spec=krb5_asn1.CMSCBCParameter()) + + # Decrypt the content. + cipher = Cipher(cipher_algorithm(decrypted_key), + modes.CBC(parameters), + default_backend()) + decryptor = cipher.decryptor() + decrypted_content = decryptor.update(encrypted_content) + decrypted_content += decryptor.finalize() + + # The padding doesn’t fully comply to PKCS7 with a specified + # blocksize, so we must unpad the data ourselves. + decrypted_content = self.unpad(decrypted_content) + + signed_data = None + signed_data_rfc2315 = None + + first_tag = decrypted_content[0] + if first_tag == 0x30: # ASN.1 SEQUENCE tag + signed_data = decrypted_content + else: + # Windows encodes the ASN.1 incorrectly, neglecting to add + # the SEQUENCE tag. We’ll have to prepend it ourselves in + # order for the decoding to work. + encoded_len = self.asn1_length(decrypted_content) + decrypted_content = bytes([0x30]) + encoded_len + ( + decrypted_content) + + if first_tag == 0x02: # ASN.1 INTEGER tag + + # The INTEGER tag indicates that the data is encoded + # with the earlier variant of the SignedData ASN.1 + # schema specified in RFC2315, as per [MS-PKCA] 2.2.4 + # (PA-PK-AS-REP). + signed_data_rfc2315 = decrypted_content + + elif first_tag == 0x06: # ASN.1 OBJECT IDENTIFIER tag + + # The OBJECT IDENTIFIER tag indicates that the data is + # encoded as SignedData and wrapped in a ContentInfo + # structure, which we shall have to decode first. This + # seems to be the case when the supportedCMSTypes field + # in the client’s AuthPack is missing or empty. + + content_info = self.der_decode( + decrypted_content, + asn1Spec=krb5_asn1.ContentInfo()) + self.assertEqual(str(krb5_asn1.id_signedData), + content_info['contentType']) + signed_data = content_info['content'] + else: + self.fail(f'got reply with unknown initial tag ' + f'({first_tag})') + + if signed_data is not None: + signed_data = self.der_decode( + signed_data, asn1Spec=krb5_asn1.SignedData()) + + encap_content_info = signed_data['encapContentInfo'] + + content_type = encap_content_info['eContentType'] + content = encap_content_info['eContent'] + elif signed_data_rfc2315 is not None: + signed_data = self.der_decode( + signed_data_rfc2315, + asn1Spec=krb5_asn1.SignedData_RFC2315()) + + encap_content_info = signed_data['contentInfo'] + + content_type = encap_content_info['contentType'] + content = self.der_decode( + encap_content_info['content'], + asn1Spec=pyasn1.type.univ.OctetString()) + else: + self.fail('we must have got SignedData') + + self.assertEqual(str(krb5_asn1.id_pkinit_rkeyData), + content_type) + reply_key_pack = self.der_decode( + content, asn1Spec=reply_key_pack_asn1_spec()) + + req_obj = kdc_exchange_dict['req_obj'] + req_asn1Spec = kdc_exchange_dict['req_asn1Spec'] + req_obj = self.der_encode(req_obj, + asn1Spec=req_asn1Spec()) + + reply_key = reply_key_pack['replyKey'] + + # Reply the encpart decryption key with the decrypted key from + # the reply. + encpart_decryption_key = self.SessionKey_create( + etype=reply_key['keytype'], + contents=reply_key['keyvalue'], + kvno=None) + + if not pk_win2k: + as_checksum = reply_key_pack['asChecksum'] + + # Verify the checksum over the AS request body. + kcrypto.verify_checksum(as_checksum['cksumtype'], + encpart_decryption_key.key, + KU_PKINIT_AS_REQ, + req_obj, + as_checksum['checksum']) + elif using_pkinit is PkInit.DIFFIE_HELLMAN: + content_info = self.der_decode( + pk_as_rep['dhInfo']['dhSignedData'], + asn1Spec=krb5_asn1.ContentInfo()) + self.assertEqual(str(krb5_asn1.id_signedData), + content_info['contentType']) + + signed_data = self.der_decode(content_info['content'], + asn1Spec=krb5_asn1.SignedData()) + + encap_content_info = signed_data['encapContentInfo'] + content = encap_content_info['eContent'] + + self.assertEqual(str(krb5_asn1.id_pkinit_DHKeyData), + encap_content_info['eContentType']) + + dh_key_info = self.der_decode( + content, asn1Spec=krb5_asn1.KDCDHKeyInfo()) + + self.assertNotIn('dhKeyExpiration', dh_key_info) + + dh_private_key = encpart_decryption_key + self.assertIsInstance(dh_private_key, + asymmetric.dh.DHPrivateKey) + + self.assertElementEqual(dh_key_info, 'nonce', + kdc_exchange_dict['pk_nonce']) + + dh_public_key_data = self.bytes_from_bit_string( + dh_key_info['subjectPublicKey']) + dh_public_key_decoded = self.der_decode( + dh_public_key_data, asn1Spec=krb5_asn1.DHPublicKey()) + + dh_numbers = dh_private_key.parameters().parameter_numbers() + + public_numbers = asymmetric.dh.DHPublicNumbers( + dh_public_key_decoded, dh_numbers) + dh_public_key = public_numbers.public_key(default_backend()) + + # Perform the Diffie-Hellman key exchange. + shared_secret = dh_private_key.exchange(dh_public_key) + + # Pad the shared secret out to the length of ‘p’. + p_len = self.length_in_bytes(dh_numbers.p) + padding_len = p_len - len(shared_secret) + self.assertGreaterEqual(padding_len, 0) + padded_shared_secret = bytes(padding_len) + shared_secret + + reply_key_enc_type = self.expected_etype(kdc_exchange_dict) + + # At the moment, we don’t specify a nonce in the request, so we + # can assume these are empty. + client_nonce = b'' + server_nonce = b'' + + ciphertext = padded_shared_secret + client_nonce + server_nonce + + # Replace the encpart decryption key with the key derived from + # the Diffie-Hellman key exchange. + encpart_decryption_key = self.octetstring2key( + ciphertext, reply_key_enc_type) + else: + self.fail(f'invalid value for using_pkinit: {using_pkinit}') + + self.assertEqual(3, signed_data['version']) + + digest_algorithms = signed_data['digestAlgorithms'] + self.assertEqual(1, len(digest_algorithms)) + digest_algorithm = digest_algorithms[0] + # Ensure the hash algorithm is valid. + _ = self.hash_from_algorithm_id(digest_algorithm) + + self.assertFalse(signed_data.get('crls')) + + signer_infos = signed_data['signerInfos'] + self.assertEqual(1, len(signer_infos)) + signer_info = signer_infos[0] + + self.assertEqual(1, signer_info['version']) + + # Get the certificate presented by the KDC. + kdc_certificates = signed_data['certificates'] + self.assertEqual(1, len(kdc_certificates)) + kdc_certificate = self.der_encode( + kdc_certificates[0], asn1Spec=krb5_asn1.CertificateChoices()) + kdc_certificate = x509.load_der_x509_certificate(kdc_certificate, + default_backend()) + + # Verify that the KDC’s certificate is named as the signer. + sid = signer_info['sid'] + try: + issuer_and_serial_number = sid['issuerAndSerialNumber'] + except KeyError: + extension = kdc_certificate.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.SUBJECT_KEY_IDENTIFIER) + cert_subject_key_id = extension.value.digest + self.assertEqual(sid['subjectKeyIdentifier'], cert_subject_key_id) + else: + self.assertIsNotNone(issuer_and_serial_number['issuer']) + self.assertEqual(issuer_and_serial_number['serialNumber'], + kdc_certificate.serial_number) + + digest_algorithm = signer_info['digestAlgorithm'] + digest_hash_fn = self.hash_from_algorithm_id(digest_algorithm) + + signed_attrs = signer_info['signedAttrs'] + self.assertEqual(2, len(signed_attrs)) + + signed_attr0 = signed_attrs[0] + self.assertEqual(str(krb5_asn1.id_contentType), + signed_attr0['type']) + signed_attr0_values = signed_attr0['values'] + self.assertEqual(1, len(signed_attr0_values)) + signed_attr0_value = self.der_decode( + signed_attr0_values[0], + asn1Spec=krb5_asn1.ContentType()) + if using_pkinit is PkInit.DIFFIE_HELLMAN: + self.assertEqual(str(krb5_asn1.id_pkinit_DHKeyData), + signed_attr0_value) + else: + self.assertEqual(str(krb5_asn1.id_pkinit_rkeyData), + signed_attr0_value) + + signed_attr1 = signed_attrs[1] + self.assertEqual(str(krb5_asn1.id_messageDigest), + signed_attr1['type']) + signed_attr1_values = signed_attr1['values'] + self.assertEqual(1, len(signed_attr1_values)) + message_digest = self.der_decode(signed_attr1_values[0], + krb5_asn1.MessageDigest()) + + signature_algorithm = signer_info['signatureAlgorithm'] + hash_fn = self.hash_from_algorithm_id(signature_algorithm) + + # Compute the hash of the content to be signed. With the + # Diffie-Hellman key exchange, this signature is over the type + # KDCDHKeyInfo; otherwise, it is over the type ReplyKeyPack. + digest = hashes.Hash(digest_hash_fn(), default_backend()) + digest.update(content) + digest = digest.finalize() + + # Verify the hash. Note: this is a non–constant time comparison. + self.assertEqual(digest, message_digest) + + # Re-encode the attributes ready for verifying the signature. + cms_attrs = self.der_encode(signed_attrs, + asn1Spec=krb5_asn1.CMSAttributes()) + + # Verify the signature. + kdc_public_key = kdc_certificate.public_key() + kdc_public_key.verify( + signer_info['signature'], + cms_attrs, + asymmetric.padding.PKCS1v15(), + hash_fn()) + + self.assertFalse(signer_info.get('unsignedAttrs')) + + if armor_key is not None: + if PADATA_FX_FAST in pa_dict: + fx_fast_data = pa_dict[PADATA_FX_FAST] + fast_response = self.check_fx_fast_data(kdc_exchange_dict, + fx_fast_data, + armor_key, + finished=True) + + if 'strengthen-key' in fast_response: + strengthen_key = self.EncryptionKey_import( + fast_response['strengthen-key']) + encpart_decryption_key = ( + self.generate_strengthen_reply_key( + strengthen_key, + encpart_decryption_key)) + + fast_finished = fast_response.get('finished') + if fast_finished is not None: + ticket_checksum = fast_finished['ticket-checksum'] + + self.check_rep_padata(kdc_exchange_dict, + callback_dict, + fast_response['padata'], + error_code=0) + + ticket_private = None + if ticket_decryption_key is not None: + self.assertElementEqual(ticket_encpart, 'etype', + ticket_decryption_key.etype) + self.assertElementKVNO(ticket_encpart, 'kvno', + ticket_decryption_key.kvno) + ticket_decpart = ticket_decryption_key.decrypt(KU_TICKET, + ticket_cipher) + ticket_private = self.der_decode( + ticket_decpart, + asn1Spec=krb5_asn1.EncTicketPart()) + + encpart_private = None + self.assertIsNotNone(encpart_decryption_key) + if encpart_decryption_key is not None: + self.assertElementEqual(encpart, 'etype', + encpart_decryption_key.etype) + if self.strict_checking: + self.assertElementKVNO(encpart, 'kvno', + encpart_decryption_key.kvno) + rep_decpart = encpart_decryption_key.decrypt( + encpart_decryption_usage, + encpart_cipher) + # MIT KDC encodes both EncASRepPart and EncTGSRepPart with + # application tag 26 + try: + encpart_private = self.der_decode( + rep_decpart, + asn1Spec=rep_encpart_asn1Spec()) + except Exception: + encpart_private = self.der_decode( + rep_decpart, + asn1Spec=krb5_asn1.EncTGSRepPart()) + + kdc_exchange_dict['reply_key'] = encpart_decryption_key + + self.assertIsNotNone(check_kdc_private_fn) + if check_kdc_private_fn is not None: + check_kdc_private_fn(kdc_exchange_dict, callback_dict, + rep, ticket_private, encpart_private, + ticket_checksum) + + return rep + + def check_fx_fast_data(self, + kdc_exchange_dict, + fx_fast_data, + armor_key, + finished=False, + expect_strengthen_key=True): + fx_fast_data = self.der_decode(fx_fast_data, + asn1Spec=krb5_asn1.PA_FX_FAST_REPLY()) + + enc_fast_rep = fx_fast_data['armored-data']['enc-fast-rep'] + self.assertEqual(enc_fast_rep['etype'], armor_key.etype) + + fast_rep = armor_key.decrypt(KU_FAST_REP, enc_fast_rep['cipher']) + + fast_response = self.der_decode(fast_rep, + asn1Spec=krb5_asn1.KrbFastResponse()) + + if expect_strengthen_key and self.strict_checking: + self.assertIn('strengthen-key', fast_response) + + if finished: + self.assertIn('finished', fast_response) + + # Ensure that the nonce matches the nonce in the body of the request + # (RFC6113 5.4.3). + nonce = kdc_exchange_dict['nonce'] + self.assertEqual(nonce, fast_response['nonce']) + + return fast_response + + def generic_check_kdc_private(self, + kdc_exchange_dict, + callback_dict, + rep, + ticket_private, + encpart_private, + ticket_checksum): + kdc_options = kdc_exchange_dict['kdc_options'] + canon_pos = len(tuple(krb5_asn1.KDCOptions('canonicalize'))) - 1 + canonicalize = (canon_pos < len(kdc_options) + and kdc_options[canon_pos] == '1') + renewable_pos = len(tuple(krb5_asn1.KDCOptions('renewable'))) - 1 + renewable = (renewable_pos < len(kdc_options) + and kdc_options[renewable_pos] == '1') + renew_pos = len(tuple(krb5_asn1.KDCOptions('renew'))) - 1 + renew = (renew_pos < len(kdc_options) + and kdc_options[renew_pos] == '1') + expect_renew_till = renewable or renew + + expected_crealm = kdc_exchange_dict['expected_crealm'] + expected_cname = kdc_exchange_dict['expected_cname'] + expected_srealm = kdc_exchange_dict['expected_srealm'] + expected_sname = kdc_exchange_dict['expected_sname'] + ticket_decryption_key = kdc_exchange_dict['ticket_decryption_key'] + + rep_msg_type = kdc_exchange_dict['rep_msg_type'] + + expected_flags = kdc_exchange_dict.get('expected_flags') + unexpected_flags = kdc_exchange_dict.get('unexpected_flags') + + ticket = self.getElementValue(rep, 'ticket') + + if ticket_checksum is not None: + armor_key = kdc_exchange_dict['armor_key'] + self.verify_ticket_checksum(ticket, ticket_checksum, armor_key) + + to_rodc = kdc_exchange_dict['to_rodc'] + if to_rodc: + krbtgt_creds = self.get_rodc_krbtgt_creds() + else: + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + krbtgt_keys = [krbtgt_key] + if not self.strict_checking: + krbtgt_key_rc4 = self.TicketDecryptionKey_from_creds( + krbtgt_creds, + etype=kcrypto.Enctype.RC4) + krbtgt_keys.append(krbtgt_key_rc4) + + if self.expect_pac and self.is_tgs(expected_sname): + expect_pac = True + else: + expect_pac = kdc_exchange_dict['expect_pac'] + + ticket_session_key = None + if ticket_private is not None: + self.assertElementFlags(ticket_private, 'flags', + expected_flags, + unexpected_flags) + self.assertElementPresent(ticket_private, 'key') + ticket_key = self.getElementValue(ticket_private, 'key') + self.assertIsNotNone(ticket_key) + if ticket_key is not None: # Never None, but gives indentation + self.assertElementPresent(ticket_key, 'keytype') + self.assertElementPresent(ticket_key, 'keyvalue') + ticket_session_key = self.EncryptionKey_import(ticket_key) + self.assertElementEqualUTF8(ticket_private, 'crealm', + expected_crealm) + if self.cname_checking: + self.assertElementEqualPrincipal(ticket_private, 'cname', + expected_cname) + self.assertElementPresent(ticket_private, 'transited') + self.assertElementPresent(ticket_private, 'authtime') + if self.strict_checking: + self.assertElementPresent(ticket_private, 'starttime') + self.assertElementPresent(ticket_private, 'endtime') + if self.strict_checking: + if expect_renew_till: + self.assertElementPresent(ticket_private, 'renew-till') + else: + self.assertElementMissing(ticket_private, 'renew-till') + if self.strict_checking: + self.assertElementMissing(ticket_private, 'caddr') + if expect_pac is not None: + if expect_pac: + self.assertElementPresent(ticket_private, + 'authorization-data', + expect_empty=not expect_pac) + else: + # It is more correct to not have an authorization-data + # present than an empty one. + # + # https://github.com/krb5/krb5/pull/1225#issuecomment-995104193 + v = self.getElementValue(ticket_private, + 'authorization-data') + if v is not None: + self.assertElementPresent(ticket_private, + 'authorization-data', + expect_empty=True) + + encpart_session_key = None + if encpart_private is not None: + self.assertElementPresent(encpart_private, 'key') + encpart_key = self.getElementValue(encpart_private, 'key') + self.assertIsNotNone(encpart_key) + if encpart_key is not None: # Never None, but gives indentation + self.assertElementPresent(encpart_key, 'keytype') + self.assertElementPresent(encpart_key, 'keyvalue') + encpart_session_key = self.EncryptionKey_import(encpart_key) + self.assertElementPresent(encpart_private, 'last-req') + expected_nonce = kdc_exchange_dict.get('pk_nonce') + if not expected_nonce: + expected_nonce = kdc_exchange_dict['nonce'] + self.assertElementEqual(encpart_private, 'nonce', + expected_nonce) + if rep_msg_type == KRB_AS_REP: + if self.strict_checking: + self.assertElementPresent(encpart_private, + 'key-expiration') + else: + self.assertElementMissing(encpart_private, + 'key-expiration') + self.assertElementFlags(encpart_private, 'flags', + expected_flags, + unexpected_flags) + self.assertElementPresent(encpart_private, 'authtime') + if self.strict_checking: + self.assertElementPresent(encpart_private, 'starttime') + self.assertElementPresent(encpart_private, 'endtime') + if self.strict_checking: + if expect_renew_till: + self.assertElementPresent(encpart_private, 'renew-till') + else: + self.assertElementMissing(encpart_private, 'renew-till') + self.assertElementEqualUTF8(encpart_private, 'srealm', + expected_srealm) + self.assertElementEqualPrincipal(encpart_private, 'sname', + expected_sname) + if self.strict_checking: + self.assertElementMissing(encpart_private, 'caddr') + + sent_pac_options = self.get_sent_pac_options(kdc_exchange_dict) + + sent_enc_pa_rep = self.sent_enc_pa_rep(kdc_exchange_dict) + + enc_padata = self.getElementValue(encpart_private, + 'encrypted-pa-data') + if (canonicalize or '1' in sent_pac_options or ( + rep_msg_type == KRB_AS_REP and sent_enc_pa_rep)): + if self.strict_checking: + self.assertIsNotNone(enc_padata) + + if enc_padata is not None: + enc_pa_dict = self.get_pa_dict(enc_padata) + if self.strict_checking: + if canonicalize: + self.assertIn(PADATA_SUPPORTED_ETYPES, enc_pa_dict) + else: + self.assertNotIn(PADATA_SUPPORTED_ETYPES, + enc_pa_dict) + + if '1' in sent_pac_options: + self.assertIn(PADATA_PAC_OPTIONS, enc_pa_dict) + else: + self.assertNotIn(PADATA_PAC_OPTIONS, enc_pa_dict) + + if rep_msg_type == KRB_AS_REP and sent_enc_pa_rep: + self.assertIn(PADATA_REQ_ENC_PA_REP, enc_pa_dict) + else: + self.assertNotIn(PADATA_REQ_ENC_PA_REP, enc_pa_dict) + + if PADATA_SUPPORTED_ETYPES in enc_pa_dict: + expected_supported_etypes = kdc_exchange_dict[ + 'expected_supported_etypes'] + + (supported_etypes,) = struct.unpack( + '<L', + enc_pa_dict[PADATA_SUPPORTED_ETYPES]) + + ignore_bits = (security.KERB_ENCTYPE_DES_CBC_CRC | + security.KERB_ENCTYPE_DES_CBC_MD5) + + self.assertEqual( + supported_etypes & ~ignore_bits, + expected_supported_etypes & ~ignore_bits, + f'PADATA_SUPPORTED_ETYPES: got: {supported_etypes} (0x{supported_etypes:X}), ' + f'expected: {expected_supported_etypes} (0x{expected_supported_etypes:X})') + + if PADATA_PAC_OPTIONS in enc_pa_dict: + pac_options = self.der_decode( + enc_pa_dict[PADATA_PAC_OPTIONS], + asn1Spec=krb5_asn1.PA_PAC_OPTIONS()) + + self.assertElementEqual(pac_options, 'options', + sent_pac_options) + + if PADATA_REQ_ENC_PA_REP in enc_pa_dict: + enc_pa_rep = enc_pa_dict[PADATA_REQ_ENC_PA_REP] + + enc_pa_rep = self.der_decode( + enc_pa_rep, + asn1Spec=krb5_asn1.Checksum()) + + reply_key = kdc_exchange_dict['reply_key'] + req_obj = kdc_exchange_dict['req_obj'] + req_asn1Spec = kdc_exchange_dict['req_asn1Spec'] + + req_obj = self.der_encode(req_obj, + asn1Spec=req_asn1Spec()) + + checksum = enc_pa_rep['checksum'] + ctype = enc_pa_rep['cksumtype'] + + reply_key.verify_checksum(KU_AS_REQ, + req_obj, + ctype, + checksum) + else: + if enc_padata is not None: + self.assertEqual(enc_padata, []) + + if ticket_session_key is not None and encpart_session_key is not None: + self.assertEqual(ticket_session_key.etype, + encpart_session_key.etype) + self.assertEqual(ticket_session_key.key.contents, + encpart_session_key.key.contents) + if encpart_session_key is not None: + session_key = encpart_session_key + else: + session_key = ticket_session_key + ticket_creds = KerberosTicketCreds( + ticket, + session_key, + crealm=expected_crealm, + cname=expected_cname, + srealm=expected_srealm, + sname=expected_sname, + decryption_key=ticket_decryption_key, + ticket_private=ticket_private, + encpart_private=encpart_private) + + if ticket_private is not None: + pac_data = self.get_ticket_pac(ticket_creds, expect_pac=expect_pac) + if expect_pac is True: + self.assertIsNotNone(pac_data) + elif expect_pac is False: + self.assertIsNone(pac_data) + + if pac_data is not None: + self.check_pac_buffers(pac_data, kdc_exchange_dict) + + expect_ticket_checksum = kdc_exchange_dict['expect_ticket_checksum'] + expect_full_checksum = kdc_exchange_dict['expect_full_checksum'] + if expect_ticket_checksum or expect_full_checksum: + self.assertIsNotNone(ticket_decryption_key) + + if ticket_decryption_key is not None: + service_ticket = (rep_msg_type == KRB_TGS_REP + and not self.is_tgs_principal(expected_sname)) + self.verify_ticket(ticket_creds, krbtgt_keys, + service_ticket=service_ticket, + expect_pac=expect_pac, + expect_ticket_checksum=expect_ticket_checksum + or self.tkt_sig_support, + expect_full_checksum=expect_full_checksum + or self.full_sig_support) + + kdc_exchange_dict['rep_ticket_creds'] = ticket_creds + + # Check the SIDs in a LOGON_INFO PAC buffer. + def check_logon_info_sids(self, logon_info_buffer, kdc_exchange_dict): + info3 = logon_info_buffer.info.info.info3 + logon_info = info3.base + resource_groups = logon_info_buffer.info.info.resource_groups + + expected_groups = kdc_exchange_dict['expected_groups'] + unexpected_groups = kdc_exchange_dict['unexpected_groups'] + expected_domain_sid = kdc_exchange_dict['expected_domain_sid'] + expected_sid = kdc_exchange_dict['expected_sid'] + + domain_sid = logon_info.domain_sid + if expected_domain_sid is not None: + self.assertEqual(expected_domain_sid, str(domain_sid)) + + if expected_sid is not None: + got_sid = f'{domain_sid}-{logon_info.rid}' + self.assertEqual(expected_sid, got_sid) + + if expected_groups is None and unexpected_groups is None: + # Nothing more to do. + return + + # Check the SIDs in the PAC. + + # Form a representation of the PAC, containing at first the primary + # GID. + primary_sid = f'{domain_sid}-{logon_info.primary_gid}' + pac_sids = { + (primary_sid, self.SidType.PRIMARY_GID, None), + } + + # Collect the Extra SIDs. + if info3.sids is not None: + self.assertTrue(logon_info.user_flags & ( + netlogon.NETLOGON_EXTRA_SIDS), + 'extra SIDs present, but EXTRA_SIDS flag not set') + self.assertTrue(info3.sids, 'got empty SIDs') + + for sid_attr in info3.sids: + got_sid = str(sid_attr.sid) + if unexpected_groups is not None: + self.assertNotIn(got_sid, unexpected_groups) + + pac_sid = (got_sid, + self.SidType.EXTRA_SID, + sid_attr.attributes) + self.assertNotIn(pac_sid, pac_sids, 'got duplicated SID') + pac_sids.add(pac_sid) + else: + self.assertFalse(logon_info.user_flags & ( + netlogon.NETLOGON_EXTRA_SIDS), + 'no extra SIDs present, but EXTRA_SIDS flag set') + + # Collect the Base RIDs. + if logon_info.groups.rids is not None: + self.assertTrue(logon_info.groups.rids, 'got empty RIDs') + + for group in logon_info.groups.rids: + got_sid = f'{domain_sid}-{group.rid}' + if unexpected_groups is not None: + self.assertNotIn(got_sid, unexpected_groups) + + pac_sid = (got_sid, self.SidType.BASE_SID, group.attributes) + self.assertNotIn(pac_sid, pac_sids, 'got duplicated SID') + pac_sids.add(pac_sid) + + # Collect the Resource SIDs. + expect_resource_groups_flag = kdc_exchange_dict[ + 'expect_resource_groups_flag'] + expect_set_reason = '' + expect_reset_reason = '' + if expect_resource_groups_flag is None: + expect_resource_groups_flag = ( + resource_groups.groups.rids is not None) + expect_set_reason = 'resource groups present, but ' + expect_reset_reason = 'no resource groups present, but ' + + if expect_resource_groups_flag: + self.assertTrue( + logon_info.user_flags & netlogon.NETLOGON_RESOURCE_GROUPS, + f'{expect_set_reason}RESOURCE_GROUPS flag unexpectedly reset') + else: + self.assertFalse( + logon_info.user_flags & netlogon.NETLOGON_RESOURCE_GROUPS, + f'{expect_reset_reason}RESOURCE_GROUPS flag unexpectedly set') + + if resource_groups.groups.rids is not None: + self.assertTrue(resource_groups.groups.rids, 'got empty RIDs') + + resource_group_sid = resource_groups.domain_sid + for resource_group in resource_groups.groups.rids: + got_sid = f'{resource_group_sid}-{resource_group.rid}' + if unexpected_groups is not None: + self.assertNotIn(got_sid, unexpected_groups) + + pac_sid = (got_sid, + self.SidType.RESOURCE_SID, + resource_group.attributes) + self.assertNotIn(pac_sid, pac_sids, 'got duplicated SID') + pac_sids.add(pac_sid) + + # Compare the aggregated SIDs against the set of expected SIDs. + if expected_groups is not None: + if ... in expected_groups: + # The caller is only interested in asserting the + # presence of particular groups, and doesn't mind if + # other groups are present as well. + pac_sids.add(...) + self.assertLessEqual(expected_groups, pac_sids, + 'expected groups') + else: + # The caller wants to make sure the groups match + # exactly. + self.assertEqual(expected_groups, pac_sids, + 'expected != got') + + def check_device_info(self, device_info, kdc_exchange_dict): + armor_tgt = kdc_exchange_dict['armor_tgt'] + armor_auth_data = armor_tgt.ticket_private.get( + 'authorization-data') + self.assertIsNotNone(armor_auth_data, + 'missing authdata for armor TGT') + armor_pac_data = self.get_pac(armor_auth_data) + armor_pac = ndr_unpack(krb5pac.PAC_DATA, armor_pac_data) + for armor_pac_buffer in armor_pac.buffers: + if armor_pac_buffer.type == krb5pac.PAC_TYPE_LOGON_INFO: + armor_info = armor_pac_buffer.info.info.info3 + break + else: + self.fail('missing logon info for armor PAC') + self.assertEqual(armor_info.base.rid, device_info.rid) + + device_domain_sid = kdc_exchange_dict['expected_device_domain_sid'] + expected_device_groups = kdc_exchange_dict['expected_device_groups'] + if kdc_exchange_dict['expect_device_info']: + self.assertIsNotNone(device_domain_sid) + self.assertIsNotNone(expected_device_groups) + + if device_domain_sid is not None: + self.assertEqual(device_domain_sid, str(device_info.domain_sid)) + else: + device_domain_sid = str(device_info.domain_sid) + + # Check the device info SIDs. + + # A representation of the device info groups. + primary_sid = f'{device_domain_sid}-{device_info.primary_gid}' + got_sids = { + (primary_sid, self.SidType.PRIMARY_GID, None), + } + + # Collect the groups. + if device_info.groups.rids is not None: + self.assertTrue(device_info.groups.rids, 'got empty RIDs') + + for group in device_info.groups.rids: + got_sid = f'{device_domain_sid}-{group.rid}' + + device_sid = (got_sid, self.SidType.BASE_SID, group.attributes) + self.assertNotIn(device_sid, got_sids, 'got duplicated SID') + got_sids.add(device_sid) + + # Collect the SIDs. + if device_info.sids is not None: + self.assertTrue(device_info.sids, 'got empty SIDs') + + for sid_attr in device_info.sids: + got_sid = str(sid_attr.sid) + + in_a_domain = sid_attr.sid.num_auths == 5 and ( + str(sid_attr.sid).startswith('S-1-5-21-')) + self.assertFalse(in_a_domain, + f'got unexpected SID for domain: {got_sid} ' + f'(should be in device_info.domain_groups)') + + device_sid = (got_sid, + self.SidType.EXTRA_SID, + sid_attr.attributes) + self.assertNotIn(device_sid, got_sids, 'got duplicated SID') + got_sids.add(device_sid) + + # Collect the domain groups. + if device_info.domain_groups is not None: + self.assertTrue(device_info.domain_groups, 'got empty domain groups') + + for domain_group in device_info.domain_groups: + self.assertTrue(domain_group, 'got empty domain group') + + got_domain_sids = set() + + resource_group_sid = domain_group.domain_sid + + in_a_domain = resource_group_sid.num_auths == 4 and ( + str(resource_group_sid).startswith('S-1-5-21-')) + self.assertTrue( + in_a_domain, + f'got unexpected domain SID for non-domain: {resource_group_sid} ' + f'(should be in device_info.sids)') + + for resource_group in domain_group.groups.rids: + got_sid = f'{resource_group_sid}-{resource_group.rid}' + + device_sid = (got_sid, + self.SidType.RESOURCE_SID, + resource_group.attributes) + self.assertNotIn(device_sid, got_domain_sids, 'got duplicated SID') + got_domain_sids.add(device_sid) + + got_domain_sids = frozenset(got_domain_sids) + self.assertNotIn(got_domain_sids, got_sids) + got_sids.add(got_domain_sids) + + # Compare the aggregated device SIDs against the set of expected device + # SIDs. + if expected_device_groups is not None: + self.assertEqual(expected_device_groups, got_sids, + 'expected != got') + + def check_pac_buffers(self, pac_data, kdc_exchange_dict): + pac = ndr_unpack(krb5pac.PAC_DATA, pac_data) + + rep_msg_type = kdc_exchange_dict['rep_msg_type'] + armor_tgt = kdc_exchange_dict['armor_tgt'] + + compound_id = rep_msg_type == KRB_TGS_REP and armor_tgt is not None + + expected_sname = kdc_exchange_dict['expected_sname'] + expect_client_claims = kdc_exchange_dict['expect_client_claims'] + expect_device_info = kdc_exchange_dict['expect_device_info'] + expect_device_claims = kdc_exchange_dict['expect_device_claims'] + + expected_types = [krb5pac.PAC_TYPE_LOGON_INFO, + krb5pac.PAC_TYPE_SRV_CHECKSUM, + krb5pac.PAC_TYPE_KDC_CHECKSUM, + krb5pac.PAC_TYPE_LOGON_NAME, + krb5pac.PAC_TYPE_UPN_DNS_INFO] + + kdc_options = kdc_exchange_dict['kdc_options'] + pos = len(tuple(krb5_asn1.KDCOptions('cname-in-addl-tkt'))) - 1 + constrained_delegation = (pos < len(kdc_options) + and kdc_options[pos] == '1') + if constrained_delegation: + expected_types.append(krb5pac.PAC_TYPE_CONSTRAINED_DELEGATION) + + require_strict = set() + unchecked = set() + if not self.tkt_sig_support: + require_strict.add(krb5pac.PAC_TYPE_TICKET_CHECKSUM) + if not self.full_sig_support: + require_strict.add(krb5pac.PAC_TYPE_FULL_CHECKSUM) + + expected_client_claims = kdc_exchange_dict['expected_client_claims'] + unexpected_client_claims = kdc_exchange_dict[ + 'unexpected_client_claims'] + + if self.kdc_claims_support and expect_client_claims: + expected_types.append(krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO) + else: + self.assertFalse( + expected_client_claims, + 'expected client claims, but client claims not expected in ' + 'PAC') + self.assertFalse( + unexpected_client_claims, + 'unexpected client claims, but client claims not expected in ' + 'PAC') + + if expect_client_claims is None: + unchecked.add(krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO) + + expected_device_claims = kdc_exchange_dict['expected_device_claims'] + unexpected_device_claims = kdc_exchange_dict['unexpected_device_claims'] + + expected_device_groups = kdc_exchange_dict['expected_device_groups'] + + if (self.kdc_claims_support and self.kdc_compound_id_support + and expect_device_claims and compound_id): + expected_types.append(krb5pac.PAC_TYPE_DEVICE_CLAIMS_INFO) + else: + self.assertFalse( + expect_device_claims, + 'expected device claims buffer, but device claims not ' + 'expected in PAC') + self.assertFalse( + expected_device_claims, + 'expected device claims, but device claims not expected in ' + 'PAC') + self.assertFalse( + unexpected_device_claims, + 'unexpected device claims, but device claims not expected in ' + 'PAC') + + if expect_device_claims is None and compound_id: + unchecked.add(krb5pac.PAC_TYPE_DEVICE_CLAIMS_INFO) + + if self.kdc_compound_id_support and compound_id and expect_device_info: + expected_types.append(krb5pac.PAC_TYPE_DEVICE_INFO) + else: + self.assertFalse(expect_device_info, + 'expected device info with no armor TGT or ' + 'for non-TGS request') + self.assertFalse(expected_device_groups, + 'expected device groups, but device info not ' + 'expected in PAC') + + if expect_device_info is None and compound_id: + unchecked.add(krb5pac.PAC_TYPE_DEVICE_INFO) + + if rep_msg_type == KRB_TGS_REP: + if not self.is_tgs_principal(expected_sname): + expected_types.append(krb5pac.PAC_TYPE_TICKET_CHECKSUM) + expected_types.append(krb5pac.PAC_TYPE_FULL_CHECKSUM) + + expect_extra_pac_buffers = self.is_tgs(expected_sname) + + expect_pac_attrs = kdc_exchange_dict['expect_pac_attrs'] + + if expect_pac_attrs: + expect_pac_attrs_pac_request = kdc_exchange_dict[ + 'expect_pac_attrs_pac_request'] + else: + expect_pac_attrs_pac_request = kdc_exchange_dict[ + 'pac_request'] + + if expect_pac_attrs is None: + if self.expect_extra_pac_buffers: + expect_pac_attrs = expect_extra_pac_buffers + else: + require_strict.add(krb5pac.PAC_TYPE_ATTRIBUTES_INFO) + if expect_pac_attrs: + expected_types.append(krb5pac.PAC_TYPE_ATTRIBUTES_INFO) + + expect_requester_sid = kdc_exchange_dict['expect_requester_sid'] + expected_requester_sid = kdc_exchange_dict['expected_requester_sid'] + + if expect_requester_sid is None: + if self.expect_extra_pac_buffers: + expect_requester_sid = expect_extra_pac_buffers + else: + require_strict.add(krb5pac.PAC_TYPE_REQUESTER_SID) + if expected_requester_sid is not None: + expect_requester_sid = True + if expect_requester_sid: + expected_types.append(krb5pac.PAC_TYPE_REQUESTER_SID) + + sent_pk_as_req = self.sent_pk_as_req(kdc_exchange_dict) or ( + self.sent_pk_as_req_win2k(kdc_exchange_dict)) + if sent_pk_as_req: + expected_types.append(krb5pac.PAC_TYPE_CREDENTIAL_INFO) + + expected_extra_pac_buffers = kdc_exchange_dict['expected_extra_pac_buffers'] + if expected_extra_pac_buffers is not None: + expected_types.extend(expected_extra_pac_buffers) + + buffer_types = [pac_buffer.type + for pac_buffer in pac.buffers] + self.assertSequenceElementsEqual( + expected_types, buffer_types, + require_ordered=False, + require_strict=require_strict, + unchecked=unchecked) + + expected_account_name = kdc_exchange_dict['expected_account_name'] + expected_sid = kdc_exchange_dict['expected_sid'] + + expect_upn_dns_info_ex = kdc_exchange_dict['expect_upn_dns_info_ex'] + if expect_upn_dns_info_ex is None and ( + expected_account_name is not None + or expected_sid is not None): + expect_upn_dns_info_ex = True + + for pac_buffer in pac.buffers: + if pac_buffer.type == krb5pac.PAC_TYPE_CONSTRAINED_DELEGATION: + expected_proxy_target = kdc_exchange_dict[ + 'expected_proxy_target'] + expected_transited_services = kdc_exchange_dict[ + 'expected_transited_services'] + + delegation_info = pac_buffer.info.info + + self.assertEqual(expected_proxy_target, + str(delegation_info.proxy_target)) + + transited_services = list(map( + str, delegation_info.transited_services)) + self.assertEqual(expected_transited_services, + transited_services) + + elif pac_buffer.type == krb5pac.PAC_TYPE_LOGON_NAME: + expected_cname = kdc_exchange_dict['expected_cname'] + account_name = '/'.join(expected_cname['name-string']) + + self.assertEqual(account_name, pac_buffer.info.account_name) + + elif pac_buffer.type == krb5pac.PAC_TYPE_LOGON_INFO: + info3 = pac_buffer.info.info.info3 + logon_info = info3.base + + if expected_account_name is not None: + self.assertEqual(expected_account_name, + str(logon_info.account_name)) + + self.check_logon_info_sids(pac_buffer, kdc_exchange_dict) + + elif pac_buffer.type == krb5pac.PAC_TYPE_UPN_DNS_INFO: + upn_dns_info = pac_buffer.info + upn_dns_info_ex = upn_dns_info.ex + + expected_realm = kdc_exchange_dict['expected_crealm'] + self.assertEqual(expected_realm, + upn_dns_info.dns_domain_name) + + expected_upn_name = kdc_exchange_dict['expected_upn_name'] + if expected_upn_name is not None: + self.assertEqual(expected_upn_name, + upn_dns_info.upn_name) + + if expect_upn_dns_info_ex: + self.assertIsNotNone(upn_dns_info_ex) + + if upn_dns_info_ex is not None: + if expected_account_name is not None: + self.assertEqual(expected_account_name, + upn_dns_info_ex.samaccountname) + + if expected_sid is not None: + self.assertEqual(expected_sid, + str(upn_dns_info_ex.objectsid)) + + elif (pac_buffer.type == krb5pac.PAC_TYPE_ATTRIBUTES_INFO + and expect_pac_attrs): + attr_info = pac_buffer.info + + self.assertEqual(2, attr_info.flags_length) + + flags = attr_info.flags + + requested_pac = bool(flags & 1) + given_pac = bool(flags & 2) + + self.assertEqual(expect_pac_attrs_pac_request is True, + requested_pac) + self.assertEqual(expect_pac_attrs_pac_request is None, + given_pac) + + elif (pac_buffer.type == krb5pac.PAC_TYPE_REQUESTER_SID + and expect_requester_sid): + requester_sid = pac_buffer.info.sid + + if expected_requester_sid is None: + expected_requester_sid = expected_sid + if expected_sid is not None: + self.assertEqual(expected_requester_sid, + str(requester_sid)) + + elif pac_buffer.type in {krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO, + krb5pac.PAC_TYPE_DEVICE_CLAIMS_INFO}: + remaining = pac_buffer.info.remaining + + if pac_buffer.type == krb5pac.PAC_TYPE_CLIENT_CLAIMS_INFO: + claims_type = 'client claims' + expected_claims = expected_client_claims + unexpected_claims = unexpected_client_claims + else: + claims_type = 'device claims' + expected_claims = expected_device_claims + unexpected_claims = unexpected_device_claims + + if not remaining: + # Windows may produce an empty claims buffer. + self.assertFalse(expected_claims, + f'expected {claims_type}, but the PAC ' + f'buffer was empty') + continue + + if expected_claims: + empty_msg = f', and {claims_type} were expected' + else: + empty_msg = f' for {claims_type} (should be missing)' + + claims_metadata_ndr = ndr_unpack(claims.CLAIMS_SET_METADATA_NDR, + remaining) + claims_metadata = claims_metadata_ndr.claims.metadata + self.assertIsNotNone(claims_metadata, + f'got empty CLAIMS_SET_METADATA_NDR ' + f'inner structure {empty_msg}') + + self.assertIsNotNone(claims_metadata.claims_set, + f'got empty CLAIMS_SET_METADATA ' + f'structure {empty_msg}') + + uncompressed_size = claims_metadata.uncompressed_claims_set_size + compression_format = claims_metadata.compression_format + + if uncompressed_size < ( + claims.CLAIM_LOWER_COMPRESSION_THRESHOLD): + self.assertEqual(claims.CLAIMS_COMPRESSION_FORMAT_NONE, + compression_format, + f'{claims_type} unexpectedly ' + f'compressed ({uncompressed_size} ' + f'bytes uncompressed)') + elif uncompressed_size >= ( + claims.CLAIM_UPPER_COMPRESSION_THRESHOLD): + self.assertEqual( + claims.CLAIMS_COMPRESSION_FORMAT_XPRESS_HUFF, + compression_format, + f'{claims_type} unexpectedly not compressed ' + f'({uncompressed_size} bytes uncompressed)') + + claims_set = claims_metadata.claims_set.claims.claims + self.assertIsNotNone(claims_set, + f'got empty CLAIMS_SET_NDR inner ' + f'structure {empty_msg}') + + claims_arrays = claims_set.claims_arrays + self.assertIsNotNone(claims_arrays, + f'got empty CLAIMS_SET structure ' + f'{empty_msg}') + self.assertGreater(len(claims_arrays), 0, + f'got empty claims array {empty_msg}') + self.assertEqual(len(claims_arrays), + claims_set.claims_array_count, + f'{claims_type} arrays size mismatch') + + got_claims = {} + + for claims_array in claims_arrays: + claim_entries = claims_array.claim_entries + self.assertIsNotNone(claim_entries, + f'got empty CLAIMS_ARRAY structure ' + f'{empty_msg}') + self.assertGreater(len(claim_entries), 0, + f'got empty claim entries array ' + f'{empty_msg}') + self.assertEqual(len(claim_entries), + claims_array.claims_count, + f'{claims_type} entries array size ' + f'mismatch') + + for entry in claim_entries: + if unexpected_claims is not None: + self.assertNotIn(entry.id, unexpected_claims, + f'got unexpected {claims_type} ' + f'in PAC') + if expected_claims is None: + continue + + expected_claim = expected_claims.get(entry.id) + if expected_claim is None: + continue + + self.assertNotIn(entry.id, got_claims, + f'got duplicate {claims_type}') + + self.assertIsNotNone(entry.values.values, + f'got {claims_type} with no ' + f'values') + self.assertGreater(len(entry.values.values), 0, + f'got empty {claims_type} values ' + f'array') + self.assertEqual(len(entry.values.values), + entry.values.value_count, + f'{claims_type} values array size ' + f'mismatch') + + expected_claim_values = expected_claim.get('values') + self.assertIsNotNone(expected_claim_values, + f'got expected {claims_type} ' + f'with no values') + + values = type(expected_claim_values)( + entry.values.values) + + got_claims[entry.id] = { + 'source_type': claims_array.claims_source_type, + 'type': entry.type, + 'values': values, + } + + self.assertEqual(expected_claims, got_claims or None, + f'{claims_type} did not match expectations') + + elif pac_buffer.type == krb5pac.PAC_TYPE_DEVICE_INFO: + device_info = pac_buffer.info.info + + self.check_device_info(device_info, kdc_exchange_dict) + + elif pac_buffer.type == krb5pac.PAC_TYPE_CREDENTIAL_INFO: + credential_info = pac_buffer.info + + expected_etype = self.expected_etype(kdc_exchange_dict) + + self.assertEqual(0, credential_info.version) + self.assertEqual(expected_etype, + credential_info.encryption_type) + + encrypted_data = credential_info.encrypted_data + reply_key = kdc_exchange_dict['reply_key'] + + data = reply_key.decrypt(KU_NON_KERB_SALT, encrypted_data) + + credential_data_ndr = ndr_unpack( + krb5pac.PAC_CREDENTIAL_DATA_NDR, data) + + credential_data = credential_data_ndr.ctr.data + + self.assertEqual(1, credential_data.credential_count) + self.assertEqual(credential_data.credential_count, + len(credential_data.credentials)) + + package = credential_data.credentials[0] + self.assertEqual('NTLM', str(package.package_name)) + + ntlm_blob = bytes(package.credential) + + ntlm_package = ndr_unpack(krb5pac.PAC_CREDENTIAL_NTLM_SECPKG, + ntlm_blob) + + self.assertEqual(0, ntlm_package.version) + self.assertEqual(krb5pac.PAC_CREDENTIAL_NTLM_HAS_NT_HASH, + ntlm_package.flags) + + creds = kdc_exchange_dict['creds'] + nt_password = bytes(ntlm_package.nt_password.hash) + self.assertEqual(creds.get_nt_hash(), nt_password) + + lm_password = bytes(ntlm_package.lm_password.hash) + self.assertEqual(bytes(16), lm_password) + + def generic_check_kdc_error(self, + kdc_exchange_dict, + callback_dict, + rep, + inner=False): + + rep_msg_type = kdc_exchange_dict['rep_msg_type'] + + expected_anon = kdc_exchange_dict['expected_anon'] + expected_srealm = kdc_exchange_dict['expected_srealm'] + expected_sname = kdc_exchange_dict['expected_sname'] + expected_error_mode = kdc_exchange_dict['expected_error_mode'] + + sent_fast = self.sent_fast(kdc_exchange_dict) + + fast_armor_type = kdc_exchange_dict['fast_armor_type'] + + self.assertElementEqual(rep, 'pvno', 5) + self.assertElementEqual(rep, 'msg-type', KRB_ERROR) + error_code = self.getElementValue(rep, 'error-code') + self.assertIn(error_code, expected_error_mode) + if self.strict_checking: + self.assertElementMissing(rep, 'ctime') + self.assertElementMissing(rep, 'cusec') + self.assertElementPresent(rep, 'stime') + self.assertElementPresent(rep, 'susec') + # error-code checked above + if expected_anon and not inner: + expected_cname = self.PrincipalName_create( + name_type=NT_WELLKNOWN, + names=['WELLKNOWN', 'ANONYMOUS']) + self.assertElementEqualPrincipal(rep, 'cname', expected_cname) + elif self.strict_checking: + self.assertElementMissing(rep, 'cname') + if self.strict_checking: + self.assertElementMissing(rep, 'crealm') + self.assertElementEqualUTF8(rep, 'realm', expected_srealm) + self.assertElementEqualPrincipal(rep, 'sname', expected_sname) + self.assertElementMissing(rep, 'e-text') + expect_status = kdc_exchange_dict['expect_status'] + expected_status = kdc_exchange_dict['expected_status'] + expect_edata = kdc_exchange_dict['expect_edata'] + if expect_edata is None: + expect_edata = (error_code != KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS + and (not sent_fast or fast_armor_type is None + or fast_armor_type == FX_FAST_ARMOR_AP_REQUEST) + and not inner) + if inner and expect_edata is self.expect_padata_outer: + expect_edata = False + if not expect_edata: + self.assertFalse(expect_status) + if self.strict_checking or expect_status is False: + self.assertElementMissing(rep, 'e-data') + return rep + edata = self.getElementValue(rep, 'e-data') + if self.strict_checking or expect_status: + self.assertIsNotNone(edata) + if edata is not None: + try: + error_data = self.der_decode( + edata, + asn1Spec=krb5_asn1.KERB_ERROR_DATA()) + except PyAsn1Error: + if expect_status: + # The test requires that the KDC be declared to support + # NTSTATUS values in e-data to proceed. + self.assertTrue( + self.expect_nt_status, + 'expected status code (which, according to ' + 'EXPECT_NT_STATUS=0, the KDC does not support)') + + self.fail('expected to get status code') + + rep_padata = self.der_decode( + edata, asn1Spec=krb5_asn1.METHOD_DATA()) + self.assertGreater(len(rep_padata), 0) + + if sent_fast: + self.assertEqual(1, len(rep_padata)) + rep_pa_dict = self.get_pa_dict(rep_padata) + self.assertIn(PADATA_FX_FAST, rep_pa_dict) + + armor_key = kdc_exchange_dict['armor_key'] + self.assertIsNotNone(armor_key) + fast_response = self.check_fx_fast_data( + kdc_exchange_dict, + rep_pa_dict[PADATA_FX_FAST], + armor_key, + expect_strengthen_key=False) + + rep_padata = fast_response['padata'] + + etype_info2 = self.check_rep_padata(kdc_exchange_dict, + callback_dict, + rep_padata, + error_code) + + kdc_exchange_dict['preauth_etype_info2'] = etype_info2 + else: + self.assertTrue(self.expect_nt_status, + 'got status code, but EXPECT_NT_STATUS=0') + + if expect_status is not None: + self.assertTrue(expect_status, + 'got unexpected status code') + + self.assertEqual(KERB_ERR_TYPE_EXTENDED, + error_data['data-type']) + + extended_error = error_data['data-value'] + + self.assertEqual(12, len(extended_error)) + + status = int.from_bytes(extended_error[:4], 'little') + flags = int.from_bytes(extended_error[8:], 'little') + + self.assertEqual(expected_status, status) + + if rep_msg_type == KRB_TGS_REP: + self.assertEqual(3, flags) + else: + self.assertEqual(1, flags) + + return rep + + def check_reply_padata(self, + kdc_exchange_dict, + callback_dict, + encpart, + rep_padata): + expected_patypes = () + + sent_fast = self.sent_fast(kdc_exchange_dict) + rep_msg_type = kdc_exchange_dict['rep_msg_type'] + + if sent_fast: + expected_patypes += (PADATA_FX_FAST,) + elif rep_msg_type == KRB_AS_REP: + if self.sent_pk_as_req(kdc_exchange_dict): + expected_patypes += PADATA_PK_AS_REP, + elif self.sent_pk_as_req_win2k(kdc_exchange_dict): + expected_patypes += PADATA_PK_AS_REP_19, + else: + chosen_etype = self.getElementValue(encpart, 'etype') + self.assertIsNotNone(chosen_etype) + + if chosen_etype in {kcrypto.Enctype.AES256, + kcrypto.Enctype.AES128}: + expected_patypes += (PADATA_ETYPE_INFO2,) + + preauth_key = kdc_exchange_dict['preauth_key'] + self.assertIsInstance(preauth_key, Krb5EncryptionKey) + if preauth_key.etype == kcrypto.Enctype.RC4 and rep_padata is None: + rep_padata = () + elif rep_msg_type == KRB_TGS_REP: + if expected_patypes == () and rep_padata is None: + rep_padata = () + + if not self.strict_checking and rep_padata is None: + rep_padata = () + + self.assertIsNotNone(rep_padata) + got_patypes = tuple(pa['padata-type'] for pa in rep_padata) + self.assertSequenceElementsEqual(expected_patypes, got_patypes, + # Windows does not add this. + unchecked={PADATA_PKINIT_KX}) + + if len(expected_patypes) == 0: + return None + + pa_dict = self.get_pa_dict(rep_padata) + + etype_info2 = pa_dict.get(PADATA_ETYPE_INFO2) + if etype_info2 is not None: + etype_info2 = self.der_decode(etype_info2, + asn1Spec=krb5_asn1.ETYPE_INFO2()) + self.assertEqual(len(etype_info2), 1) + elem = etype_info2[0] + + e = self.getElementValue(elem, 'etype') + self.assertEqual(e, chosen_etype) + salt = self.getElementValue(elem, 'salt') + self.assertIsNotNone(salt) + expected_salt = kdc_exchange_dict['expected_salt'] + if expected_salt is not None: + self.assertEqual(salt, expected_salt) + s2kparams = self.getElementValue(elem, 's2kparams') + if self.strict_checking: + self.assertIsNone(s2kparams) + + @staticmethod + def greatest_common_etype(etypes, proposed_etypes): + return max(filter(lambda e: e in etypes, proposed_etypes), + default=None) + + @staticmethod + def first_common_etype(etypes, proposed_etypes): + return next(filter(lambda e: e in etypes, proposed_etypes), None) + + def supported_aes_rc4_etypes(self, kdc_exchange_dict): + creds = kdc_exchange_dict['creds'] + supported_etypes = self.get_default_enctypes(creds) + + rc4_support = kdc_exchange_dict['rc4_support'] + + aes_etypes = set() + if kcrypto.Enctype.AES256 in supported_etypes: + aes_etypes.add(kcrypto.Enctype.AES256) + if kcrypto.Enctype.AES128 in supported_etypes: + aes_etypes.add(kcrypto.Enctype.AES128) + + rc4_etypes = set() + if rc4_support and kcrypto.Enctype.RC4 in supported_etypes: + rc4_etypes.add(kcrypto.Enctype.RC4) + + return aes_etypes, rc4_etypes + + def greatest_aes_rc4_etypes(self, kdc_exchange_dict): + req_body = kdc_exchange_dict['req_body'] + proposed_etypes = req_body['etype'] + + aes_etypes, rc4_etypes = self.supported_aes_rc4_etypes(kdc_exchange_dict) + + expected_aes = self.greatest_common_etype(aes_etypes, proposed_etypes) + expected_rc4 = self.greatest_common_etype(rc4_etypes, proposed_etypes) + + return expected_aes, expected_rc4 + + def expected_etype(self, kdc_exchange_dict): + req_body = kdc_exchange_dict['req_body'] + proposed_etypes = req_body['etype'] + + aes_etypes, rc4_etypes = self.supported_aes_rc4_etypes( + kdc_exchange_dict) + + return self.first_common_etype(aes_etypes | rc4_etypes, + proposed_etypes) + + def check_rep_padata(self, + kdc_exchange_dict, + callback_dict, + rep_padata, + error_code): + rep_msg_type = kdc_exchange_dict['rep_msg_type'] + + sent_fast = self.sent_fast(kdc_exchange_dict) + sent_enc_challenge = self.sent_enc_challenge(kdc_exchange_dict) + + if rep_msg_type == KRB_TGS_REP: + self.assertTrue(sent_fast) + + rc4_support = kdc_exchange_dict['rc4_support'] + + expected_aes, expected_rc4 = self.greatest_aes_rc4_etypes( + kdc_exchange_dict) + + expect_etype_info2 = () + expect_etype_info = False + if expected_aes is not None: + expect_etype_info2 += (expected_aes,) + if expected_rc4 is not None: + if error_code != 0: + expect_etype_info2 += (expected_rc4,) + if expected_aes is None: + expect_etype_info = True + + if expect_etype_info: + self.assertGreater(len(expect_etype_info2), 0) + + sent_pac_options = self.get_sent_pac_options(kdc_exchange_dict) + + check_patypes = kdc_exchange_dict['check_patypes'] + if check_patypes: + expected_patypes = () + if sent_fast and error_code != 0: + expected_patypes += (PADATA_FX_ERROR,) + expected_patypes += (PADATA_FX_COOKIE,) + + if rep_msg_type == KRB_TGS_REP: + if ('1' in sent_pac_options + and error_code not in (0, KDC_ERR_GENERIC)): + expected_patypes += (PADATA_PAC_OPTIONS,) + elif error_code != KDC_ERR_GENERIC: + if expect_etype_info: + expected_patypes += (PADATA_ETYPE_INFO,) + if len(expect_etype_info2) != 0: + expected_patypes += (PADATA_ETYPE_INFO2,) + + sent_freshness = self.sent_freshness(kdc_exchange_dict) + + if error_code not in (KDC_ERR_PREAUTH_FAILED, KDC_ERR_SKEW, + KDC_ERR_POLICY, KDC_ERR_CLIENT_REVOKED): + if sent_fast: + expected_patypes += (PADATA_ENCRYPTED_CHALLENGE,) + else: + expected_patypes += (PADATA_ENC_TIMESTAMP,) + + if not sent_enc_challenge: + expected_patypes += (PADATA_PK_AS_REQ,) + if not sent_freshness: + expected_patypes += (PADATA_PK_AS_REP_19,) + + if sent_freshness: + expected_patypes += PADATA_AS_FRESHNESS, + + if (self.kdc_fast_support + and not sent_fast + and not sent_enc_challenge): + expected_patypes += (PADATA_FX_FAST,) + expected_patypes += (PADATA_FX_COOKIE,) + + require_strict = {PADATA_FX_COOKIE, + PADATA_FX_FAST, + PADATA_PAC_OPTIONS, + PADATA_PK_AS_REP_19, + PADATA_PK_AS_REQ, + PADATA_PKINIT_KX, + PADATA_GSS} + strict_edata_checking = kdc_exchange_dict['strict_edata_checking'] + if not strict_edata_checking: + require_strict.add(PADATA_ETYPE_INFO2) + require_strict.add(PADATA_ENCRYPTED_CHALLENGE) + + got_patypes = tuple(pa['padata-type'] for pa in rep_padata) + self.assertSequenceElementsEqual(expected_patypes, got_patypes, + require_strict=require_strict, + unchecked={PADATA_PW_SALT}) + + if not expected_patypes: + return None + + pa_dict = self.get_pa_dict(rep_padata) + + enc_timestamp = pa_dict.get(PADATA_ENC_TIMESTAMP) + if enc_timestamp is not None: + self.assertEqual(len(enc_timestamp), 0) + + pk_as_req = pa_dict.get(PADATA_PK_AS_REQ) + if pk_as_req is not None: + self.assertEqual(len(pk_as_req), 0) + + pk_as_rep19 = pa_dict.get(PADATA_PK_AS_REP_19) + if pk_as_rep19 is not None: + self.assertEqual(len(pk_as_rep19), 0) + + freshness_token = pa_dict.get(PADATA_AS_FRESHNESS) + if freshness_token is not None: + self.assertEqual(bytes(2), freshness_token[:2]) + + freshness = self.der_decode(freshness_token[2:], + asn1Spec=krb5_asn1.EncryptedData()) + + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + self.assertElementEqual(freshness, 'etype', krbtgt_key.etype) + self.assertElementKVNO(freshness, 'kvno', krbtgt_key.kvno) + + # Decrypt the freshness token. + ts_enc = krbtgt_key.decrypt(KU_AS_FRESHNESS, + freshness['cipher']) + + # Ensure that we can decode it as PA-ENC-TS-ENC. + ts_enc = self.der_decode(ts_enc, + asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + freshness_time = self.get_EpochFromKerberosTime( + ts_enc['patimestamp']) + freshness_time += ts_enc['pausec'] / 1e6 + + # Ensure that it is reasonably close to the current time (within + # five minutes, to allow for clock skew). + current_time = datetime.datetime.now( + datetime.timezone.utc).timestamp() + self.assertLess(current_time - 5 * 60, freshness_time) + self.assertLess(freshness_time, current_time + 5 * 60) + + kdc_exchange_dict['freshness_token'] = freshness_token + + fx_fast = pa_dict.get(PADATA_FX_FAST) + if fx_fast is not None: + self.assertEqual(len(fx_fast), 0) + + fast_cookie = pa_dict.get(PADATA_FX_COOKIE) + if fast_cookie is not None: + kdc_exchange_dict['fast_cookie'] = fast_cookie + + fast_error = pa_dict.get(PADATA_FX_ERROR) + if fast_error is not None: + fast_error = self.der_decode(fast_error, + asn1Spec=krb5_asn1.KRB_ERROR()) + self.generic_check_kdc_error(kdc_exchange_dict, + callback_dict, + fast_error, + inner=True) + + pac_options = pa_dict.get(PADATA_PAC_OPTIONS) + if pac_options is not None: + pac_options = self.der_decode( + pac_options, + asn1Spec=krb5_asn1.PA_PAC_OPTIONS()) + self.assertElementEqual(pac_options, 'options', sent_pac_options) + + enc_challenge = pa_dict.get(PADATA_ENCRYPTED_CHALLENGE) + if enc_challenge is not None: + if not sent_enc_challenge: + self.assertEqual(len(enc_challenge), 0) + else: + armor_key = kdc_exchange_dict['armor_key'] + self.assertIsNotNone(armor_key) + + preauth_key, _ = self.get_preauth_key(kdc_exchange_dict) + + kdc_challenge_key = self.generate_kdc_challenge_key( + armor_key, preauth_key) + + # Ensure that the encrypted challenge FAST factor is supported + # (RFC6113 5.4.6). + if self.strict_checking: + self.assertNotEqual(len(enc_challenge), 0) + if len(enc_challenge) != 0: + encrypted_challenge = self.der_decode( + enc_challenge, + asn1Spec=krb5_asn1.EncryptedData()) + self.assertEqual(encrypted_challenge['etype'], + kdc_challenge_key.etype) + + challenge = kdc_challenge_key.decrypt( + KU_ENC_CHALLENGE_KDC, + encrypted_challenge['cipher']) + challenge = self.der_decode( + challenge, + asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + # Retrieve the returned timestamp. + rep_patime = challenge['patimestamp'] + self.assertIn('pausec', challenge) + + # Ensure the returned time is within five minutes of the + # current time. + rep_time = self.get_EpochFromKerberosTime(rep_patime) + current_time = time.time() + + self.assertLess(current_time - 300, rep_time) + self.assertLess(rep_time, current_time + 300) + + etype_info2 = pa_dict.get(PADATA_ETYPE_INFO2) + if etype_info2 is not None: + etype_info2 = self.der_decode(etype_info2, + asn1Spec=krb5_asn1.ETYPE_INFO2()) + self.assertGreaterEqual(len(etype_info2), 1) + if self.strict_checking: + self.assertEqual(len(etype_info2), len(expect_etype_info2)) + for i in range(0, len(etype_info2)): + e = self.getElementValue(etype_info2[i], 'etype') + if self.strict_checking: + self.assertEqual(e, expect_etype_info2[i]) + salt = self.getElementValue(etype_info2[i], 'salt') + if e == kcrypto.Enctype.RC4: + if self.strict_checking: + self.assertIsNone(salt) + else: + self.assertIsNotNone(salt) + expected_salt = kdc_exchange_dict['expected_salt'] + if expected_salt is not None: + self.assertEqual(salt, expected_salt) + s2kparams = self.getElementValue(etype_info2[i], 's2kparams') + if self.strict_checking: + self.assertIsNone(s2kparams) + + etype_info = pa_dict.get(PADATA_ETYPE_INFO) + if etype_info is not None: + etype_info = self.der_decode(etype_info, + asn1Spec=krb5_asn1.ETYPE_INFO()) + self.assertEqual(len(etype_info), 1) + e = self.getElementValue(etype_info[0], 'etype') + self.assertEqual(e, kcrypto.Enctype.RC4) + if rc4_support: + self.assertEqual(e, expect_etype_info2[0]) + salt = self.getElementValue(etype_info[0], 'salt') + if self.strict_checking: + self.assertIsNotNone(salt) + self.assertEqual(len(salt), 0) + + return etype_info2 + + def generate_simple_fast(self, + kdc_exchange_dict, + _callback_dict, + req_body, + fast_padata, + fast_armor, + checksum, + fast_options=''): + armor_key = kdc_exchange_dict['armor_key'] + + fast_req = self.KRB_FAST_REQ_create(fast_options, + fast_padata, + req_body) + fast_req = self.der_encode(fast_req, + asn1Spec=krb5_asn1.KrbFastReq()) + fast_req = self.EncryptedData_create(armor_key, + KU_FAST_ENC, + fast_req) + + fast_armored_req = self.KRB_FAST_ARMORED_REQ_create(fast_armor, + checksum, + fast_req) + + fx_fast_request = self.PA_FX_FAST_REQUEST_create(fast_armored_req) + fx_fast_request = self.der_encode( + fx_fast_request, + asn1Spec=krb5_asn1.PA_FX_FAST_REQUEST()) + + fast_padata = self.PA_DATA_create(PADATA_FX_FAST, + fx_fast_request) + + return fast_padata + + def generate_ap_req(self, + kdc_exchange_dict, + _callback_dict, + req_body, + armor, + usage=None, + seq_number=None): + req_body_checksum = None + + if armor: + self.assertIsNone(req_body) + + tgt = kdc_exchange_dict['armor_tgt'] + authenticator_subkey = kdc_exchange_dict['armor_subkey'] + else: + tgt = kdc_exchange_dict['tgt'] + authenticator_subkey = kdc_exchange_dict['authenticator_subkey'] + + if req_body is not None: + body_checksum_type = kdc_exchange_dict['body_checksum_type'] + + req_body_blob = self.der_encode( + req_body, asn1Spec=krb5_asn1.KDC_REQ_BODY()) + + req_body_checksum = self.Checksum_create( + tgt.session_key, + KU_TGS_REQ_AUTH_CKSUM, + req_body_blob, + ctype=body_checksum_type) + + auth_data = kdc_exchange_dict['auth_data'] + + subkey_obj = None + if authenticator_subkey is not None: + subkey_obj = authenticator_subkey.export_obj() + if seq_number is None: + seq_number = random.randint(0, 0xfffffffe) + (ctime, cusec) = self.get_KerberosTimeWithUsec() + authenticator_obj = self.Authenticator_create( + crealm=tgt.crealm, + cname=tgt.cname, + cksum=req_body_checksum, + cusec=cusec, + ctime=ctime, + subkey=subkey_obj, + seq_number=seq_number, + authorization_data=auth_data) + authenticator_blob = self.der_encode( + authenticator_obj, + asn1Spec=krb5_asn1.Authenticator()) + + if usage is None: + usage = KU_AP_REQ_AUTH if armor else KU_TGS_REQ_AUTH + authenticator = self.EncryptedData_create(tgt.session_key, + usage, + authenticator_blob) + + if armor: + ap_options = kdc_exchange_dict['fast_ap_options'] + else: + ap_options = kdc_exchange_dict['ap_options'] + if ap_options is None: + ap_options = str(krb5_asn1.APOptions('0')) + ap_req_obj = self.AP_REQ_create(ap_options=ap_options, + ticket=tgt.ticket, + authenticator=authenticator) + ap_req = self.der_encode(ap_req_obj, asn1Spec=krb5_asn1.AP_REQ()) + + return ap_req + + def generate_simple_tgs_padata(self, + kdc_exchange_dict, + callback_dict, + req_body): + ap_req = self.generate_ap_req(kdc_exchange_dict, + callback_dict, + req_body, + armor=False) + pa_tgs_req = self.PA_DATA_create(PADATA_KDC_REQ, ap_req) + padata = [pa_tgs_req] + + return padata, req_body + + def get_preauth_key(self, kdc_exchange_dict): + msg_type = kdc_exchange_dict['rep_msg_type'] + + if msg_type == KRB_AS_REP: + key = kdc_exchange_dict['preauth_key'] + usage = KU_AS_REP_ENC_PART + else: # KRB_TGS_REP + authenticator_subkey = kdc_exchange_dict['authenticator_subkey'] + if authenticator_subkey is not None: + key = authenticator_subkey + usage = KU_TGS_REP_ENC_PART_SUB_KEY + else: + tgt = kdc_exchange_dict['tgt'] + key = tgt.session_key + usage = KU_TGS_REP_ENC_PART_SESSION + + self.assertIsNotNone(key) + + return key, usage + + def generate_armor_key(self, subkey, session_key): + armor_key = kcrypto.cf2(subkey.key, + session_key.key, + b'subkeyarmor', + b'ticketarmor') + armor_key = Krb5EncryptionKey(armor_key, None) + + return armor_key + + def generate_strengthen_reply_key(self, strengthen_key, reply_key): + strengthen_reply_key = kcrypto.cf2(strengthen_key.key, + reply_key.key, + b'strengthenkey', + b'replykey') + strengthen_reply_key = Krb5EncryptionKey(strengthen_reply_key, + reply_key.kvno) + + return strengthen_reply_key + + def generate_client_challenge_key(self, armor_key, longterm_key): + client_challenge_key = kcrypto.cf2(armor_key.key, + longterm_key.key, + b'clientchallengearmor', + b'challengelongterm') + client_challenge_key = Krb5EncryptionKey(client_challenge_key, None) + + return client_challenge_key + + def generate_kdc_challenge_key(self, armor_key, longterm_key): + kdc_challenge_key = kcrypto.cf2(armor_key.key, + longterm_key.key, + b'kdcchallengearmor', + b'challengelongterm') + kdc_challenge_key = Krb5EncryptionKey(kdc_challenge_key, None) + + return kdc_challenge_key + + def verify_ticket_checksum(self, ticket, expected_checksum, armor_key): + expected_type = expected_checksum['cksumtype'] + self.assertEqual(armor_key.ctype, expected_type) + + ticket_blob = self.der_encode(ticket, + asn1Spec=krb5_asn1.Ticket()) + checksum = self.Checksum_create(armor_key, + KU_FAST_FINISHED, + ticket_blob) + self.assertEqual(expected_checksum, checksum) + + def verify_ticket(self, ticket, krbtgt_keys, service_ticket, + expect_pac=True, + expect_ticket_checksum=True, + expect_full_checksum=None): + # Decrypt the ticket. + + key = ticket.decryption_key + enc_part = ticket.ticket['enc-part'] + + self.assertElementEqual(enc_part, 'etype', key.etype) + self.assertElementKVNO(enc_part, 'kvno', key.kvno) + + enc_part = key.decrypt(KU_TICKET, enc_part['cipher']) + enc_part = self.der_decode( + enc_part, asn1Spec=krb5_asn1.EncTicketPart()) + + # Fetch the authorization data from the ticket. + auth_data = enc_part.get('authorization-data') + if expect_pac: + self.assertIsNotNone(auth_data) + elif auth_data is None: + return + + # Get a copy of the authdata with an empty PAC, and the existing PAC + # (if present). + empty_pac = self.get_empty_pac() + auth_data, pac_data = self.replace_pac(auth_data, + empty_pac, + expect_pac=expect_pac) + if not expect_pac: + return + + # Unpack the PAC as both PAC_DATA and PAC_DATA_RAW types. We use the + # raw type to create a new PAC with zeroed signatures for + # verification. This is because on Windows, the resource_groups field + # is added to PAC_LOGON_INFO after the info3 field has been created, + # which results in a different ordering of pointer values than Samba + # (see commit 0e201ecdc53). Using the raw type avoids changing + # PAC_LOGON_INFO, so verification against Windows can work. We still + # need the PAC_DATA type to retrieve the actual checksums, because the + # signatures in the raw type may contain padding bytes. + pac = ndr_unpack(krb5pac.PAC_DATA, + pac_data) + raw_pac = ndr_unpack(krb5pac.PAC_DATA_RAW, + pac_data) + + checksums = {} + + full_checksum_buffer = None + + for pac_buffer, raw_pac_buffer in zip(pac.buffers, raw_pac.buffers): + buffer_type = pac_buffer.type + if buffer_type in self.pac_checksum_types: + self.assertNotIn(buffer_type, checksums, + f'Duplicate checksum type {buffer_type}') + + # Fetch the checksum and the checksum type from the PAC buffer. + checksum = pac_buffer.info.signature + ctype = pac_buffer.info.type + if ctype & 1 << 31: + ctype |= -1 << 31 + + checksums[buffer_type] = checksum, ctype + + if buffer_type == krb5pac.PAC_TYPE_FULL_CHECKSUM: + full_checksum_buffer = raw_pac_buffer + elif buffer_type != krb5pac.PAC_TYPE_TICKET_CHECKSUM: + # Zero the checksum field so that we can later verify the + # checksums. The ticket checksum field is not zeroed. + + signature = ndr_unpack( + krb5pac.PAC_SIGNATURE_DATA, + raw_pac_buffer.info.remaining) + signature.signature = bytes(len(checksum)) + raw_pac_buffer.info.remaining = ndr_pack( + signature) + + # Re-encode the PAC. + pac_data = ndr_pack(raw_pac) + + if full_checksum_buffer is not None: + signature = ndr_unpack( + krb5pac.PAC_SIGNATURE_DATA, + full_checksum_buffer.info.remaining) + signature.signature = bytes(len(checksum)) + full_checksum_buffer.info.remaining = ndr_pack( + signature) + + # Re-encode the PAC. + full_pac_data = ndr_pack(raw_pac) + + # Verify the signatures. + + server_checksum, server_ctype = checksums[ + krb5pac.PAC_TYPE_SRV_CHECKSUM] + key.verify_checksum(KU_NON_KERB_CKSUM_SALT, + pac_data, + server_ctype, + server_checksum) + + kdc_checksum, kdc_ctype = checksums[ + krb5pac.PAC_TYPE_KDC_CHECKSUM] + + if isinstance(krbtgt_keys, collections.abc.Container): + if self.strict_checking: + krbtgt_key = krbtgt_keys[0] + else: + krbtgt_key = next(key for key in krbtgt_keys + if key.ctype == kdc_ctype) + else: + krbtgt_key = krbtgt_keys + + krbtgt_key.verify_rodc_checksum(KU_NON_KERB_CKSUM_SALT, + server_checksum, + kdc_ctype, + kdc_checksum) + + if not service_ticket: + self.assertNotIn(krb5pac.PAC_TYPE_TICKET_CHECKSUM, checksums) + self.assertNotIn(krb5pac.PAC_TYPE_FULL_CHECKSUM, checksums) + else: + ticket_checksum, ticket_ctype = checksums.get( + krb5pac.PAC_TYPE_TICKET_CHECKSUM, + (None, None)) + if expect_ticket_checksum: + self.assertIsNotNone(ticket_checksum) + elif expect_ticket_checksum is False: + self.assertIsNone(ticket_checksum) + if ticket_checksum is not None: + enc_part['authorization-data'] = auth_data + enc_part = self.der_encode(enc_part, + asn1Spec=krb5_asn1.EncTicketPart()) + + krbtgt_key.verify_rodc_checksum(KU_NON_KERB_CKSUM_SALT, + enc_part, + ticket_ctype, + ticket_checksum) + + full_checksum, full_ctype = checksums.get( + krb5pac.PAC_TYPE_FULL_CHECKSUM, + (None, None)) + if expect_full_checksum: + self.assertIsNotNone(full_checksum) + elif expect_full_checksum is False: + self.assertIsNone(full_checksum) + if full_checksum is not None: + krbtgt_key.verify_rodc_checksum(KU_NON_KERB_CKSUM_SALT, + full_pac_data, + full_ctype, + full_checksum) + + def modified_ticket(self, + ticket, *, + new_ticket_key=None, + modify_fn=None, + modify_pac_fn=None, + exclude_pac=False, + allow_empty_authdata=False, + update_pac_checksums=None, + checksum_keys=None, + include_checksums=None): + if checksum_keys is None: + # A dict containing a key for each checksum type to be created in + # the PAC. + checksum_keys = {} + else: + checksum_keys = dict(checksum_keys) + + if include_checksums is None: + # A dict containing a value for each checksum type; True if the + # checksum type is to be included in the PAC, False if it is to be + # excluded, or None/not present if the checksum is to be included + # based on its presence in the original PAC. + include_checksums = {} + else: + include_checksums = dict(include_checksums) + + # Check that the values passed in by the caller make sense. + + self.assertLessEqual(checksum_keys.keys(), self.pac_checksum_types) + self.assertLessEqual(include_checksums.keys(), self.pac_checksum_types) + + if update_pac_checksums is None: + update_pac_checksums = not exclude_pac + + if exclude_pac: + self.assertIsNone(modify_pac_fn) + self.assertFalse(update_pac_checksums) + + if not update_pac_checksums: + self.assertFalse(checksum_keys) + self.assertFalse(include_checksums) + + expect_pac = bool(modify_pac_fn) + + key = ticket.decryption_key + + if new_ticket_key is None: + # Use the same key to re-encrypt the ticket. + new_ticket_key = key + + if krb5pac.PAC_TYPE_SRV_CHECKSUM not in checksum_keys: + # If the server signature key is not present, fall back to the key + # used to encrypt the ticket. + checksum_keys[krb5pac.PAC_TYPE_SRV_CHECKSUM] = new_ticket_key + + if krb5pac.PAC_TYPE_TICKET_CHECKSUM not in checksum_keys: + # If the ticket signature key is not present, fall back to the key + # used for the KDC signature. + kdc_checksum_key = checksum_keys.get(krb5pac.PAC_TYPE_KDC_CHECKSUM) + if kdc_checksum_key is not None: + checksum_keys[krb5pac.PAC_TYPE_TICKET_CHECKSUM] = ( + kdc_checksum_key) + + if krb5pac.PAC_TYPE_FULL_CHECKSUM not in checksum_keys: + # If the full signature key is not present, fall back to the key + # used for the KDC signature. + kdc_checksum_key = checksum_keys.get(krb5pac.PAC_TYPE_KDC_CHECKSUM) + if kdc_checksum_key is not None: + checksum_keys[krb5pac.PAC_TYPE_FULL_CHECKSUM] = ( + kdc_checksum_key) + + # Decrypt the ticket. + + enc_part = ticket.ticket['enc-part'] + + self.assertElementEqual(enc_part, 'etype', key.etype) + self.assertElementKVNO(enc_part, 'kvno', key.kvno) + + enc_part = key.decrypt(KU_TICKET, enc_part['cipher']) + enc_part = self.der_decode( + enc_part, asn1Spec=krb5_asn1.EncTicketPart()) + + # Modify the ticket here. + if callable(modify_fn): + enc_part = modify_fn(enc_part) + elif modify_fn: + for fn in modify_fn: + enc_part = fn(enc_part) + + auth_data = enc_part.get('authorization-data') + if expect_pac: + self.assertIsNotNone(auth_data) + if auth_data is not None: + new_pac = None + if exclude_pac: + need_to_call_replace_pac = True + elif not modify_pac_fn and not update_pac_checksums: + need_to_call_replace_pac = False + else: + need_to_call_replace_pac = True + # Get a copy of the authdata with an empty PAC, and the + # existing PAC (if present). + empty_pac = self.get_empty_pac() + empty_pac_auth_data, pac_data = self.replace_pac( + auth_data, + empty_pac, + expect_pac=expect_pac) + + if pac_data is not None: + pac = ndr_unpack(krb5pac.PAC_DATA, pac_data) + + # Modify the PAC here. + if callable(modify_pac_fn): + pac = modify_pac_fn(pac) + elif modify_pac_fn: + for fn in modify_pac_fn: + pac = fn(pac) + + if update_pac_checksums: + # Get the enc-part with an empty PAC, which is needed + # to create a ticket signature. + enc_part_to_sign = enc_part.copy() + enc_part_to_sign['authorization-data'] = ( + empty_pac_auth_data) + enc_part_to_sign = self.der_encode( + enc_part_to_sign, + asn1Spec=krb5_asn1.EncTicketPart()) + + self.update_pac_checksums(pac, + checksum_keys, + include_checksums, + enc_part_to_sign) + + # Re-encode the PAC. + pac_data = ndr_pack(pac) + new_pac = self.AuthorizationData_create(AD_WIN2K_PAC, + pac_data) + + # Replace the PAC in the authorization data and re-add it to the + # ticket enc-part. + if need_to_call_replace_pac: + auth_data, _ = self.replace_pac( + auth_data, new_pac, + expect_pac=expect_pac, + allow_empty_authdata=allow_empty_authdata) + enc_part['authorization-data'] = auth_data + + # Re-encrypt the ticket enc-part with the new key. + enc_part_new = self.der_encode(enc_part, + asn1Spec=krb5_asn1.EncTicketPart()) + enc_part_new = self.EncryptedData_create(new_ticket_key, + KU_TICKET, + enc_part_new) + + # Create a copy of the ticket with the new enc-part. + new_ticket = ticket.ticket.copy() + new_ticket['enc-part'] = enc_part_new + + new_ticket_creds = KerberosTicketCreds( + new_ticket, + session_key=ticket.session_key, + crealm=ticket.crealm, + cname=ticket.cname, + srealm=ticket.srealm, + sname=ticket.sname, + decryption_key=new_ticket_key, + ticket_private=enc_part, + encpart_private=ticket.encpart_private) + + return new_ticket_creds + + def update_pac_checksums(self, + pac, + checksum_keys, + include_checksums, + enc_part=None): + pac_buffers = pac.buffers + checksum_buffers = {} + + # Find the relevant PAC checksum buffers. + for pac_buffer in pac_buffers: + buffer_type = pac_buffer.type + if buffer_type in self.pac_checksum_types: + self.assertNotIn(buffer_type, checksum_buffers, + f'Duplicate checksum type {buffer_type}') + + checksum_buffers[buffer_type] = pac_buffer + + # Create any additional buffers that were requested but not + # present. Conversely, remove any buffers that were requested to be + # removed. + for buffer_type in self.pac_checksum_types: + if buffer_type in checksum_buffers: + if include_checksums.get(buffer_type) is False: + checksum_buffer = checksum_buffers.pop(buffer_type) + + pac.num_buffers -= 1 + pac_buffers.remove(checksum_buffer) + + elif include_checksums.get(buffer_type) is True: + info = krb5pac.PAC_SIGNATURE_DATA() + + checksum_buffer = krb5pac.PAC_BUFFER() + checksum_buffer.type = buffer_type + checksum_buffer.info = info + + pac_buffers.append(checksum_buffer) + pac.num_buffers += 1 + + checksum_buffers[buffer_type] = checksum_buffer + + # Fill the relevant checksum buffers. + for buffer_type, checksum_buffer in checksum_buffers.items(): + checksum_key = checksum_keys[buffer_type] + ctype = checksum_key.ctype & ((1 << 32) - 1) + + if buffer_type == krb5pac.PAC_TYPE_TICKET_CHECKSUM: + self.assertIsNotNone(enc_part) + + signature = checksum_key.make_rodc_checksum( + KU_NON_KERB_CKSUM_SALT, + enc_part) + + elif buffer_type == krb5pac.PAC_TYPE_SRV_CHECKSUM: + signature = checksum_key.make_zeroed_checksum() + + else: + signature = checksum_key.make_rodc_zeroed_checksum() + + checksum_buffer.info.signature = signature + checksum_buffer.info.type = ctype + + # Add the new checksum buffers to the PAC. + pac.buffers = pac_buffers + + # Calculate the full checksum and insert it into the PAC. + full_checksum_buffer = checksum_buffers.get( + krb5pac.PAC_TYPE_FULL_CHECKSUM) + if full_checksum_buffer is not None: + full_checksum_key = checksum_keys[krb5pac.PAC_TYPE_FULL_CHECKSUM] + + pac_data = ndr_pack(pac) + full_checksum = full_checksum_key.make_rodc_checksum( + KU_NON_KERB_CKSUM_SALT, + pac_data) + + full_checksum_buffer.info.signature = full_checksum + + # Calculate the server and KDC checksums and insert them into the PAC. + + server_checksum_buffer = checksum_buffers.get( + krb5pac.PAC_TYPE_SRV_CHECKSUM) + if server_checksum_buffer is not None: + server_checksum_key = checksum_keys[krb5pac.PAC_TYPE_SRV_CHECKSUM] + + pac_data = ndr_pack(pac) + server_checksum = server_checksum_key.make_checksum( + KU_NON_KERB_CKSUM_SALT, + pac_data) + + server_checksum_buffer.info.signature = server_checksum + + kdc_checksum_buffer = checksum_buffers.get( + krb5pac.PAC_TYPE_KDC_CHECKSUM) + if kdc_checksum_buffer is not None: + if server_checksum_buffer is None: + # There's no server signature to make the checksum over, so + # just make the checksum over an empty bytes object. + server_checksum = bytes() + + kdc_checksum_key = checksum_keys[krb5pac.PAC_TYPE_KDC_CHECKSUM] + + kdc_checksum = kdc_checksum_key.make_rodc_checksum( + KU_NON_KERB_CKSUM_SALT, + server_checksum) + + kdc_checksum_buffer.info.signature = kdc_checksum + + def replace_pac(self, auth_data, new_pac, expect_pac=True, + allow_empty_authdata=False): + if new_pac is not None: + self.assertElementEqual(new_pac, 'ad-type', AD_WIN2K_PAC) + self.assertElementPresent(new_pac, 'ad-data') + + new_auth_data = [] + + ad_relevant = None + old_pac = None + + for authdata_elem in auth_data: + if authdata_elem['ad-type'] == AD_IF_RELEVANT: + ad_relevant = self.der_decode( + authdata_elem['ad-data'], + asn1Spec=krb5_asn1.AD_IF_RELEVANT()) + + relevant_elems = [] + for relevant_elem in ad_relevant: + if relevant_elem['ad-type'] == AD_WIN2K_PAC: + self.assertIsNone(old_pac, 'Multiple PACs detected') + old_pac = relevant_elem['ad-data'] + + if new_pac is not None: + relevant_elems.append(new_pac) + else: + relevant_elems.append(relevant_elem) + if expect_pac: + self.assertIsNotNone(old_pac, 'Expected PAC') + + if relevant_elems or allow_empty_authdata: + ad_relevant = self.der_encode( + relevant_elems, + asn1Spec=krb5_asn1.AD_IF_RELEVANT()) + + authdata_elem = self.AuthorizationData_create( + AD_IF_RELEVANT, + ad_relevant) + else: + authdata_elem = None + + if authdata_elem is not None or allow_empty_authdata: + new_auth_data.append(authdata_elem) + + if expect_pac: + self.assertIsNotNone(ad_relevant, 'Expected AD-RELEVANT') + + return new_auth_data, old_pac + + def get_pac(self, auth_data, expect_pac=True): + _, pac = self.replace_pac(auth_data, None, expect_pac) + return pac + + def get_ticket_pac(self, ticket, expect_pac=True): + auth_data = ticket.ticket_private.get('authorization-data') + if expect_pac: + self.assertIsNotNone(auth_data) + elif auth_data is None: + return None + + return self.get_pac(auth_data, expect_pac=expect_pac) + + def get_krbtgt_checksum_key(self): + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + return { + krb5pac.PAC_TYPE_KDC_CHECKSUM: krbtgt_key + } + + def is_tgs_principal(self, principal): + if self.is_tgs(principal): + return True + + if self.kadmin_is_tgs and self.is_kadmin(principal): + return True + + return False + + def is_kadmin(self, principal): + name = principal['name-string'][0] + return name in ('kadmin', b'kadmin') + + def is_tgs(self, principal): + name_string = principal['name-string'] + if 1 <= len(name_string) <= 2: + return name_string[0] in ('krbtgt', b'krbtgt') + + return False + + def is_tgt(self, ticket): + sname = ticket.ticket['sname'] + return self.is_tgs(sname) + + def get_empty_pac(self): + return self.AuthorizationData_create(AD_WIN2K_PAC, bytes(1)) + + def get_outer_pa_dict(self, kdc_exchange_dict): + return self.get_pa_dict(kdc_exchange_dict['req_padata']) + + def get_fast_pa_dict(self, kdc_exchange_dict): + req_pa_dict = self.get_pa_dict(kdc_exchange_dict['fast_padata']) + + if req_pa_dict: + return req_pa_dict + + return self.get_outer_pa_dict(kdc_exchange_dict) + + def sent_fast(self, kdc_exchange_dict): + outer_pa_dict = self.get_outer_pa_dict(kdc_exchange_dict) + + return PADATA_FX_FAST in outer_pa_dict + + def sent_enc_challenge(self, kdc_exchange_dict): + fast_pa_dict = self.get_fast_pa_dict(kdc_exchange_dict) + + return PADATA_ENCRYPTED_CHALLENGE in fast_pa_dict + + def sent_enc_pa_rep(self, kdc_exchange_dict): + fast_pa_dict = self.get_fast_pa_dict(kdc_exchange_dict) + + return PADATA_REQ_ENC_PA_REP in fast_pa_dict + + def sent_pk_as_req(self, kdc_exchange_dict): + fast_pa_dict = self.get_fast_pa_dict(kdc_exchange_dict) + + return PADATA_PK_AS_REQ in fast_pa_dict + + def sent_pk_as_req_win2k(self, kdc_exchange_dict): + fast_pa_dict = self.get_fast_pa_dict(kdc_exchange_dict) + + return PADATA_PK_AS_REP_19 in fast_pa_dict + + def sent_freshness(self, kdc_exchange_dict): + fast_pa_dict = self.get_fast_pa_dict(kdc_exchange_dict) + + return PADATA_AS_FRESHNESS in fast_pa_dict + + def get_sent_pac_options(self, kdc_exchange_dict): + fast_pa_dict = self.get_fast_pa_dict(kdc_exchange_dict) + + if PADATA_PAC_OPTIONS not in fast_pa_dict: + return '' + + pac_options = self.der_decode(fast_pa_dict[PADATA_PAC_OPTIONS], + asn1Spec=krb5_asn1.PA_PAC_OPTIONS()) + pac_options = pac_options['options'] + + # Mask out unsupported bits. + pac_options, remaining = pac_options[:4], pac_options[4:] + pac_options += '0' * len(remaining) + + return pac_options + + def get_krbtgt_sname(self): + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_username = krbtgt_creds.get_username() + krbtgt_realm = krbtgt_creds.get_realm() + krbtgt_sname = self.PrincipalName_create( + name_type=NT_SRV_INST, names=[krbtgt_username, krbtgt_realm]) + + return krbtgt_sname + + def add_requester_sid(self, pac, sid): + pac_buffers = pac.buffers + + buffer_types = [pac_buffer.type for pac_buffer in pac_buffers] + self.assertNotIn(krb5pac.PAC_TYPE_REQUESTER_SID, buffer_types) + + requester_sid = krb5pac.PAC_REQUESTER_SID() + requester_sid.sid = security.dom_sid(sid) + + requester_sid_buffer = krb5pac.PAC_BUFFER() + requester_sid_buffer.type = krb5pac.PAC_TYPE_REQUESTER_SID + requester_sid_buffer.info = requester_sid + + pac_buffers.append(requester_sid_buffer) + + pac.buffers = pac_buffers + pac.num_buffers += 1 + + return pac + + def modify_lifetime(self, ticket, lifetime, requester_sid=None): + # Get the krbtgt key. + krbtgt_creds = self.get_krbtgt_creds() + + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + checksum_keys = { + krb5pac.PAC_TYPE_KDC_CHECKSUM: krbtgt_key, + } + + current_time = time.time() + + # Set authtime and starttime to an hour in the past, to show that they + # do not affect ticket rejection. + start_time = self.get_KerberosTime(epoch=current_time, offset=-60 * 60) + + # Set the endtime of the ticket relative to our current time, so that + # the ticket has 'lifetime' seconds remaining to live. + end_time = self.get_KerberosTime(epoch=current_time, offset=lifetime) + + # Modify the times in the ticket. + def modify_ticket_times(enc_part): + enc_part['authtime'] = start_time + if 'starttime' in enc_part: + enc_part['starttime'] = start_time + + enc_part['endtime'] = end_time + + return enc_part + + # We have to set the times in both the ticket and the PAC, otherwise + # Heimdal will complain. + def modify_pac_time(pac): + pac_buffers = pac.buffers + + for pac_buffer in pac_buffers: + if pac_buffer.type == krb5pac.PAC_TYPE_LOGON_NAME: + logon_time = self.get_EpochFromKerberosTime(start_time) + pac_buffer.info.logon_time = unix2nttime(logon_time) + break + else: + self.fail('failed to find LOGON_NAME PAC buffer') + + pac.buffers = pac_buffers + + return pac + + def modify_pac_fn(pac): + if requester_sid is not None: + # Add a requester SID to show that the KDC will then accept + # this kpasswd ticket as if it were a TGT. + pac = self.add_requester_sid(pac, sid=requester_sid) + pac = modify_pac_time(pac) + return pac + + # Do the actual modification. + return self.modified_ticket(ticket, + new_ticket_key=krbtgt_key, + modify_fn=modify_ticket_times, + modify_pac_fn=modify_pac_fn, + checksum_keys=checksum_keys) + + def _test_as_exchange(self, + cname, + realm, + sname, + till, + expected_error_mode, + expected_crealm, + expected_cname, + expected_srealm, + expected_sname, + expected_salt, + etypes, + padata, + kdc_options, + creds=None, + renew_time=None, + expected_account_name=None, + expected_groups=None, + unexpected_groups=None, + expected_upn_name=None, + expected_sid=None, + expected_domain_sid=None, + expected_flags=None, + unexpected_flags=None, + expected_supported_etypes=None, + preauth_key=None, + ticket_decryption_key=None, + pac_request=None, + pac_options=None, + expect_pac=True, + expect_pac_attrs=None, + expect_pac_attrs_pac_request=None, + expect_requester_sid=None, + expect_client_claims=None, + expect_device_claims=None, + expected_client_claims=None, + unexpected_client_claims=None, + expected_device_claims=None, + unexpected_device_claims=None, + expect_edata=None, + expect_status=None, + expected_status=None, + rc4_support=True, + to_rodc=False): + + def _generate_padata_copy(_kdc_exchange_dict, + _callback_dict, + req_body): + return padata, req_body + + if not expected_error_mode: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + else: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + + if padata is not None: + generate_padata_fn = _generate_padata_copy + else: + generate_padata_fn = None + + kdc_exchange_dict = self.as_exchange_dict( + creds=creds, + expected_crealm=expected_crealm, + expected_cname=expected_cname, + expected_srealm=expected_srealm, + expected_sname=expected_sname, + expected_account_name=expected_account_name, + expected_groups=expected_groups, + unexpected_groups=unexpected_groups, + expected_upn_name=expected_upn_name, + expected_sid=expected_sid, + expected_domain_sid=expected_domain_sid, + expected_supported_etypes=expected_supported_etypes, + ticket_decryption_key=ticket_decryption_key, + generate_padata_fn=generate_padata_fn, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error_mode, + expected_salt=expected_salt, + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + preauth_key=preauth_key, + kdc_options=str(kdc_options), + pac_request=pac_request, + pac_options=pac_options, + expect_pac=expect_pac, + expect_pac_attrs=expect_pac_attrs, + expect_pac_attrs_pac_request=expect_pac_attrs_pac_request, + expect_requester_sid=expect_requester_sid, + expect_client_claims=expect_client_claims, + expect_device_claims=expect_device_claims, + expected_client_claims=expected_client_claims, + unexpected_client_claims=unexpected_client_claims, + expected_device_claims=expected_device_claims, + unexpected_device_claims=unexpected_device_claims, + expect_edata=expect_edata, + expect_status=expect_status, + expected_status=expected_status, + rc4_support=rc4_support, + to_rodc=to_rodc) + + rep = self._generic_kdc_exchange(kdc_exchange_dict, + cname=cname, + realm=realm, + sname=sname, + till_time=till, + renew_time=renew_time, + etypes=etypes) + + return rep, kdc_exchange_dict diff --git a/python/samba/tests/krb5/rfc4120.asn1 b/python/samba/tests/krb5/rfc4120.asn1 new file mode 100644 index 0000000..79449d8 --- /dev/null +++ b/python/samba/tests/krb5/rfc4120.asn1 @@ -0,0 +1,1908 @@ +-- Portions of these ASN.1 modules are structures from RFC6113 +-- authored by S. Hartman (Painless Security) and L. Zhu (Microsoft) +-- +-- Portions of these ASN.1 modules are structures from RFC4556 +-- authored by L. Zhu (Microsoft Corporation) and B. Tung (Aerospace +-- Corporation) +-- +-- Portions of these ASN.1 modules are structures from RFC5280 +-- authored by D. Cooper (NIST), S. Santesson (Microsoft), +-- S. Farrell (Trinity College Dublin), S. Boeyen (Entrust), +-- R. Housley (Vigil Security), W. Polk (NIST) +-- +-- Portions of these ASN.1 modules are structures from RFC0817 +-- authored by K. Moriarty, Ed. (EMC Corporation) +-- B. Kaliski (Verisign), J. Jonsson (Subset AB), A. Rusch (RSA) + +-- Portions of these ASN.1 modules are structures from RFC0818 +-- authored by K. Moriarty, Ed. (Dell EMC), B. Kaliski (Verisign) +-- A. Rusch (RSA) +-- +-- Copyright (c) 2011 IETF Trust and the persons identified as authors of the +-- code. All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, is permitted pursuant to, and subject to the license terms +-- contained in, the Simplified BSD License set forth in Section 4.c of the IETF +-- Trust’s Legal Provisions Relating to IETF Documents +-- (http://trustee.ietf.org/license-info). +-- +-- BSD License: +-- +-- Copyright (c) 2011 IETF Trust and the persons identified as authors of the code. All rights reserved. +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided +-- that the following conditions are met: +-- • Redistributions of source code must retain the above copyright notice, this list of conditions and +-- the following disclaimer. +-- +-- • Redistributions in binary form must reproduce the above copyright notice, this list of conditions +-- and the following disclaimer in the documentation and/or other materials provided with the +-- distribution. +-- +-- • Neither the name of Internet Society, IETF or IETF Trust, nor the names of specific contributors, +-- may be used to endorse or promote products derived from this software without specific prior written +-- permission. +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” +-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +-- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +-- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +-- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +-- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +-- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +-- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +-- POSSIBILITY OF SUCH DAMAGE. +-- +-- +-- Portions of these ASN.1 modules are from Microsoft's MS-WCCE and MS-KILE +-- from the Microsoft Open Specifications Documentation. +-- +-- Intellectual Property Rights Notice for Open Specifications Documentation +-- +-- * Technical Documentation. Microsoft publishes Open Specifications +-- documentation (“this documentation”) for protocols, file formats, +-- data portability, computer languages, and standards +-- support. Additionally, overview documents cover inter-protocol +-- relationships and interactions. +-- +-- * Copyrights. This documentation is covered by Microsoft +-- copyrights. Regardless of any other terms that are contained in +-- the terms of use for the Microsoft website that hosts this +-- documentation, you can make copies of it in order to develop +-- implementations of the technologies that are described in this +-- documentation and can distribute portions of it in your +-- implementations that use these technologies or in your +-- documentation as necessary to properly document the +-- implementation. You can also distribute in your implementation, +-- with or without modification, any schemas, IDLs, or code samples +-- that are included in the documentation. This permission also +-- applies to any documents that are referenced in the Open +-- Specifications documentation. +-- +-- * No Trade Secrets. Microsoft does not claim any trade secret rights +-- in this documentation. +-- +-- * Patents. Microsoft has patents that might cover your +-- implementations of the technologies described in the Open +-- Specifications documentation. Neither this notice nor Microsoft's +-- delivery of this documentation grants any licenses under those +-- patents or any other Microsoft patents. However, a given Open +-- Specifications document might be covered by the Microsoft Open +-- Specifications Promise or the Microsoft Community Promise. If you +-- would prefer a written license, or if the technologies described +-- in this documentation are not covered by the Open Specifications +-- Promise or Community Promise, as applicable, patent licenses are +-- available by contacting iplg@microsoft.com. +-- +-- * License Programs. To see all of the protocols in scope under a +-- specific license program and the associated patents, visit the +-- Patent Map. +-- +-- * Trademarks. The names of companies and products contained in this +-- documentation might be covered by trademarks or similar +-- intellectual property rights. This notice does not grant any +-- licenses under those rights. For a list of Microsoft trademarks, +-- visit www.microsoft.com/trademarks. +-- +-- * Fictitious Names. The example companies, organizations, products, +-- domain names, email addresses, logos, people, places, and events +-- that are depicted in this documentation are fictitious. No +-- association with any real company, organization, product, domain +-- name, email address, logo, person, place, or event is intended or +-- should be inferred. +-- +-- Reservation of Rights. All other rights are reserved, and this notice +-- does not grant any rights other than as specifically described above, +-- whether by implication, estoppel, or otherwise. +-- +-- Tools. The Open Specifications documentation does not require the use +-- of Microsoft programming tools or programming environments in order +-- for you to develop an implementation. If you have access to Microsoft +-- programming tools and environments, you are free to take advantage of +-- them. Certain Open Specifications documents are intended for use in +-- conjunction with publicly available standards specifications and +-- network programming art and, as such, assume that the reader either +-- is familiar with the aforementioned material or has immediate access +-- to it. +-- +-- Support. For questions and support, please contact dochelp@microsoft.com + + +-- The above is the IPR notice from MS-KILE + +KerberosV5Spec2 { + iso(1) identified-organization(3) dod(6) internet(1) + security(5) kerberosV5(2) modules(4) krb5spec2(2) +} DEFINITIONS EXPLICIT TAGS ::= BEGIN + +-- OID arc for KerberosV5 +-- +-- This OID may be used to identify Kerberos protocol messages +-- encapsulated in other protocols. +-- +-- This OID also designates the OID arc for KerberosV5-related OIDs. +-- +-- NOTE: RFC 1510 had an incorrect value (5) for "dod" in its OID. +id-krb5 OBJECT IDENTIFIER ::= { + iso(1) identified-organization(3) dod(6) internet(1) + security(5) kerberosV5(2) +} + +Int32 ::= INTEGER (-2147483648..2147483647) + -- signed values representable in 32 bits + +UInt32 ::= INTEGER (0..4294967295) + -- unsigned 32 bit values + +Microseconds ::= INTEGER (0..999999) + -- microseconds + +-- +-- asn1ate doesn't support 'GeneralString (IA5String)' +-- only 'GeneralString' or 'IA5String', on the wire +-- GeneralString is used. +-- +-- KerberosString ::= GeneralString (IA5String) +KerberosString ::= GeneralString + +Realm ::= KerberosString + +PrincipalName ::= SEQUENCE { + name-type [0] NameType, -- Int32, + name-string [1] SEQUENCE OF KerberosString +} + +NameType ::= Int32 + +KerberosTime ::= GeneralizedTime -- with no fractional seconds + +HostAddress ::= SEQUENCE { + addr-type [0] Int32, + address [1] OCTET STRING +} + +-- NOTE: HostAddresses is always used as an OPTIONAL field and +-- should not be empty. +HostAddresses -- NOTE: subtly different from rfc1510, + -- but has a value mapping and encodes the same + ::= SEQUENCE OF HostAddress + +-- NOTE: AuthorizationData is always used as an OPTIONAL field and +-- should not be empty. +AuthorizationData ::= SEQUENCE OF SEQUENCE { + ad-type [0] AuthDataType, -- Int32, + ad-data [1] OCTET STRING +} + +AuthDataType ::= Int32 + +PA-DATA ::= SEQUENCE { + -- NOTE: first tag is [1], not [0] + padata-type [1] PADataType, -- Int32 + padata-value [2] OCTET STRING -- might be encoded AP-REQ +} + +PADataType ::= Int32 + +-- +-- asn1ate doesn't support 'MAX' nor a lower range != 1. +-- We'll use a custom enodeValue() hooks for BitString +-- in order to encode them with at least 32-Bit. +-- +-- KerberosFlags ::= BIT STRING (SIZE (32..MAX)) +KerberosFlags ::= BIT STRING (SIZE (1..32)) + -- minimum number of bits shall be sent, + -- but no fewer than 32 + +EncryptedData ::= SEQUENCE { + etype [0] EncryptionType, --Int32 EncryptionType -- + kvno [1] Int32 OPTIONAL, + cipher [2] OCTET STRING -- ciphertext +} + +EncryptionKey ::= SEQUENCE { + keytype [0] EncryptionType, -- Int32 actually encryption type -- + keyvalue [1] OCTET STRING +} + +Checksum ::= SEQUENCE { + cksumtype [0] ChecksumType, -- Int32, + checksum [1] OCTET STRING +} + +ChecksumType ::= Int32 + +Ticket ::= [APPLICATION 1] SEQUENCE { + tkt-vno [0] INTEGER (5), + realm [1] Realm, + sname [2] PrincipalName, + enc-part [3] EncryptedData -- EncTicketPart +} + +-- Encrypted part of ticket +EncTicketPart ::= [APPLICATION 3] SEQUENCE { + flags [0] TicketFlags, + key [1] EncryptionKey, + crealm [2] Realm, + cname [3] PrincipalName, + transited [4] TransitedEncoding, + authtime [5] KerberosTime, + starttime [6] KerberosTime OPTIONAL, + endtime [7] KerberosTime, + renew-till [8] KerberosTime OPTIONAL, + caddr [9] HostAddresses OPTIONAL, + authorization-data [10] AuthorizationData OPTIONAL +} + +-- encoded Transited field +TransitedEncoding ::= SEQUENCE { + tr-type [0] Int32 -- must be registered --, + contents [1] OCTET STRING +} + +TicketFlags ::= KerberosFlags + -- reserved(0), + -- forwardable(1), + -- forwarded(2), + -- proxiable(3), + -- proxy(4), + -- may-postdate(5), + -- postdated(6), + -- invalid(7), + -- renewable(8), + -- initial(9), + -- pre-authent(10), + -- hw-authent(11), +-- the following are new since 1510 + -- transited-policy-checked(12), + -- ok-as-delegate(13) + -- enc-pa-rep(15) + +AS-REQ ::= [APPLICATION 10] KDC-REQ + +TGS-REQ ::= [APPLICATION 12] KDC-REQ + +KDC-REQ ::= SEQUENCE { + -- NOTE: first tag is [1], not [0] + pvno [1] INTEGER (5) , + msg-type [2] INTEGER (10 -- AS -- | 12 -- TGS --), + padata [3] SEQUENCE OF PA-DATA OPTIONAL + -- NOTE: not empty --, + req-body [4] KDC-REQ-BODY +} + +KDC-REQ-BODY ::= SEQUENCE { + kdc-options [0] KDCOptions, + cname [1] PrincipalName OPTIONAL + -- Used only in AS-REQ --, + realm [2] Realm + -- Server's realm + -- Also client's in AS-REQ --, + sname [3] PrincipalName OPTIONAL, + from [4] KerberosTime OPTIONAL, + till [5] KerberosTime, + rtime [6] KerberosTime OPTIONAL, + nonce [7] UInt32, + etype [8] SEQUENCE OF EncryptionType -- Int32 - EncryptionType + -- in preference order --, + addresses [9] HostAddresses OPTIONAL, + enc-authorization-data [10] EncryptedData OPTIONAL + -- AuthorizationData --, + additional-tickets [11] SEQUENCE OF Ticket OPTIONAL + -- NOTE: not empty +} + +EncryptionType ::= Int32 + +KDCOptions ::= KerberosFlags + -- reserved(0), + -- forwardable(1), + -- forwarded(2), + -- proxiable(3), + -- proxy(4), + -- allow-postdate(5), + -- postdated(6), + -- unused7(7), + -- renewable(8), + -- unused9(9), + -- unused10(10), + -- opt-hardware-auth(11), + -- unused12(12), + -- unused13(13), +-- Canonicalize is used in RFC 6806 + -- canonicalize(15), +-- 26 was unused in 1510 + -- disable-transited-check(26), +-- + -- renewable-ok(27), + -- enc-tkt-in-skey(28), + -- renew(30), + -- validate(31) + +AS-REP ::= [APPLICATION 11] KDC-REP + +TGS-REP ::= [APPLICATION 13] KDC-REP + +KDC-REP ::= SEQUENCE { + pvno [0] INTEGER (5), + msg-type [1] INTEGER (11 -- AS -- | 13 -- TGS --), + padata [2] SEQUENCE OF PA-DATA OPTIONAL + -- NOTE: not empty --, + crealm [3] Realm, + cname [4] PrincipalName, + ticket [5] Ticket, + enc-part [6] EncryptedData + -- EncASRepPart or EncTGSRepPart, + -- as appropriate +} + +EncASRepPart ::= [APPLICATION 25] EncKDCRepPart + +EncTGSRepPart ::= [APPLICATION 26] EncKDCRepPart + +EncKDCRepPart ::= SEQUENCE { + key [0] EncryptionKey, + last-req [1] LastReq, + nonce [2] UInt32, + key-expiration [3] KerberosTime OPTIONAL, + flags [4] TicketFlags, + authtime [5] KerberosTime, + starttime [6] KerberosTime OPTIONAL, + endtime [7] KerberosTime, + renew-till [8] KerberosTime OPTIONAL, + srealm [9] Realm, + sname [10] PrincipalName, + caddr [11] HostAddresses OPTIONAL, + encrypted-pa-data[12] METHOD-DATA OPTIONAL +} + +LastReq ::= SEQUENCE OF SEQUENCE { + lr-type [0] Int32, + lr-value [1] KerberosTime +} + +AP-REQ ::= [APPLICATION 14] SEQUENCE { + pvno [0] INTEGER (5), + msg-type [1] INTEGER (14), + ap-options [2] APOptions, + ticket [3] Ticket, + authenticator [4] EncryptedData -- Authenticator +} + +APOptions ::= KerberosFlags + -- reserved(0), + -- use-session-key(1), + -- mutual-required(2) + +-- Unencrypted authenticator +Authenticator ::= [APPLICATION 2] SEQUENCE { + authenticator-vno [0] INTEGER (5), + crealm [1] Realm, + cname [2] PrincipalName, + cksum [3] Checksum OPTIONAL, + cusec [4] Microseconds, + ctime [5] KerberosTime, + subkey [6] EncryptionKey OPTIONAL, + seq-number [7] UInt32 OPTIONAL, + authorization-data [8] AuthorizationData OPTIONAL +} + +AP-REP ::= [APPLICATION 15] SEQUENCE { + pvno [0] INTEGER (5), + msg-type [1] INTEGER (15), + enc-part [2] EncryptedData -- EncAPRepPart +} + +EncAPRepPart ::= [APPLICATION 27] SEQUENCE { + ctime [0] KerberosTime, + cusec [1] Microseconds, + subkey [2] EncryptionKey OPTIONAL, + seq-number [3] UInt32 OPTIONAL +} + +KRB-SAFE ::= [APPLICATION 20] SEQUENCE { + pvno [0] INTEGER (5), + msg-type [1] INTEGER (20), + safe-body [2] KRB-SAFE-BODY, + cksum [3] Checksum +} + +KRB-SAFE-BODY ::= SEQUENCE { + user-data [0] OCTET STRING, + timestamp [1] KerberosTime OPTIONAL, + usec [2] Microseconds OPTIONAL, + seq-number [3] UInt32 OPTIONAL, + s-address [4] HostAddress, + r-address [5] HostAddress OPTIONAL +} + +KRB-PRIV ::= [APPLICATION 21] SEQUENCE { + pvno [0] INTEGER (5), + msg-type [1] INTEGER (21), + -- NOTE: there is no [2] tag + enc-part [3] EncryptedData -- EncKrbPrivPart +} + +EncKrbPrivPart ::= [APPLICATION 28] SEQUENCE { + user-data [0] OCTET STRING, + timestamp [1] KerberosTime OPTIONAL, + usec [2] Microseconds OPTIONAL, + seq-number [3] UInt32 OPTIONAL, + s-address [4] HostAddress -- sender's addr --, + r-address [5] HostAddress OPTIONAL -- recip's addr +} + +KRB-CRED ::= [APPLICATION 22] SEQUENCE { + pvno [0] INTEGER (5), + msg-type [1] INTEGER (22), + tickets [2] SEQUENCE OF Ticket, + enc-part [3] EncryptedData -- EncKrbCredPart +} + +EncKrbCredPart ::= [APPLICATION 29] SEQUENCE { + ticket-info [0] SEQUENCE OF KrbCredInfo, + nonce [1] UInt32 OPTIONAL, + timestamp [2] KerberosTime OPTIONAL, + usec [3] Microseconds OPTIONAL, + s-address [4] HostAddress OPTIONAL, + r-address [5] HostAddress OPTIONAL +} + +KrbCredInfo ::= SEQUENCE { + key [0] EncryptionKey, + prealm [1] Realm OPTIONAL, + pname [2] PrincipalName OPTIONAL, + flags [3] TicketFlags OPTIONAL, + authtime [4] KerberosTime OPTIONAL, + starttime [5] KerberosTime OPTIONAL, + endtime [6] KerberosTime OPTIONAL, + renew-till [7] KerberosTime OPTIONAL, + srealm [8] Realm OPTIONAL, + sname [9] PrincipalName OPTIONAL, + caddr [10] HostAddresses OPTIONAL +} + +KRB-ERROR ::= [APPLICATION 30] SEQUENCE { + pvno [0] INTEGER (5), + msg-type [1] INTEGER (30), + ctime [2] KerberosTime OPTIONAL, + cusec [3] Microseconds OPTIONAL, + stime [4] KerberosTime, + susec [5] Microseconds, + error-code [6] Int32, + crealm [7] Realm OPTIONAL, + cname [8] PrincipalName OPTIONAL, + realm [9] Realm -- service realm --, + sname [10] PrincipalName -- service name --, + e-text [11] KerberosString OPTIONAL, + e-data [12] OCTET STRING OPTIONAL +} + +METHOD-DATA ::= SEQUENCE OF PA-DATA + +-- +-- asn1ate doesn't support 'MAX' +-- +-- TYPED-DATA ::= SEQUENCE SIZE (1..MAX) OF SEQUENCE { +TYPED-DATA ::= SEQUENCE SIZE (1..256) OF SEQUENCE { + data-type [0] Int32, + data-value [1] OCTET STRING OPTIONAL +} + +-- preauth stuff follows + +PA-ENC-TIMESTAMP ::= EncryptedData -- PA-ENC-TS-ENC + +PA-ENC-TS-ENC ::= SEQUENCE { + patimestamp [0] KerberosTime -- client's time --, + pausec [1] Microseconds OPTIONAL +} + +ETYPE-INFO-ENTRY ::= SEQUENCE { + etype [0] EncryptionType, --Int32 EncryptionType -- + salt [1] OCTET STRING OPTIONAL +} + +ETYPE-INFO ::= SEQUENCE OF ETYPE-INFO-ENTRY + +ETYPE-INFO2-ENTRY ::= SEQUENCE { + etype [0] EncryptionType, --Int32 EncryptionType -- + salt [1] KerberosString OPTIONAL, + s2kparams [2] OCTET STRING OPTIONAL +} + +ETYPE-INFO2 ::= SEQUENCE SIZE (1..256) OF ETYPE-INFO2-ENTRY + +AD-IF-RELEVANT ::= AuthorizationData + +AD-KDCIssued ::= SEQUENCE { + ad-checksum [0] Checksum, + i-realm [1] Realm OPTIONAL, + i-sname [2] PrincipalName OPTIONAL, + elements [3] AuthorizationData +} + +AD-AND-OR ::= SEQUENCE { + condition-count [0] Int32, + elements [1] AuthorizationData +} + +AD-MANDATORY-FOR-KDC ::= AuthorizationData + +-- S4U + +PA-S4U2Self ::= SEQUENCE { + name [0] PrincipalName, + realm [1] Realm, + cksum [2] Checksum, + auth [3] KerberosString +} + +-- PK-INIT + +-- (from RFC 1422) + +-- asn1ate doesn’t support ‘SIGNED’. +-- CertificateRevocationList ::= SIGNED SEQUENCE { +CertificateRevocationList ::= SEQUENCE { + signature AlgorithmIdentifier, + issuer Name, + lastUpdate UTCTime, + nextUpdate UTCTime, + revokedCertificates + SEQUENCE OF CRLEntry OPTIONAL +} + +CRLEntry ::= SEQUENCE{ + userCertificate SerialNumber, + revocationDate UTCTime +} + +-- Not actually defined in an RFC. +SerialNumber ::= INTEGER + +-- (from RFC 2315) + +SignedData-RFC2315 ::= SEQUENCE { + version Version-RFC2315, + digestAlgorithms DigestAlgorithmIdentifiers, + contentInfo ContentInfo, + certificates [0] IMPLICIT CertificateSet OPTIONAL, + crls [1] IMPLICIT RevocationInfoChoices OPTIONAL, + signerInfos SignerInfos +} + +Version-RFC2315 ::= INTEGER + +ContentInfo ::= SEQUENCE { + contentType ContentType, + content + [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL +} + +ExtendedCertificatesAndCertificates ::= + SET OF ExtendedCertificateOrCertificate + +ExtendedCertificateOrCertificate ::= CHOICE { + certificate Certificate, -- X.509 + extendedCertificate [0] IMPLICIT ExtendedCertificate +} + +CertificateRevocationLists ::= + SET OF CertificateRevocationList + +-- (from RFC 3279) + +DomainParameters ::= SEQUENCE { + p INTEGER, -- odd prime, p=jq +1 + g INTEGER, -- generator, g + -- Note: RFC 3279 does not mention that ‘q’ is optional. + q INTEGER OPTIONAL, -- factor of p-1 + j INTEGER OPTIONAL, -- subgroup factor + validationParms ValidationParms OPTIONAL +} + +ValidationParms ::= SEQUENCE { + seed BIT STRING, + pgenCounter INTEGER +} + +DHPublicKey ::= INTEGER -- public key, y = g^x mod p + +dhpublicnumber OBJECT IDENTIFIER ::= { + iso(1) member-body(2) + us(840) ansi-x942(10046) number-type(2) 1 +} + +md2 OBJECT IDENTIFIER ::= { + iso(1) member-body(2) us(840) rsadsi(113549) + digestAlgorithm(2) 2 +} + +md5 OBJECT IDENTIFIER ::= { + iso(1) member-body(2) us(840) rsadsi(113549) + digestAlgorithm(2) 5 +} + +id-sha1 OBJECT IDENTIFIER ::= { + iso(1) identified-organization(3) oiw(14) secsig(3) + algorithms(2) 26 +} + +-- (from RFC 3281) + +AttributeCertificate ::= SEQUENCE { + acinfo AttributeCertificateInfo, + signatureAlgorithm AlgorithmIdentifier, + signatureValue BIT STRING +} + +AttributeCertificateInfo ::= SEQUENCE { + version AttCertVersion, -- version is v2 + holder Holder, + issuer AttCertIssuer, + signature AlgorithmIdentifier, + serialNumber CertificateSerialNumber, + attrCertValidityPeriod AttCertValidityPeriod, + attributes SEQUENCE OF Attribute, + issuerUniqueID UniqueIdentifier OPTIONAL, + extensions Extensions OPTIONAL +} + +AttCertVersion ::= INTEGER { v2(1) } + +Holder ::= SEQUENCE { + baseCertificateID [0] IssuerSerial OPTIONAL, + entityName [1] GeneralNames OPTIONAL, + objectDigestInfo [2] ObjectDigestInfo OPTIONAL +} + +ObjectDigestInfo ::= SEQUENCE { + digestedObjectType ENUMERATED { + publicKey (0), + publicKeyCert (1), + otherObjectTypes (2) + }, + -- otherObjectTypes MUST NOT + -- be used in this profile + otherObjectTypeID OBJECT IDENTIFIER OPTIONAL, + digestAlgorithm AlgorithmIdentifier, + objectDigest BIT STRING +} + +-- (from RFC 3370) + +sha1WithRSAEncryption OBJECT IDENTIFIER ::= { + iso(1) + member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-1(1) 5 +} + +-- (from RFC 4556) + +id-pkinit OBJECT IDENTIFIER ::= { + iso(1) identified-organization(3) dod(6) internet(1) + security(5) kerberosv5(2) pkinit (3) +} + +id-pkinit-authData OBJECT IDENTIFIER ::= { id-pkinit 1 } + +id-pkinit-DHKeyData OBJECT IDENTIFIER ::= { id-pkinit 2 } + +id-pkinit-rkeyData OBJECT IDENTIFIER ::= { id-pkinit 3 } + +PA-PK-AS-REQ ::= SEQUENCE { + signedAuthPack [0] IMPLICIT OCTET STRING, + trustedCertifiers [1] SEQUENCE OF + ExternalPrincipalIdentifier OPTIONAL, + kdcPkId [2] IMPLICIT OCTET STRING + OPTIONAL, + ... +} + +DHNonce ::= OCTET STRING + +ExternalPrincipalIdentifier ::= SEQUENCE { + subjectName [0] IMPLICIT OCTET STRING OPTIONAL, + issuerAndSerialNumber [1] IMPLICIT OCTET STRING OPTIONAL, + subjectKeyIdentifier [2] IMPLICIT OCTET STRING OPTIONAL, + ... +} + +AuthPack ::= SEQUENCE { + pkAuthenticator [0] PKAuthenticator, + clientPublicValue [1] SubjectPublicKeyInfo OPTIONAL, + supportedCMSTypes [2] SEQUENCE OF AlgorithmIdentifier + OPTIONAL, + clientDHNonce [3] DHNonce OPTIONAL, + ... +} + +PKAuthenticator ::= SEQUENCE { + cusec [0] INTEGER (0..999999), + ctime [1] KerberosTime, + nonce [2] INTEGER (0..4294967295), + paChecksum [3] OCTET STRING OPTIONAL, + freshnessToken [4] OCTET STRING OPTIONAL, + ... +} + +TD-TRUSTED-CERTIFIERS ::= SEQUENCE OF ExternalPrincipalIdentifier +TD-INVALID-CERTIFICATES ::= SEQUENCE OF ExternalPrincipalIdentifier + +KRB5PrincipalName ::= SEQUENCE { + realm [0] Realm, + principalName [1] PrincipalName +} + +AD-INITIAL-VERIFIED-CAS ::= SEQUENCE OF ExternalPrincipalIdentifier + +PA-PK-AS-REP ::= CHOICE { + dhInfo [0] DHRepInfo, + encKeyPack [1] IMPLICIT OCTET STRING, + ... +} + +DHRepInfo ::= SEQUENCE { + dhSignedData [0] IMPLICIT OCTET STRING, + serverDHNonce [1] DHNonce OPTIONAL, + ... +} + +KDCDHKeyInfo ::= SEQUENCE { + subjectPublicKey [0] BIT STRING, + nonce [1] INTEGER (0..4294967295), + dhKeyExpiration [2] KerberosTime OPTIONAL, + ... +} + +ReplyKeyPack ::= SEQUENCE { + replyKey [0] EncryptionKey, + asChecksum [1] Checksum, + ... +} + +TD-DH-PARAMETERS ::= SEQUENCE OF AlgorithmIdentifier + +-- (from RFC 5755) + +Attribute ::= SEQUENCE { + type AttributeType, + values SET OF AttributeValue + -- at least one value is required +} + +AttCertIssuer ::= CHOICE { + v1Form GeneralNames, -- MUST NOT be used in this + -- profile + v2Form [0] V2Form -- v2 only +} + +V2Form ::= SEQUENCE { + issuerName GeneralNames OPTIONAL, + baseCertificateID [0] IssuerSerial OPTIONAL, + objectDigestInfo [1] ObjectDigestInfo OPTIONAL + -- issuerName MUST be present in this profile + -- baseCertificateID and objectDigestInfo MUST NOT + -- be present in this profile +} + +IssuerSerial ::= SEQUENCE { + issuer GeneralNames, + serial CertificateSerialNumber, + issuerUID UniqueIdentifier OPTIONAL +} + +AttCertValidityPeriod ::= SEQUENCE { + notBeforeTime GeneralizedTime, + notAfterTime GeneralizedTime +} + +-- (from RFC 5280) + +id-ce OBJECT IDENTIFIER ::= { joint-iso-ccitt(2) ds(5) 29 } + +id-ce-subjectAltName OBJECT IDENTIFIER ::= { id-ce 17 } + +SubjectAltName ::= GeneralNames + +-- +-- asn1ate doesn’t support ‘MAX’. +-- +-- GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName +GeneralNames ::= SEQUENCE SIZE (1..256) OF GeneralName + +GeneralName ::= CHOICE { + otherName [0] OtherName, + rfc822Name [1] IA5String, + dNSName [2] IA5String, + x400Address [3] ORAddress, + directoryName [4] Name, + ediPartyName [5] EDIPartyName, + uniformResourceIdentifier [6] IA5String, + iPAddress [7] OCTET STRING, + registeredID [8] OBJECT IDENTIFIER +} + +OtherName ::= SEQUENCE { + type-id OBJECT IDENTIFIER, + value [0] EXPLICIT ANY DEFINED BY type-id +} + +EDIPartyName ::= SEQUENCE { + nameAssigner [0] DirectoryString OPTIONAL, + partyName [1] DirectoryString +} + +Name ::= CHOICE { -- only one possibility for now -- + rdnSequence RDNSequence +} + +DirectoryString ::= CHOICE { +-- +-- asn1ate doesn’t support ‘MAX’. +-- +-- teletexString TeletexString (SIZE (1..MAX)), +-- printableString PrintableString (SIZE (1..MAX)), +-- universalString UniversalString (SIZE (1..MAX)), +-- utf8String UTF8String (SIZE (1..MAX)), +-- bmpString BMPString (SIZE (1..MAX)) + teletexString TeletexString (SIZE (1..256)), + printableString PrintableString (SIZE (1..256)), + universalString UniversalString (SIZE (1..256)), + utf8String UTF8String (SIZE (1..256)), + bmpString BMPString (SIZE (1..256)) +} + +Certificate ::= SEQUENCE { + tbsCertificate TBSCertificate, + signatureAlgorithm AlgorithmIdentifier, + signatureValue BIT STRING +} + +TBSCertificate ::= SEQUENCE { +-- +-- asn1ate doesn’t support ‘v1’. +-- +-- version [0] EXPLICIT Version DEFAULT v1, + version [0] EXPLICIT Version DEFAULT 1, + serialNumber CertificateSerialNumber, + signature AlgorithmIdentifier, + issuer Name, + validity Validity, + subject Name, + subjectPublicKeyInfo SubjectPublicKeyInfo, + issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + -- If present, version MUST be v2 or v3 + subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + -- If present, version MUST be v2 or v3 + extensions [3] EXPLICIT Extensions OPTIONAL + -- If present, version MUST be v3 +} + +Version ::= INTEGER { v1(0), v2(1), v3(2) } + +CertificateSerialNumber ::= INTEGER + +Validity ::= SEQUENCE { + notBefore Time, + notAfter Time +} + +Time ::= CHOICE { + utcTime UTCTime, + generalTime GeneralizedTime +} + +UniqueIdentifier ::= BIT STRING + +AlgorithmIdentifier ::= SEQUENCE { + algorithm OBJECT IDENTIFIER, + parameters ANY DEFINED BY algorithm OPTIONAL +} + +SubjectPublicKeyInfo ::= SEQUENCE { + algorithm AlgorithmIdentifier, + subjectPublicKey BIT STRING +} + +RDNSequence ::= SEQUENCE OF RelativeDistinguishedName + +ORAddress ::= SEQUENCE { + built-in-standard-attributes BuiltInStandardAttributes, + built-in-domain-defined-attributes + BuiltInDomainDefinedAttributes OPTIONAL, + -- see also teletex-domain-defined-attributes + extension-attributes ExtensionAttributes OPTIONAL +} + +BuiltInStandardAttributes ::= SEQUENCE { + country-name CountryName OPTIONAL, + administration-domain-name AdministrationDomainName OPTIONAL, + network-address [0] IMPLICIT NetworkAddress OPTIONAL, + -- see also extended-network-address + terminal-identifier [1] IMPLICIT TerminalIdentifier OPTIONAL, + private-domain-name [2] PrivateDomainName OPTIONAL, + organization-name [3] IMPLICIT OrganizationName OPTIONAL, + -- see also teletex-organization-name + numeric-user-identifier [4] IMPLICIT NumericUserIdentifier + OPTIONAL, + personal-name [5] IMPLICIT PersonalName OPTIONAL, + -- see also teletex-personal-name + organizational-unit-names [6] IMPLICIT OrganizationalUnitNames + OPTIONAL + -- see also teletex-organizational-unit-names +} + +CountryName ::= [APPLICATION 1] CHOICE { + x121-dcc-code NumericString + (SIZE (ub-country-name-numeric-length)), + iso-3166-alpha2-code PrintableString + (SIZE (ub-country-name-alpha-length)) +} + +AdministrationDomainName ::= [APPLICATION 2] CHOICE { + numeric NumericString (SIZE (0..ub-domain-name-length)), + printable PrintableString (SIZE (0..ub-domain-name-length)) +} + +NetworkAddress ::= X121Address -- see also extended-network-address + +X121Address ::= NumericString (SIZE (1..ub-x121-address-length)) + +TerminalIdentifier ::= PrintableString (SIZE (1..ub-terminal-id-length)) + +PrivateDomainName ::= CHOICE { + numeric NumericString (SIZE (1..ub-domain-name-length)), + printable PrintableString (SIZE (1..ub-domain-name-length)) +} + +OrganizationName ::= PrintableString + (SIZE (1..ub-organization-name-length)) + -- see also teletex-organization-name + +NumericUserIdentifier ::= NumericString + (SIZE (1..ub-numeric-user-id-length)) + +PersonalName ::= SET { + surname [0] IMPLICIT PrintableString + (SIZE (1..ub-surname-length)), + given-name [1] IMPLICIT PrintableString + (SIZE (1..ub-given-name-length)) OPTIONAL, + initials [2] IMPLICIT PrintableString + (SIZE (1..ub-initials-length)) OPTIONAL, + generation-qualifier [3] IMPLICIT PrintableString + (SIZE (1..ub-generation-qualifier-length)) + OPTIONAL +} + -- see also teletex-personal-name + +OrganizationalUnitNames ::= SEQUENCE SIZE (1..ub-organizational-units) + OF OrganizationalUnitName + -- see also teletex-organizational-unit-names + +OrganizationalUnitName ::= PrintableString (SIZE + (1..ub-organizational-unit-name-length)) + +BuiltInDomainDefinedAttributes ::= SEQUENCE SIZE + (1..ub-domain-defined-attributes) OF + BuiltInDomainDefinedAttribute + +BuiltInDomainDefinedAttribute ::= SEQUENCE { + type PrintableString (SIZE + (1..ub-domain-defined-attribute-type-length)), + value PrintableString (SIZE + (1..ub-domain-defined-attribute-value-length)) +} + +ExtensionAttributes ::= SET SIZE (1..ub-extension-attributes) OF + ExtensionAttribute + +ExtensionAttribute ::= SEQUENCE { + extension-attribute-type [0] IMPLICIT INTEGER + (0..ub-extension-attributes), + extension-attribute-value [1] + ANY DEFINED BY extension-attribute-type +} + +-- +-- asn1ate doesn’t support ‘MAX’. +-- +-- Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension +Extensions ::= SEQUENCE SIZE (1..256) OF Extension + +Extension ::= SEQUENCE { + extnID OBJECT IDENTIFIER, + critical BOOLEAN DEFAULT FALSE, + extnValue OCTET STRING + -- contains the DER encoding of an ASN.1 value + -- corresponding to the extension type identified + -- by extnID +} + +CertificateList ::= SEQUENCE { + tbsCertList TBSCertList, + signatureAlgorithm AlgorithmIdentifier, + signatureValue BIT STRING +} + +TBSCertList ::= SEQUENCE { + version Version OPTIONAL, + -- if present, MUST be v2 + signature AlgorithmIdentifier, + issuer Name, + thisUpdate Time, + nextUpdate Time OPTIONAL, + revokedCertificates SEQUENCE OF SEQUENCE { + userCertificate CertificateSerialNumber, + revocationDate Time, + crlEntryExtensions Extensions OPTIONAL + -- if present, version MUST be v2 + } OPTIONAL, + crlExtensions [0] EXPLICIT Extensions OPTIONAL + -- if present, version MUST be v2 +} + +ub-name INTEGER ::= 32768 +ub-common-name INTEGER ::= 64 +ub-locality-name INTEGER ::= 128 +ub-state-name INTEGER ::= 128 +ub-organization-name INTEGER ::= 64 +ub-organizational-unit-name INTEGER ::= 64 +ub-title INTEGER ::= 64 +ub-serial-number INTEGER ::= 64 +ub-match INTEGER ::= 128 +ub-emailaddress-length INTEGER ::= 255 +ub-common-name-length INTEGER ::= 64 +ub-country-name-alpha-length INTEGER ::= 2 +ub-country-name-numeric-length INTEGER ::= 3 +ub-domain-defined-attributes INTEGER ::= 4 +ub-domain-defined-attribute-type-length INTEGER ::= 8 +ub-domain-defined-attribute-value-length INTEGER ::= 128 +ub-domain-name-length INTEGER ::= 16 +ub-extension-attributes INTEGER ::= 256 +ub-e163-4-number-length INTEGER ::= 15 +ub-e163-4-sub-address-length INTEGER ::= 40 +ub-generation-qualifier-length INTEGER ::= 3 +ub-given-name-length INTEGER ::= 16 +ub-initials-length INTEGER ::= 5 +ub-integer-options INTEGER ::= 256 +ub-numeric-user-id-length INTEGER ::= 32 +ub-organization-name-length INTEGER ::= 64 +ub-organizational-unit-name-length INTEGER ::= 32 +ub-organizational-units INTEGER ::= 4 +ub-pds-name-length INTEGER ::= 16 +ub-pds-parameter-length INTEGER ::= 30 +ub-pds-physical-address-lines INTEGER ::= 6 +ub-postal-code-length INTEGER ::= 16 +ub-pseudonym INTEGER ::= 128 +ub-surname-length INTEGER ::= 40 +ub-terminal-id-length INTEGER ::= 24 +ub-unformatted-address-length INTEGER ::= 180 +ub-x121-address-length INTEGER ::= 16 + +-- +-- asn1ate doesn’t support ‘MAX’. +-- +-- RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue +RelativeDistinguishedName ::= SET SIZE (1..256) OF AttributeTypeAndValue + +AttributeTypeAndValue ::= SEQUENCE { + type AttributeType, + value AttributeValue +} + +AttributeType ::= OBJECT IDENTIFIER + +AttributeValue ::= ANY -- DEFINED BY AttributeType + +-- (from RFC 5652) + +ContentType ::= OBJECT IDENTIFIER + +RevocationInfoChoices ::= SET OF RevocationInfoChoice + +RevocationInfoChoice ::= CHOICE { + crl CertificateList, + other [1] IMPLICIT OtherRevocationInfoFormat +} + +OtherRevocationInfoFormat ::= SEQUENCE { + otherRevInfoFormat OBJECT IDENTIFIER, + otherRevInfo ANY DEFINED BY otherRevInfoFormat +} + +AttributeCertificateV1 ::= SEQUENCE { + acInfo AttributeCertificateInfoV1, + signatureAlgorithm AlgorithmIdentifier, + signature BIT STRING +} + +AttributeCertificateInfoV1 ::= SEQUENCE { +-- +-- asn1ate doesn’t support ‘v1’. +-- +-- version AttCertVersionV1 DEFAULT v1, + version AttCertVersionV1 DEFAULT 1, + subject CHOICE { + baseCertificateID [0] IssuerSerial, + -- associated with a Public Key Certificate + subjectName [1] GeneralNames }, + -- associated with a name + issuer GeneralNames, + signature AlgorithmIdentifier, + serialNumber CertificateSerialNumber, + attCertValidityPeriod AttCertValidityPeriod, + attributes SEQUENCE OF Attribute, + issuerUniqueID UniqueIdentifier OPTIONAL, + extensions Extensions OPTIONAL +} + +AttCertVersionV1 ::= INTEGER { v1(0) } + +ExtendedCertificate ::= SEQUENCE { + extendedCertificateInfo ExtendedCertificateInfo, + signatureAlgorithm SignatureAlgorithmIdentifier, + signature Signature +} + +ExtendedCertificateInfo ::= SEQUENCE { + version CMSVersion, + certificate Certificate, + attributes UnauthAttributes +} + +CertificateChoices ::= CHOICE { + certificate Certificate, + extendedCertificate [0] IMPLICIT ExtendedCertificate, -- Obsolete + v1AttrCert [1] IMPLICIT AttributeCertificateV1, -- Obsolete + v2AttrCert [2] IMPLICIT AttributeCertificateV2, + other [3] IMPLICIT OtherCertificateFormat +} + +AttributeCertificateV2 ::= AttributeCertificate + +OtherCertificateFormat ::= SEQUENCE { + otherCertFormat OBJECT IDENTIFIER, + otherCert ANY DEFINED BY otherCertFormat +} + +CertificateSet ::= SET OF CertificateChoices + +IssuerAndSerialNumber ::= SEQUENCE { + issuer Name, + serialNumber CertificateSerialNumber +} + +CMSVersion ::= INTEGER { v0(0), v1(1), v2(2), v3(3), v4(4), v5(5) } + +SignerInfo ::= SEQUENCE { + version CMSVersion, + sid SignerIdentifier, + digestAlgorithm DigestAlgorithmIdentifier, + signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL, + signatureAlgorithm SignatureAlgorithmIdentifier, + signature SignatureValue, + unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL +} + +SignerIdentifier ::= CHOICE { + issuerAndSerialNumber IssuerAndSerialNumber, + subjectKeyIdentifier [0] SubjectKeyIdentifier +} + +SubjectKeyIdentifier ::= OCTET STRING + +-- +-- asn1ate doesn’t support ‘MAX’. +-- +-- SignedAttributes ::= SET SIZE (1..MAX) OF Attribute +SignedAttributes ::= SET SIZE (1..256) OF Attribute + +-- +-- asn1ate doesn’t support ‘MAX’. +-- +-- UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute +UnsignedAttributes ::= SET SIZE (1..256) OF Attribute + +SignatureValue ::= OCTET STRING + +SignedData ::= SEQUENCE { + version CMSVersion, + digestAlgorithms DigestAlgorithmIdentifiers, + encapContentInfo EncapsulatedContentInfo, + certificates [0] IMPLICIT CertificateSet OPTIONAL, + crls [1] IMPLICIT RevocationInfoChoices OPTIONAL, + signerInfos SignerInfos +} + +DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier + +SignerInfos ::= SET OF SignerInfo + +EncapsulatedContentInfo ::= SEQUENCE { + eContentType ContentType, + eContent [0] EXPLICIT OCTET STRING OPTIONAL +} + +EnvelopedData ::= SEQUENCE { + version CMSVersion, + originatorInfo [0] IMPLICIT OriginatorInfo OPTIONAL, + recipientInfos RecipientInfos, + encryptedContentInfo EncryptedContentInfo, + unprotectedAttrs [1] IMPLICIT UnprotectedAttributes OPTIONAL +} + +OriginatorInfo ::= SEQUENCE { + certs [0] IMPLICIT CertificateSet OPTIONAL, + crls [1] IMPLICIT RevocationInfoChoices OPTIONAL +} + +-- +-- asn1ate doesn't support 'MAX' +-- +-- RecipientInfos ::= SET SIZE (1..MAX) OF RecipientInfo +RecipientInfos ::= SET SIZE (1..256) OF RecipientInfo + +EncryptedContentInfo ::= SEQUENCE { + contentType ContentType, + contentEncryptionAlgorithm ContentEncryptionAlgorithmIdentifier, + encryptedContent [0] IMPLICIT EncryptedContent OPTIONAL +} + +EncryptedContent ::= OCTET STRING + +-- +-- asn1ate doesn't support 'MAX' +-- +-- UnprotectedAttributes ::= SET SIZE (1..MAX) OF Attribute +UnprotectedAttributes ::= SET SIZE (1..256) OF Attribute + +RecipientInfo ::= CHOICE { + ktri KeyTransRecipientInfo, + kari [1] KeyAgreeRecipientInfo, + kekri [2] KEKRecipientInfo, + pwri [3] PasswordRecipientInfo, + ori [4] OtherRecipientInfo +} + +EncryptedKey ::= OCTET STRING + +KeyTransRecipientInfo ::= SEQUENCE { + version CMSVersion, -- always set to 0 or 2 + rid RecipientIdentifier, + keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + encryptedKey EncryptedKey +} + +RecipientIdentifier ::= CHOICE { + issuerAndSerialNumber IssuerAndSerialNumber, + subjectKeyIdentifier [0] SubjectKeyIdentifier +} + +KeyAgreeRecipientInfo ::= SEQUENCE { + version CMSVersion, -- always set to 3 + originator [0] EXPLICIT OriginatorIdentifierOrKey, + ukm [1] EXPLICIT UserKeyingMaterial OPTIONAL, + keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + recipientEncryptedKeys RecipientEncryptedKeys +} + +OriginatorIdentifierOrKey ::= CHOICE { + issuerAndSerialNumber IssuerAndSerialNumber, + subjectKeyIdentifier [0] SubjectKeyIdentifier, + originatorKey [1] OriginatorPublicKey +} + +OriginatorPublicKey ::= SEQUENCE { + algorithm AlgorithmIdentifier, + publicKey BIT STRING +} + +RecipientEncryptedKeys ::= SEQUENCE OF RecipientEncryptedKey + +RecipientEncryptedKey ::= SEQUENCE { + rid KeyAgreeRecipientIdentifier, + encryptedKey EncryptedKey +} + +KeyAgreeRecipientIdentifier ::= CHOICE { + issuerAndSerialNumber IssuerAndSerialNumber, + rKeyId [0] IMPLICIT RecipientKeyIdentifier +} + +RecipientKeyIdentifier ::= SEQUENCE { + subjectKeyIdentifier SubjectKeyIdentifier, + date GeneralizedTime OPTIONAL, + other OtherKeyAttribute OPTIONAL +} + +KEKRecipientInfo ::= SEQUENCE { + version CMSVersion, -- always set to 4 + kekid KEKIdentifier, + keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + encryptedKey EncryptedKey +} + +KEKIdentifier ::= SEQUENCE { + keyIdentifier OCTET STRING, + date GeneralizedTime OPTIONAL, + other OtherKeyAttribute OPTIONAL +} + +PasswordRecipientInfo ::= SEQUENCE { + version CMSVersion, -- always set to 0 + keyDerivationAlgorithm [0] KeyDerivationAlgorithmIdentifier + OPTIONAL, + keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + encryptedKey EncryptedKey +} + +OtherRecipientInfo ::= SEQUENCE { + oriType OBJECT IDENTIFIER, + oriValue ANY DEFINED BY oriType +} + +UserKeyingMaterial ::= OCTET STRING + +OtherKeyAttribute ::= SEQUENCE { + keyAttrId OBJECT IDENTIFIER, + keyAttr ANY DEFINED BY keyAttrId OPTIONAL +} + +MessageDigest ::= OCTET STRING + +id-data OBJECT IDENTIFIER ::= { + iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs7(7) 1 +} + +id-signedData OBJECT IDENTIFIER ::= { + iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs7(7) 2 +} + +id-envelopedData OBJECT IDENTIFIER ::= { + iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs7(7) 3 +} + +id-contentType OBJECT IDENTIFIER ::= { + iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs9(9) 3 +} + +id-messageDigest OBJECT IDENTIFIER ::= { + iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs9(9) 4 +} + +-- +-- asn1ate doesn’t support ‘MAX’. +-- +-- UnauthAttributes ::= SET SIZE (1..MAX) OF Attribute +UnauthAttributes ::= SET SIZE (1..256) OF Attribute + +DigestAlgorithmIdentifier ::= AlgorithmIdentifier + +SignatureAlgorithmIdentifier ::= AlgorithmIdentifier + +KeyEncryptionAlgorithmIdentifier ::= AlgorithmIdentifier + +KeyDerivationAlgorithmIdentifier ::= AlgorithmIdentifier + +ContentEncryptionAlgorithmIdentifier ::= AlgorithmIdentifier + +Signature ::= BIT STRING + +-- Other PK-INIT definitions + +id-pkcs1-sha256WithRSAEncryption OBJECT IDENTIFIER ::= { + iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) + label-less(1) label-less(11) +} + +MS-UPN-SAN ::= UTF8String + +CMSCBCParameter ::= OCTET STRING + +-- (from MS-WCCE) + +szOID-NTDS-CA-SECURITY-EXT OBJECT IDENTIFIER ::= { + iso(1) org(3) dod(6) internet(1) private(4) enterprise(1) + microsoft(311) directory-service(25) 2 +} + +szOID-NTDS-OBJECTSID OBJECT IDENTIFIER ::= { + iso(1) org(3) dod(6) internet(1) private(4) enterprise(1) + microsoft(311) directory-service(25) 2 1 +} + +-- (from RFC 8017) + +rsaEncryption OBJECT IDENTIFIER ::= { + iso(1) + member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-1(1) 1 +} + +id-sha512 OBJECT IDENTIFIER ::= { + joint-iso-itu-t (2) country (16) us (840) organization (1) + gov (101) csor (3) nistalgorithm (4) hashalgs (2) 3 +} + +-- (from RFC 8018) + +nistAlgorithms OBJECT IDENTIFIER ::= {joint-iso-itu-t(2) country(16) + us(840) organization(1) + gov(101) csor(3) 4} + +aes OBJECT IDENTIFIER ::= { nistAlgorithms 1 } + +aes256-CBC-PAD OBJECT IDENTIFIER ::= { aes 42 } + +rsadsi OBJECT IDENTIFIER ::= {iso(1) member-body(2) us(840) 113549} + +encryptionAlgorithm OBJECT IDENTIFIER ::= {rsadsi 3} + +des-EDE3-CBC OBJECT IDENTIFIER ::= {encryptionAlgorithm 7} + +-- Windows 2000 PK-INIT definitions + +PKAuthenticator-Win2k ::= SEQUENCE { + kdcName [0] PrincipalName, + kdcRealm [1] Realm, + cusec [2] INTEGER (0..4294967295), + ctime [3] KerberosTime, + nonce [4] INTEGER (-2147483648..2147483647) +} + +AuthPack-Win2k ::= SEQUENCE { + pkAuthenticator [0] PKAuthenticator-Win2k +} + +TrustedCA-Win2k ::= CHOICE { + caName [1] ANY, + issuerAndSerial [2] IssuerAndSerialNumber +} + +PA-PK-AS-REQ-Win2k ::= SEQUENCE { + signedAuthPack [0] IMPLICIT OCTET STRING, + trustedCertifiers [2] SEQUENCE OF TrustedCA-Win2k OPTIONAL, + kdcCert [3] IMPLICIT OCTET STRING OPTIONAL, + encryptionCert [4] IMPLICIT OCTET STRING OPTIONAL, + ... +} + +PA-PK-AS-REP-Win2k ::= CHOICE { + dhSignedData [0] IMPLICIT OCTET STRING, + encKeyPack [1] IMPLICIT OCTET STRING +} + +ReplyKeyPack-Win2k ::= SEQUENCE { + replyKey [0] EncryptionKey, + nonce [1] INTEGER (-2147483648..2147483647), + ... +} + +-- + +id-pkinit-ms-san OBJECT IDENTIFIER ::= { + iso(1) org(3) dod(6) internet(1) private(4) enterprise(1) + microsoft(311) 20 2 3 +} + +kdc-authentication OBJECT IDENTIFIER ::= { id-pkinit keyPurposeKdc(5) } + +smartcard-logon OBJECT IDENTIFIER ::= { + iso(1) org(3) dod(6) internet(1) private(4) enterprise(1) + microsoft(311) 20 2 2 +} + +CMSAttributes ::= SET OF Attribute + +-- +-- +-- MS-KILE Start + +KERB-ERROR-DATA ::= SEQUENCE { + data-type [1] KerbErrorDataType, + data-value [2] OCTET STRING OPTIONAL +} + +KerbErrorDataType ::= INTEGER + +KERB-PA-PAC-REQUEST ::= SEQUENCE { + include-pac[0] BOOLEAN --If TRUE, and no pac present, include PAC. + --If FALSE, and PAC present, remove PAC +} + +KERB-LOCAL ::= OCTET STRING -- Implementation-specific data which MUST be + -- ignored if Kerberos client is not local. + +KERB-AD-RESTRICTION-ENTRY ::= SEQUENCE { + restriction-type [0] Int32, + restriction [1] OCTET STRING -- LSAP_TOKEN_INFO_INTEGRITY structure +} + +PA-SUPPORTED-ENCTYPES ::= Int32 -- Supported Encryption Types Bit Field -- + +PACOptionFlags ::= KerberosFlags -- Claims (0) + -- Branch Aware (1) + -- Forward to Full DC (2) + -- Resource Based Constrained Delegation (3) +PA-PAC-OPTIONS ::= SEQUENCE { + options [0] PACOptionFlags +} +-- Note: KerberosFlags ::= BIT STRING (SIZE (32..MAX)) +-- minimum number of bits shall be sent, but no fewer than 32 + +KERB-KEY-LIST-REQ ::= SEQUENCE OF EncryptionType -- Int32 encryption type -- +KERB-KEY-LIST-REP ::= SEQUENCE OF EncryptionKey + +FastOptions ::= BIT STRING { + reserved(0), + hide-client-names(1), + kdc-follow-referrals(16) +} + +KrbFastReq ::= SEQUENCE { + fast-options [0] FastOptions, + padata [1] SEQUENCE OF PA-DATA, + req-body [2] KDC-REQ-BODY, + ... +} + +KrbFastArmor ::= SEQUENCE { + armor-type [0] Int32, + armor-value [1] OCTET STRING, + ... +} + +KrbFastArmoredReq ::= SEQUENCE { + armor [0] KrbFastArmor OPTIONAL, + req-checksum [1] Checksum, + enc-fast-req [2] EncryptedData -- KrbFastReq -- +} + +PA-FX-FAST-REQUEST ::= CHOICE { + armored-data [0] KrbFastArmoredReq, + ... +} + +KrbFastFinished ::= SEQUENCE { + timestamp [0] KerberosTime, + usec [1] Int32, + crealm [2] Realm, + cname [3] PrincipalName, + ticket-checksum [4] Checksum, + ... +} + +KrbFastResponse ::= SEQUENCE { + padata [0] SEQUENCE OF PA-DATA, + -- padata typed holes. + strengthen-key [1] EncryptionKey OPTIONAL, + -- This, if present, strengthens the reply key for AS and + -- TGS. MUST be present for TGS. + -- MUST be absent in KRB-ERROR. + finished [2] KrbFastFinished OPTIONAL, + -- Present in AS or TGS reply; absent otherwise. + nonce [3] UInt32, + -- Nonce from the client request. + ... +} + +KrbFastArmoredRep ::= SEQUENCE { + enc-fast-rep [0] EncryptedData, -- KrbFastResponse -- + ... +} + +PA-FX-FAST-REPLY ::= CHOICE { + armored-data [0] KrbFastArmoredRep, + ... +} + +ChangePasswdDataMS ::= SEQUENCE { + newpasswd [0] OCTET STRING, + targname [1] PrincipalName OPTIONAL, + targrealm [2] Realm OPTIONAL +} + +-- MS-KILE End +-- +-- + +-- +-- +-- prettyPrint values +-- +-- + +NameTypeValues ::= INTEGER { -- Int32 + kRB5-NT-UNKNOWN(0), -- Name type not known + kRB5-NT-PRINCIPAL(1), -- Just the name of the principal as in + kRB5-NT-SRV-INST(2), -- Service and other unique instance (krbtgt) + kRB5-NT-SRV-HST(3), -- Service with host name as instance + kRB5-NT-SRV-XHST(4), -- Service with host as remaining components + kRB5-NT-UID(5), -- Unique ID + kRB5-NT-X500-PRINCIPAL(6), -- PKINIT + kRB5-NT-SMTP-NAME(7), -- Name in form of SMTP email name + kRB5-NT-ENTERPRISE-PRINCIPAL(10), -- Windows 2000 UPN + kRB5-NT-WELLKNOWN(11), -- Wellknown + kRB5-NT-ENT-PRINCIPAL-AND-ID(-130), -- Windows 2000 UPN and SID + kRB5-NT-MS-PRINCIPAL(-128), -- NT 4 style name + kRB5-NT-MS-PRINCIPAL-AND-ID(-129) -- NT style name and SID +} +NameTypeSequence ::= SEQUENCE { + dummy [0] NameTypeValues +} + +TicketFlagsValues ::= BIT STRING { -- KerberosFlags + reserved(0), + forwardable(1), + forwarded(2), + proxiable(3), + proxy(4), + may-postdate(5), + postdated(6), + invalid(7), + renewable(8), + initial(9), + pre-authent(10), + hw-authent(11), +-- the following are new since 1510 + transited-policy-checked(12), + ok-as-delegate(13), + enc-pa-rep(15) +} +TicketFlagsSequence ::= SEQUENCE { + dummy [0] TicketFlagsValues +} + +KDCOptionsValues ::= BIT STRING { -- KerberosFlags + reserved(0), + forwardable(1), + forwarded(2), + proxiable(3), + proxy(4), + allow-postdate(5), + postdated(6), + unused7(7), + renewable(8), + unused9(9), + unused10(10), + opt-hardware-auth(11), + unused12(12), + unused13(13), + cname-in-addl-tkt(14), +-- Canonicalize is used by RFC 6806 + canonicalize(15), +-- 26 was unused in 1510 + disable-transited-check(26), +-- + renewable-ok(27), + enc-tkt-in-skey(28), + renew(30), + validate(31) +} +KDCOptionsSequence ::= SEQUENCE { + dummy [0] KDCOptionsValues +} + +APOptionsValues ::= BIT STRING { -- KerberosFlags + reserved(0), + use-session-key(1), + mutual-required(2) +} +APOptionsSequence ::= SEQUENCE { + dummy [0] APOptionsValues +} + +MessageTypeValues ::= INTEGER { + krb-as-req(10), -- Request for initial authentication + krb-as-rep(11), -- Response to KRB_AS_REQ request + krb-tgs-req(12), -- Request for authentication based on TGT + krb-tgs-rep(13), -- Response to KRB_TGS_REQ request + krb-ap-req(14), -- application request to server + krb-ap-rep(15), -- Response to KRB_AP_REQ_MUTUAL + krb-safe(20), -- Safe (checksummed) application message + krb-priv(21), -- Private (encrypted) application message + krb-cred(22), -- Private (encrypted) message to forward credentials + krb-error(30) -- Error response +} +MessageTypeSequence ::= SEQUENCE { + dummy [0] MessageTypeValues +} + +PADataTypeValues ::= INTEGER { + kRB5-PADATA-NONE(0), + -- kRB5-PADATA-TGS-REQ(1), + -- kRB5-PADATA-AP-REQ(1), + kRB5-PADATA-KDC-REQ(1), + kRB5-PADATA-ENC-TIMESTAMP(2), + kRB5-PADATA-PW-SALT(3), + kRB5-PADATA-ENC-UNIX-TIME(5), + kRB5-PADATA-SANDIA-SECUREID(6), + kRB5-PADATA-SESAME(7), + kRB5-PADATA-OSF-DCE(8), + kRB5-PADATA-CYBERSAFE-SECUREID(9), + kRB5-PADATA-AFS3-SALT(10), + kRB5-PADATA-ETYPE-INFO(11), + kRB5-PADATA-SAM-CHALLENGE(12), -- (sam/otp) + kRB5-PADATA-SAM-RESPONSE(13), -- (sam/otp) + kRB5-PADATA-PK-AS-REQ-19(14), -- (PKINIT-19) + kRB5-PADATA-PK-AS-REP-19(15), -- (PKINIT-19) + -- kRB5-PADATA-PK-AS-REQ-WIN(15), - (PKINIT - old number) + kRB5-PADATA-PK-AS-REQ(16), -- (PKINIT-25) + kRB5-PADATA-PK-AS-REP(17), -- (PKINIT-25) + kRB5-PADATA-PA-PK-OCSP-RESPONSE(18), + kRB5-PADATA-ETYPE-INFO2(19), + -- kRB5-PADATA-USE-SPECIFIED-KVNO(20), + kRB5-PADATA-SVR-REFERRAL-INFO(20), --- old ms referral number + kRB5-PADATA-SAM-REDIRECT(21), -- (sam/otp) + kRB5-PADATA-GET-FROM-TYPED-DATA(22), + kRB5-PADATA-SAM-ETYPE-INFO(23), + kRB5-PADATA-SERVER-REFERRAL(25), + kRB5-PADATA-ALT-PRINC(24), -- (crawdad@fnal.gov) + kRB5-PADATA-SAM-CHALLENGE2(30), -- (kenh@pobox.com) + kRB5-PADATA-SAM-RESPONSE2(31), -- (kenh@pobox.com) + kRB5-PA-EXTRA-TGT(41), -- Reserved extra TGT + kRB5-PADATA-TD-KRB-PRINCIPAL(102), -- PrincipalName + kRB5-PADATA-PK-TD-TRUSTED-CERTIFIERS(104), -- PKINIT + kRB5-PADATA-PK-TD-CERTIFICATE-INDEX(105), -- PKINIT + kRB5-PADATA-TD-APP-DEFINED-ERROR(106), -- application specific + kRB5-PADATA-TD-REQ-NONCE(107), -- INTEGER + kRB5-PADATA-TD-REQ-SEQ(108), -- INTEGER + kRB5-PADATA-PA-PAC-REQUEST(128), -- jbrezak@exchange.microsoft.com + kRB5-PADATA-FOR-USER(129), -- MS-KILE + kRB5-PADATA-FOR-X509-USER(130), -- MS-KILE + kRB5-PADATA-FOR-CHECK-DUPS(131), -- MS-KILE + kRB5-PADATA-AS-CHECKSUM(132), -- MS-KILE + -- kRB5-PADATA-PK-AS-09-BINDING(132), - client send this to + -- tell KDC that is supports + -- the asCheckSum in the + -- PK-AS-REP + kRB5-PADATA-FX-COOKIE(133), -- krb-wg-preauth-framework + kRB5-PADATA-AUTHENTICATION-SET(134), -- krb-wg-preauth-framework + kRB5-PADATA-AUTH-SET-SELECTED(135), -- krb-wg-preauth-framework + kRB5-PADATA-FX-FAST(136), -- krb-wg-preauth-framework + kRB5-PADATA-FX-ERROR(137), -- krb-wg-preauth-framework + kRB5-PADATA-ENCRYPTED-CHALLENGE(138), -- krb-wg-preauth-framework + kRB5-PADATA-OTP-CHALLENGE(141), -- (gareth.richards@rsa.com) + kRB5-PADATA-OTP-REQUEST(142), -- (gareth.richards@rsa.com) + kBB5-PADATA-OTP-CONFIRM(143), -- (gareth.richards@rsa.com) + kRB5-PADATA-OTP-PIN-CHANGE(144), -- (gareth.richards@rsa.com) + kRB5-PADATA-EPAK-AS-REQ(145), + kRB5-PADATA-EPAK-AS-REP(146), + kRB5-PADATA-PKINIT-KX(147), -- krb-wg-anon + kRB5-PADATA-PKU2U-NAME(148), -- zhu-pku2u + kRB5-PADATA-REQ-ENC-PA-REP(149), -- + kRB5-PADATA-AS-FRESHNESS(150), -- RFC 8070 + kRB5-PADATA-SUPPORTED-ETYPES(165), -- MS-KILE + kRB5-PADATA-PAC-OPTIONS(167), -- MS-KILE + kRB5-PADATA-GSS(655) -- gss-preauth +} +PADataTypeSequence ::= SEQUENCE { + dummy [0] PADataTypeValues +} + +AuthDataTypeValues ::= INTEGER { + kRB5-AUTHDATA-IF-RELEVANT(1), + kRB5-AUTHDATA-INTENDED-FOR-SERVER(2), + kRB5-AUTHDATA-INTENDED-FOR-APPLICATION-CLASS(3), + kRB5-AUTHDATA-KDC-ISSUED(4), + kRB5-AUTHDATA-AND-OR(5), + kRB5-AUTHDATA-MANDATORY-TICKET-EXTENSIONS(6), + kRB5-AUTHDATA-IN-TICKET-EXTENSIONS(7), + kRB5-AUTHDATA-MANDATORY-FOR-KDC(8), + kRB5-AUTHDATA-INITIAL-VERIFIED-CAS(9), + kRB5-AUTHDATA-OSF-DCE(64), + kRB5-AUTHDATA-SESAME(65), + kRB5-AUTHDATA-OSF-DCE-PKI-CERTID(66), + kRB5-AUTHDATA-WIN2K-PAC(128), + kRB5-AUTHDATA-GSS-API-ETYPE-NEGOTIATION(129), -- Authenticator only + kRB5-AUTHDATA-SIGNTICKET-OLDER(-17), + kRB5-AUTHDATA-SIGNTICKET-OLD(142), + kRB5-AUTHDATA-SIGNTICKET(512) +} +AuthDataTypeSequence ::= SEQUENCE { + dummy [0] AuthDataTypeValues +} + +ChecksumTypeValues ::= INTEGER { + kRB5-CKSUMTYPE-NONE(0), + kRB5-CKSUMTYPE-CRC32(1), + kRB5-CKSUMTYPE-RSA-MD4(2), + kRB5-CKSUMTYPE-RSA-MD4-DES(3), + kRB5-CKSUMTYPE-DES-MAC(4), + kRB5-CKSUMTYPE-DES-MAC-K(5), + kRB5-CKSUMTYPE-RSA-MD4-DES-K(6), + kRB5-CKSUMTYPE-RSA-MD5(7), + kRB5-CKSUMTYPE-RSA-MD5-DES(8), + kRB5-CKSUMTYPE-RSA-MD5-DES3(9), + kRB5-CKSUMTYPE-SHA1-OTHER(10), + kRB5-CKSUMTYPE-HMAC-SHA1-DES3(12), + kRB5-CKSUMTYPE-SHA1(14), + kRB5-CKSUMTYPE-HMAC-SHA1-96-AES-128(15), + kRB5-CKSUMTYPE-HMAC-SHA1-96-AES-256(16), + kRB5-CKSUMTYPE-GSSAPI(32771), -- 0x8003 + kRB5-CKSUMTYPE-HMAC-MD5(-138), -- unofficial microsoft number + kRB5-CKSUMTYPE-HMAC-MD5-ENC(-1138) -- even more unofficial +} +ChecksumTypeSequence ::= SEQUENCE { + dummy [0] ChecksumTypeValues +} + +EncryptionTypeValues ::= INTEGER { + kRB5-ENCTYPE-NULL(0), + kRB5-ENCTYPE-DES-CBC-CRC(1), + kRB5-ENCTYPE-DES-CBC-MD4(2), + kRB5-ENCTYPE-DES-CBC-MD5(3), + kRB5-ENCTYPE-DES3-CBC-MD5(5), + kRB5-ENCTYPE-OLD-DES3-CBC-SHA1(7), + kRB5-ENCTYPE-SIGN-DSA-GENERATE(8), + kRB5-ENCTYPE-ENCRYPT-RSA-PRIV(9), + kRB5-ENCTYPE-ENCRYPT-RSA-PUB(10), + kRB5-ENCTYPE-DES3-CBC-SHA1(16), -- with key derivation + kRB5-ENCTYPE-AES128-CTS-HMAC-SHA1-96(17), + kRB5-ENCTYPE-AES256-CTS-HMAC-SHA1-96(18), + kRB5-ENCTYPE-ARCFOUR-HMAC-MD5(23), + kRB5-ENCTYPE-ARCFOUR-HMAC-MD5-56(24), + kRB5-ENCTYPE-ENCTYPE-PK-CROSS(48), +-- some "old" windows types + kRB5-ENCTYPE-ARCFOUR-MD4(-128), + kRB5-ENCTYPE-ARCFOUR-HMAC-OLD(-133), + kRB5-ENCTYPE-ARCFOUR-HMAC-OLD-EXP(-135), +-- these are for Heimdal internal use +-- kRB5-ENCTYPE-DES-CBC-NONE(-0x1000), +-- kRB5-ENCTYPE-DES3-CBC-NONE(-0x1001), +-- kRB5-ENCTYPE-DES-CFB64-NONE(-0x1002), +-- kRB5-ENCTYPE-DES-PCBC-NONE(-0x1003), +-- kRB5-ENCTYPE-DIGEST-MD5-NONE(-0x1004), - private use, lukeh@padl.com +-- kRB5-ENCTYPE-CRAM-MD5-NONE(-0x1005) - private use, lukeh@padl.com + kRB5-ENCTYPE-DUMMY(-1111) +} +EncryptionTypeSequence ::= SEQUENCE { + dummy [0] EncryptionTypeValues +} + +KerbErrorDataTypeValues ::= INTEGER { + kERB-AP-ERR-TYPE-SKEW-RECOVERY(2), + kERB-ERR-TYPE-EXTENDED(3) +} +KerbErrorDataTypeSequence ::= SEQUENCE { + dummy [0] KerbErrorDataTypeValues +} + +PACOptionFlagsValues ::= BIT STRING { -- KerberosFlags + claims(0), + branch-aware(1), + forward-to-full-dc(2), + resource-based-constrained-delegation(3) +} +PACOptionFlagsSequence ::= SEQUENCE { + dummy [0] PACOptionFlagsValues +} + +END diff --git a/python/samba/tests/krb5/rfc4120_constants.py b/python/samba/tests/krb5/rfc4120_constants.py new file mode 100644 index 0000000..dff6017 --- /dev/null +++ b/python/samba/tests/krb5/rfc4120_constants.py @@ -0,0 +1,247 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) 2020 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +# Encryption types +AES256_CTS_HMAC_SHA1_96 = int( + krb5_asn1.EncryptionTypeValues('kRB5-ENCTYPE-AES256-CTS-HMAC-SHA1-96')) +AES128_CTS_HMAC_SHA1_96 = int( + krb5_asn1.EncryptionTypeValues('kRB5-ENCTYPE-AES128-CTS-HMAC-SHA1-96')) +ARCFOUR_HMAC_MD5 = int( + krb5_asn1.EncryptionTypeValues('kRB5-ENCTYPE-ARCFOUR-HMAC-MD5')) +DES_CBC_CRC = int( + krb5_asn1.EncryptionTypeValues('kRB5-ENCTYPE-DES-CBC-CRC')) +DES_CBC_MD5 = int( + krb5_asn1.EncryptionTypeValues('kRB5-ENCTYPE-DES-CBC-MD5')) +DES3_CBC_MD5 = int( + krb5_asn1.EncryptionTypeValues('kRB5-ENCTYPE-DES3-CBC-MD5')) +DES3_CBC_SHA1 = int( + krb5_asn1.EncryptionTypeValues('kRB5-ENCTYPE-DES3-CBC-SHA1')) + +DES_EDE3_CBC = 15 # des-ede3-cbc-EnvOID — required for Windows PK-INIT. + +# Message types +KRB_ERROR = int(krb5_asn1.MessageTypeValues('krb-error')) +KRB_AP_REP = int(krb5_asn1.MessageTypeValues('krb-ap-rep')) +KRB_AP_REQ = int(krb5_asn1.MessageTypeValues('krb-ap-req')) +KRB_AS_REP = int(krb5_asn1.MessageTypeValues('krb-as-rep')) +KRB_AS_REQ = int(krb5_asn1.MessageTypeValues('krb-as-req')) +KRB_TGS_REP = int(krb5_asn1.MessageTypeValues('krb-tgs-rep')) +KRB_TGS_REQ = int(krb5_asn1.MessageTypeValues('krb-tgs-req')) +KRB_PRIV = int(krb5_asn1.MessageTypeValues('krb-priv')) + +# PAData types +PADATA_ENC_TIMESTAMP = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-ENC-TIMESTAMP')) +PADATA_ENCRYPTED_CHALLENGE = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-ENCRYPTED-CHALLENGE')) +PADATA_ETYPE_INFO = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-ETYPE-INFO')) +PADATA_ETYPE_INFO2 = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-ETYPE-INFO2')) +PADATA_FOR_USER = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-FOR-USER')) +PADATA_FX_COOKIE = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-FX-COOKIE')) +PADATA_FX_ERROR = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-FX-ERROR')) +PADATA_FX_FAST = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-FX-FAST')) +PADATA_KDC_REQ = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-KDC-REQ')) +PADATA_PAC_OPTIONS = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-PAC-OPTIONS')) +PADATA_PAC_REQUEST = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-PA-PAC-REQUEST')) +PADATA_PK_AS_REQ = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-PK-AS-REQ')) +PADATA_PK_AS_REP = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-PK-AS-REP')) +PADATA_PK_AS_REQ_19 = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-PK-AS-REQ-19')) +PADATA_PK_AS_REP_19 = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-PK-AS-REP-19')) +PADATA_PW_SALT = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-PW-SALT')) +PADATA_SUPPORTED_ETYPES = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-SUPPORTED-ETYPES')) +PADATA_PKINIT_KX = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-PKINIT-KX')) +PADATA_GSS = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-GSS')) +PADATA_REQ_ENC_PA_REP = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-REQ-ENC-PA-REP')) +PADATA_AS_FRESHNESS = int( + krb5_asn1.PADataTypeValues('kRB5-PADATA-AS-FRESHNESS')) + +# Error codes +KDC_ERR_C_PRINCIPAL_UNKNOWN = 6 +KDC_ERR_S_PRINCIPAL_UNKNOWN = 7 +KDC_ERR_NEVER_VALID = 11 +KDC_ERR_POLICY = 12 +KDC_ERR_BADOPTION = 13 +KDC_ERR_ETYPE_NOSUPP = 14 +KDC_ERR_SUMTYPE_NOSUPP = 15 +KDC_ERR_CLIENT_REVOKED = 18 +KDC_ERR_TGT_REVOKED = 20 +KDC_ERR_PREAUTH_FAILED = 24 +KDC_ERR_PREAUTH_REQUIRED = 25 +KDC_ERR_SERVER_NOMATCH = 26 +KDC_ERR_BAD_INTEGRITY = 31 +KDC_ERR_TKT_EXPIRED = 32 +KRB_ERR_TKT_NYV = 33 +KDC_ERR_NOT_US = 35 +KDC_ERR_BADMATCH = 36 +KDC_ERR_SKEW = 37 +KDC_ERR_MODIFIED = 41 +KDC_ERR_BADKEYVER = 44 +KDC_ERR_INAPP_CKSUM = 50 +KDC_ERR_GENERIC = 60 +KDC_ERR_CLIENT_NOT_TRUSTED = 62 +KDC_ERR_INVALID_SIG = 64 +KDC_ERR_DH_KEY_PARAMETERS_NOT_ACCEPTED = 65 +KDC_ERR_WRONG_REALM = 68 +KDC_ERR_CANT_VERIFY_CERTIFICATE = 70 +KDC_ERR_INVALID_CERTIFICATE = 71 +KDC_ERR_REVOKED_CERTIFICATE = 72 +KDC_ERR_REVOCATION_STATUS_UNKNOWN = 73 +KDC_ERR_CLIENT_NAME_MISMATCH = 75 +KDC_ERR_INCONSISTENT_KEY_PURPOSE = 77 +KDC_ERR_DIGEST_IN_CERT_NOT_ACCEPTED = 78 +KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED = 79 +KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED = 80 +KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED = 81 +KDC_ERR_PREAUTH_EXPIRED = 90 +KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS = 93 + +# Kpasswd error codes +KPASSWD_SUCCESS = 0 +KPASSWD_MALFORMED = 1 +KPASSWD_HARDERROR = 2 +KPASSWD_AUTHERROR = 3 +KPASSWD_SOFTERROR = 4 +KPASSWD_ACCESSDENIED = 5 +KPASSWD_BAD_VERSION = 6 +KPASSWD_INITIAL_FLAG_NEEDED = 7 + +# Extended error types +KERB_AP_ERR_TYPE_SKEW_RECOVERY = int( + krb5_asn1.KerbErrorDataTypeValues('kERB-AP-ERR-TYPE-SKEW-RECOVERY')) +KERB_ERR_TYPE_EXTENDED = int( + krb5_asn1.KerbErrorDataTypeValues('kERB-ERR-TYPE-EXTENDED')) + +# Name types +NT_UNKNOWN = int(krb5_asn1.NameTypeValues('kRB5-NT-UNKNOWN')) +NT_PRINCIPAL = int(krb5_asn1.NameTypeValues('kRB5-NT-PRINCIPAL')) +NT_SRV_HST = int(krb5_asn1.NameTypeValues('kRB5-NT-SRV-HST')) +NT_SRV_INST = int(krb5_asn1.NameTypeValues('kRB5-NT-SRV-INST')) +NT_ENTERPRISE_PRINCIPAL = int(krb5_asn1.NameTypeValues( + 'kRB5-NT-ENTERPRISE-PRINCIPAL')) +NT_WELLKNOWN = int(krb5_asn1.NameTypeValues('kRB5-NT-WELLKNOWN')) + +# Authorization data ad-type values + +AD_IF_RELEVANT = 1 +AD_INTENDED_FOR_SERVER = 2 +AD_INTENDED_FOR_APPLICATION_CLASS = 3 +AD_KDC_ISSUED = 4 +AD_AND_OR = 5 +AD_MANDATORY_TICKET_EXTENSIONS = 6 +AD_IN_TICKET_EXTENSIONS = 7 +AD_MANDATORY_FOR_KDC = 8 +AD_INITIAL_VERIFIED_CAS = 9 +AD_FX_FAST_ARMOR = 71 +AD_FX_FAST_USED = 72 +AD_WIN2K_PAC = 128 +AD_SIGNTICKET = 512 + +# Key usage numbers +# RFC 4120 Section 7.5.1. Key Usage Numbers +KU_PA_ENC_TIMESTAMP = 1 +''' AS-REQ PA-ENC-TIMESTAMP padata timestamp, encrypted with the + client key (section 5.2.7.2) ''' +KU_TICKET = 2 +''' AS-REP Ticket and TGS-REP Ticket (includes tgs session key or + application session key), encrypted with the service key + (section 5.3) ''' +KU_AS_REP_ENC_PART = 3 +''' AS-REP encrypted part (includes tgs session key or application + session key), encrypted with the client key (section 5.4.2) ''' +KU_TGS_REQ_AUTH_DAT_SESSION = 4 +''' TGS-REQ KDC-REQ-BODY AuthorizationData, encrypted with the tgs + session key (section 5.4.1) ''' +KU_TGS_REQ_AUTH_DAT_SUBKEY = 5 +''' TGS-REQ KDC-REQ-BODY AuthorizationData, encrypted with the tgs + authenticator subkey (section 5.4.1) ''' +KU_TGS_REQ_AUTH_CKSUM = 6 +''' TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator cksum, keyed + with the tgs session key (section 5.5.1) ''' +KU_PKINIT_AS_REQ = 6 +KU_TGS_REQ_AUTH = 7 +''' TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes tgs + authenticator subkey), encrypted with the tgs session key + (section 5.5.1) ''' +KU_TGS_REP_ENC_PART_SESSION = 8 +''' TGS-REP encrypted part (includes application session key), + encrypted with the tgs session key (section 5.4.2) ''' +KU_TGS_REP_ENC_PART_SUB_KEY = 9 +''' TGS-REP encrypted part (includes application session key), + encrypted with the tgs authenticator subkey (section 5.4.2) ''' +KU_AP_REQ_AUTH_CKSUM = 10 +''' AP-REQ Authenticator cksum, keyed with the application session + key (section 5.5.1) ''' +KU_AP_REQ_AUTH = 11 +''' AP-REQ Authenticator (includes application authenticator + subkey), encrypted with the application session key (section 5.5.1) ''' +KU_AP_REQ_ENC_PART = 12 +''' AP-REP encrypted part (includes application session subkey), + encrypted with the application session key (section 5.5.2) ''' +KU_KRB_PRIV = 13 +''' KRB-PRIV encrypted part, encrypted with a key chosen by the + application (section 5.7.1) ''' +KU_KRB_CRED = 14 +''' KRB-CRED encrypted part, encrypted with a key chosen by the + application (section 5.8.1) ''' +KU_KRB_SAFE_CKSUM = 15 +''' KRB-SAFE cksum, keyed with a key chosen by the application + (section 5.6.1) ''' +KU_NON_KERB_SALT = 16 +KU_NON_KERB_CKSUM_SALT = 17 + +KU_ACCEPTOR_SEAL = 22 +KU_ACCEPTOR_SIGN = 23 +KU_INITIATOR_SEAL = 24 +KU_INITIATOR_SIGN = 25 + +KU_FAST_REQ_CHKSUM = 50 +KU_FAST_ENC = 51 +KU_FAST_REP = 52 +KU_FAST_FINISHED = 53 +KU_ENC_CHALLENGE_CLIENT = 54 +KU_ENC_CHALLENGE_KDC = 55 +KU_AS_REQ = 56 + +KU_AS_FRESHNESS = 60 + +# Armor types +FX_FAST_ARMOR_AP_REQUEST = 1 + +# PKINIT typed data errors +TD_TRUSTED_CERTIFIERS = 104 +TD_INVALID_CERTIFICATES = 105 +TD_DH_PARAMETERS = 109 diff --git a/python/samba/tests/krb5/rfc4120_pyasn1.py b/python/samba/tests/krb5/rfc4120_pyasn1.py new file mode 100644 index 0000000..ad8a6e7 --- /dev/null +++ b/python/samba/tests/krb5/rfc4120_pyasn1.py @@ -0,0 +1,92 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Catalyst.Net Ltd 2023 +# +# 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 <http://www.gnu.org/licenses/>. +# + +from samba.tests.krb5.rfc4120_pyasn1_generated import * + +# Kerberos strings should generally be treated as UTF‐8 encoded, but asn1ate +# (the tool which generates Python definitions from our ASN.1 modules) provides +# no way to specify the encoding to use. By the time we’ve imported +# ‘rfc4120_pyasn1_generated’, KerberosString in the process having been +# instantiated as part of several schema objects, it’s too late to change the +# existing objects. But by overriding the __getattribute__() method on +# KerberosString, we can have objects of that type, or a subtype thereof, +# encoded as UTF‐8 strings instead of as ISO-8859-1 strings (the default). + +class ReadOnlyUtf8EncodingDict(dict): + # Don’t allow any attributes to be set. + __slots__ = [] + + def __getitem__(self, key): + # Get the original item. This will raise KeyError if it’s not present. + val = super().__getitem__(key) + + # If anyone wants to know our encoding, say it’s UTF‐8. + if key == 'encoding': + return 'utf-8' + + return val + + # Python’s default implementations of the following methods don’t call + # __getitem__(), so we’ll need to override them with our own replacements. + # In behaviour, they are close enough to the originals for our purposes. + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def items(self): + for key in self: + yield key, self[key] + + def values(self): + for key in self: + yield self[key] + + # Don’t let anyone modify the dict’s contents. + + def __setitem__(self, key, val): + raise TypeError('item assignment not supported') + + def __delitem__(self, key): + raise TypeError('item deletion not supported') + + +KerberosString_get_attribute = KerberosString.__getattribute__ + +def get_attribute_override(self, attr): + # Get the original attribute. This will raise AttributeError if it’s not + # present. + val = KerberosString_get_attribute(self, attr) + + # If anyone wants to know our encoding, say it’s UTF‐8. + if attr == 'encoding': + return 'utf-8' + + if attr == '_readOnly': + # Return a copy of the read‐only attributes with the encoding overridden + # to be UTF-8. To avoid the possibility of changes being made to the + # original dict that do not propagate to its copies, the returned dict + # does not allow modification of its contents. Besides, this is supposed + # to be read‐only. + return ReadOnlyUtf8EncodingDict(val) + + return val + +# Override the __getattribute__() method on KerberosString. +KerberosString.__getattribute__ = get_attribute_override diff --git a/python/samba/tests/krb5/rfc4120_pyasn1_generated.py b/python/samba/tests/krb5/rfc4120_pyasn1_generated.py new file mode 100644 index 0000000..6949737 --- /dev/null +++ b/python/samba/tests/krb5/rfc4120_pyasn1_generated.py @@ -0,0 +1,2690 @@ +# Auto-generated by asn1ate v.0.6.1.dev0 from rfc4120.asn1 +# (last modified on 2023-12-15 11:13:21.627710) + +# KerberosV5Spec2 +from pyasn1.type import univ, char, namedtype, namedval, tag, constraint, useful + + +def _OID(*components): + output = [] + for x in tuple(components): + if isinstance(x, univ.ObjectIdentifier): + output.extend(list(x)) + else: + output.append(int(x)) + + return univ.ObjectIdentifier(output) + + +class Int32(univ.Integer): + pass + + +Int32.subtypeSpec = constraint.ValueRangeConstraint(-2147483648, 2147483647) + + +class AuthDataType(Int32): + pass + + +class AuthorizationData(univ.SequenceOf): + pass + + +AuthorizationData.componentType = univ.Sequence(componentType=namedtype.NamedTypes( + namedtype.NamedType('ad-type', AuthDataType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('ad-data', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +)) + + +class AD_AND_OR(univ.Sequence): + pass + + +AD_AND_OR.componentType = namedtype.NamedTypes( + namedtype.NamedType('condition-count', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('elements', AuthorizationData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class AD_IF_RELEVANT(AuthorizationData): + pass + + +class ExternalPrincipalIdentifier(univ.Sequence): + pass + + +ExternalPrincipalIdentifier.componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('subjectName', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('issuerAndSerialNumber', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('subjectKeyIdentifier', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))) +) + + +class AD_INITIAL_VERIFIED_CAS(univ.SequenceOf): + pass + + +AD_INITIAL_VERIFIED_CAS.componentType = ExternalPrincipalIdentifier() + + +class ChecksumType(Int32): + pass + + +class Checksum(univ.Sequence): + pass + + +Checksum.componentType = namedtype.NamedTypes( + namedtype.NamedType('cksumtype', ChecksumType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('checksum', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class KerberosString(char.GeneralString): + pass + + +class NameType(Int32): + pass + + +class PrincipalName(univ.Sequence): + pass + + +PrincipalName.componentType = namedtype.NamedTypes( + namedtype.NamedType('name-type', NameType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('name-string', univ.SequenceOf(componentType=KerberosString()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class Realm(KerberosString): + pass + + +class AD_KDCIssued(univ.Sequence): + pass + + +AD_KDCIssued.componentType = namedtype.NamedTypes( + namedtype.NamedType('ad-checksum', Checksum().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.OptionalNamedType('i-realm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('i-sname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.NamedType('elements', AuthorizationData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))) +) + + +class AD_MANDATORY_FOR_KDC(AuthorizationData): + pass + + +class EncryptionType(Int32): + pass + + +class EncryptedData(univ.Sequence): + pass + + +EncryptedData.componentType = namedtype.NamedTypes( + namedtype.NamedType('etype', EncryptionType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('kvno', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('cipher', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))) +) + + +class AP_REP(univ.Sequence): + pass + + +AP_REP.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 15)) +AP_REP.componentType = namedtype.NamedTypes( + namedtype.NamedType('pvno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('msg-type', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(15)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('enc-part', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))) +) + + +class KerberosFlags(univ.BitString): + pass + + +KerberosFlags.subtypeSpec=constraint.ValueSizeConstraint(1, 32) + + +class APOptions(KerberosFlags): + pass + + +class Ticket(univ.Sequence): + pass + + +Ticket.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 1)) +Ticket.componentType = namedtype.NamedTypes( + namedtype.NamedType('tkt-vno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('realm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('sname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.NamedType('enc-part', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))) +) + + +class AP_REQ(univ.Sequence): + pass + + +AP_REQ.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 14)) +AP_REQ.componentType = namedtype.NamedTypes( + namedtype.NamedType('pvno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('msg-type', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(14)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('ap-options', APOptions().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.NamedType('ticket', Ticket().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.NamedType('authenticator', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))) +) + + +class APOptionsValues(univ.BitString): + pass + + +APOptionsValues.namedValues = namedval.NamedValues( + ('reserved', 0), + ('use-session-key', 1), + ('mutual-required', 2) +) + + +class APOptionsSequence(univ.Sequence): + pass + + +APOptionsSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', APOptionsValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class PADataType(Int32): + pass + + +class PA_DATA(univ.Sequence): + pass + + +PA_DATA.componentType = namedtype.NamedTypes( + namedtype.NamedType('padata-type', PADataType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('padata-value', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))) +) + + +class KDC_REP(univ.Sequence): + pass + + +KDC_REP.componentType = namedtype.NamedTypes( + namedtype.NamedType('pvno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('msg-type', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(11, 13)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('padata', univ.SequenceOf(componentType=PA_DATA()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.NamedType('crealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.NamedType('cname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))), + namedtype.NamedType('ticket', Ticket().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))), + namedtype.NamedType('enc-part', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 6))) +) + + +class AS_REP(KDC_REP): + pass + + +AS_REP.tagSet = KDC_REP.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 11)) + + +class HostAddress(univ.Sequence): + pass + + +HostAddress.componentType = namedtype.NamedTypes( + namedtype.NamedType('addr-type', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('address', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class HostAddresses(univ.SequenceOf): + pass + + +HostAddresses.componentType = HostAddress() + + +class KDCOptions(KerberosFlags): + pass + + +class KerberosTime(useful.GeneralizedTime): + pass + + +class UInt32(univ.Integer): + pass + + +UInt32.subtypeSpec = constraint.ValueRangeConstraint(0, 4294967295) + + +class KDC_REQ_BODY(univ.Sequence): + pass + + +KDC_REQ_BODY.componentType = namedtype.NamedTypes( + namedtype.NamedType('kdc-options', KDCOptions().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('cname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))), + namedtype.NamedType('realm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('sname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))), + namedtype.OptionalNamedType('from', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))), + namedtype.NamedType('till', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))), + namedtype.OptionalNamedType('rtime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))), + namedtype.NamedType('nonce', UInt32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))), + namedtype.NamedType('etype', univ.SequenceOf(componentType=EncryptionType()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 8))), + namedtype.OptionalNamedType('addresses', HostAddresses().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 9))), + namedtype.OptionalNamedType('enc-authorization-data', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 10))), + namedtype.OptionalNamedType('additional-tickets', univ.SequenceOf(componentType=Ticket()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 11))) +) + + +class KDC_REQ(univ.Sequence): + pass + + +KDC_REQ.componentType = namedtype.NamedTypes( + namedtype.NamedType('pvno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('msg-type', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(10, 12)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('padata', univ.SequenceOf(componentType=PA_DATA()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.NamedType('req-body', KDC_REQ_BODY().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))) +) + + +class AS_REQ(KDC_REQ): + pass + + +AS_REQ.tagSet = KDC_REQ.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 10)) + + +ub_domain_name_length = univ.Integer(16) + + +class AdministrationDomainName(univ.Choice): + pass + + +AdministrationDomainName.tagSet = univ.Choice.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 2)) +AdministrationDomainName.componentType = namedtype.NamedTypes( + namedtype.NamedType('numeric', char.NumericString().subtype(subtypeSpec=constraint.ValueSizeConstraint(0, ub_domain_name_length))), + namedtype.NamedType('printable', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(0, ub_domain_name_length))) +) + + +class AlgorithmIdentifier(univ.Sequence): + pass + + +AlgorithmIdentifier.componentType = namedtype.NamedTypes( + namedtype.NamedType('algorithm', univ.ObjectIdentifier()), + namedtype.OptionalNamedType('parameters', univ.Any()) +) + + +class DirectoryString(univ.Choice): + pass + + +DirectoryString.componentType = namedtype.NamedTypes( + namedtype.NamedType('teletexString', char.TeletexString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, 256))), + namedtype.NamedType('printableString', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, 256))), + namedtype.NamedType('universalString', char.UniversalString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, 256))), + namedtype.NamedType('utf8String', char.UTF8String().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, 256))), + namedtype.NamedType('bmpString', char.BMPString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, 256))) +) + + +class EDIPartyName(univ.Sequence): + pass + + +EDIPartyName.componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('nameAssigner', DirectoryString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('partyName', DirectoryString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))) +) + + +class AttributeType(univ.ObjectIdentifier): + pass + + +class AttributeValue(univ.Any): + pass + + +class AttributeTypeAndValue(univ.Sequence): + pass + + +AttributeTypeAndValue.componentType = namedtype.NamedTypes( + namedtype.NamedType('type', AttributeType()), + namedtype.NamedType('value', AttributeValue()) +) + + +class RelativeDistinguishedName(univ.SetOf): + pass + + +RelativeDistinguishedName.componentType = AttributeTypeAndValue() +RelativeDistinguishedName.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class RDNSequence(univ.SequenceOf): + pass + + +RDNSequence.componentType = RelativeDistinguishedName() + + +class Name(univ.Choice): + pass + + +Name.componentType = namedtype.NamedTypes( + namedtype.NamedType('rdnSequence', RDNSequence()) +) + + +ub_domain_defined_attribute_type_length = univ.Integer(8) + + +ub_domain_defined_attribute_value_length = univ.Integer(128) + + +class BuiltInDomainDefinedAttribute(univ.Sequence): + pass + + +BuiltInDomainDefinedAttribute.componentType = namedtype.NamedTypes( + namedtype.NamedType('type', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, ub_domain_defined_attribute_type_length))), + namedtype.NamedType('value', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, ub_domain_defined_attribute_value_length))) +) + + +ub_domain_defined_attributes = univ.Integer(4) + + +class BuiltInDomainDefinedAttributes(univ.SequenceOf): + pass + + +BuiltInDomainDefinedAttributes.componentType = BuiltInDomainDefinedAttribute() +BuiltInDomainDefinedAttributes.subtypeSpec=constraint.ValueSizeConstraint(1, ub_domain_defined_attributes) + + +ub_country_name_alpha_length = univ.Integer(2) + + +ub_country_name_numeric_length = univ.Integer(3) + + +class CountryName(univ.Choice): + pass + + +CountryName.tagSet = univ.Choice.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 1)) +CountryName.componentType = namedtype.NamedTypes( + namedtype.NamedType('x121-dcc-code', char.NumericString().subtype(subtypeSpec=constraint.ValueSizeConstraint(ub_country_name_numeric_length, ub_country_name_numeric_length))), + namedtype.NamedType('iso-3166-alpha2-code', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(ub_country_name_alpha_length, ub_country_name_alpha_length))) +) + + +ub_x121_address_length = univ.Integer(16) + + +class X121Address(char.NumericString): + pass + + +X121Address.subtypeSpec = constraint.ValueSizeConstraint(1, ub_x121_address_length) + + +class NetworkAddress(X121Address): + pass + + +ub_numeric_user_id_length = univ.Integer(32) + + +class NumericUserIdentifier(char.NumericString): + pass + + +NumericUserIdentifier.subtypeSpec = constraint.ValueSizeConstraint(1, ub_numeric_user_id_length) + + +ub_organization_name_length = univ.Integer(64) + + +class OrganizationName(char.PrintableString): + pass + + +OrganizationName.subtypeSpec = constraint.ValueSizeConstraint(1, ub_organization_name_length) + + +ub_organizational_unit_name_length = univ.Integer(32) + + +class OrganizationalUnitName(char.PrintableString): + pass + + +OrganizationalUnitName.subtypeSpec = constraint.ValueSizeConstraint(1, ub_organizational_unit_name_length) + + +ub_organizational_units = univ.Integer(4) + + +class OrganizationalUnitNames(univ.SequenceOf): + pass + + +OrganizationalUnitNames.componentType = OrganizationalUnitName() +OrganizationalUnitNames.subtypeSpec=constraint.ValueSizeConstraint(1, ub_organizational_units) + + +ub_generation_qualifier_length = univ.Integer(3) + + +ub_given_name_length = univ.Integer(16) + + +ub_initials_length = univ.Integer(5) + + +ub_surname_length = univ.Integer(40) + + +class PersonalName(univ.Set): + pass + + +PersonalName.componentType = namedtype.NamedTypes( + namedtype.NamedType('surname', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, ub_surname_length)).subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('given-name', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, ub_given_name_length)).subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('initials', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, ub_initials_length)).subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('generation-qualifier', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, ub_generation_qualifier_length)).subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))) +) + + +class PrivateDomainName(univ.Choice): + pass + + +PrivateDomainName.componentType = namedtype.NamedTypes( + namedtype.NamedType('numeric', char.NumericString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, ub_domain_name_length))), + namedtype.NamedType('printable', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, ub_domain_name_length))) +) + + +ub_terminal_id_length = univ.Integer(24) + + +class TerminalIdentifier(char.PrintableString): + pass + + +TerminalIdentifier.subtypeSpec = constraint.ValueSizeConstraint(1, ub_terminal_id_length) + + +class BuiltInStandardAttributes(univ.Sequence): + pass + + +BuiltInStandardAttributes.componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('country-name', CountryName()), + namedtype.OptionalNamedType('administration-domain-name', AdministrationDomainName()), + namedtype.OptionalNamedType('network-address', NetworkAddress().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('terminal-identifier', TerminalIdentifier().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('private-domain-name', PrivateDomainName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.OptionalNamedType('organization-name', OrganizationName().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.OptionalNamedType('numeric-user-identifier', NumericUserIdentifier().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))), + namedtype.OptionalNamedType('personal-name', PersonalName().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 5))), + namedtype.OptionalNamedType('organizational-unit-names', OrganizationalUnitNames().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))) +) + + +ub_extension_attributes = univ.Integer(256) + + +class ExtensionAttribute(univ.Sequence): + pass + + +ExtensionAttribute.componentType = namedtype.NamedTypes( + namedtype.NamedType('extension-attribute-type', univ.Integer().subtype(subtypeSpec=constraint.ValueRangeConstraint(0, ub_extension_attributes)).subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('extension-attribute-value', univ.Any().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class ExtensionAttributes(univ.SetOf): + pass + + +ExtensionAttributes.componentType = ExtensionAttribute() +ExtensionAttributes.subtypeSpec=constraint.ValueSizeConstraint(1, ub_extension_attributes) + + +class ORAddress(univ.Sequence): + pass + + +ORAddress.componentType = namedtype.NamedTypes( + namedtype.NamedType('built-in-standard-attributes', BuiltInStandardAttributes()), + namedtype.OptionalNamedType('built-in-domain-defined-attributes', BuiltInDomainDefinedAttributes()), + namedtype.OptionalNamedType('extension-attributes', ExtensionAttributes()) +) + + +class OtherName(univ.Sequence): + pass + + +OtherName.componentType = namedtype.NamedTypes( + namedtype.NamedType('type-id', univ.ObjectIdentifier()), + namedtype.NamedType('value', univ.Any().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class GeneralName(univ.Choice): + pass + + +GeneralName.componentType = namedtype.NamedTypes( + namedtype.NamedType('otherName', OtherName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('rfc822Name', char.IA5String().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('dNSName', char.IA5String().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.NamedType('x400Address', ORAddress().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))), + namedtype.NamedType('directoryName', Name().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))), + namedtype.NamedType('ediPartyName', EDIPartyName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 5))), + namedtype.NamedType('uniformResourceIdentifier', char.IA5String().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))), + namedtype.NamedType('iPAddress', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))), + namedtype.NamedType('registeredID', univ.ObjectIdentifier().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 8))) +) + + +class GeneralNames(univ.SequenceOf): + pass + + +GeneralNames.componentType = GeneralName() +GeneralNames.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class CertificateSerialNumber(univ.Integer): + pass + + +class UniqueIdentifier(univ.BitString): + pass + + +class IssuerSerial(univ.Sequence): + pass + + +IssuerSerial.componentType = namedtype.NamedTypes( + namedtype.NamedType('issuer', GeneralNames()), + namedtype.NamedType('serial', CertificateSerialNumber()), + namedtype.OptionalNamedType('issuerUID', UniqueIdentifier()) +) + + +class ObjectDigestInfo(univ.Sequence): + pass + + +ObjectDigestInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('digestedObjectType', univ.Enumerated(namedValues=namedval.NamedValues(('publicKey', 0), ('publicKeyCert', 1), ('otherObjectTypes', 2)))), + namedtype.OptionalNamedType('otherObjectTypeID', univ.ObjectIdentifier()), + namedtype.NamedType('digestAlgorithm', AlgorithmIdentifier()), + namedtype.NamedType('objectDigest', univ.BitString()) +) + + +class V2Form(univ.Sequence): + pass + + +V2Form.componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('issuerName', GeneralNames()), + namedtype.OptionalNamedType('baseCertificateID', IssuerSerial().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.OptionalNamedType('objectDigestInfo', ObjectDigestInfo().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))) +) + + +class AttCertIssuer(univ.Choice): + pass + + +AttCertIssuer.componentType = namedtype.NamedTypes( + namedtype.NamedType('v1Form', GeneralNames()), + namedtype.NamedType('v2Form', V2Form().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))) +) + + +class AttCertValidityPeriod(univ.Sequence): + pass + + +AttCertValidityPeriod.componentType = namedtype.NamedTypes( + namedtype.NamedType('notBeforeTime', useful.GeneralizedTime()), + namedtype.NamedType('notAfterTime', useful.GeneralizedTime()) +) + + +class AttCertVersion(univ.Integer): + pass + + +AttCertVersion.namedValues = namedval.NamedValues( + ('v2', 1) +) + + +class AttCertVersionV1(univ.Integer): + pass + + +AttCertVersionV1.namedValues = namedval.NamedValues( + ('v1', 0) +) + + +class Attribute(univ.Sequence): + pass + + +Attribute.componentType = namedtype.NamedTypes( + namedtype.NamedType('type', AttributeType()), + namedtype.NamedType('values', univ.SetOf(componentType=AttributeValue())) +) + + +class Extension(univ.Sequence): + pass + + +Extension.componentType = namedtype.NamedTypes( + namedtype.NamedType('extnID', univ.ObjectIdentifier()), + namedtype.DefaultedNamedType('critical', univ.Boolean().subtype(value=0)), + namedtype.NamedType('extnValue', univ.OctetString()) +) + + +class Extensions(univ.SequenceOf): + pass + + +Extensions.componentType = Extension() +Extensions.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class Holder(univ.Sequence): + pass + + +Holder.componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('baseCertificateID', IssuerSerial().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.OptionalNamedType('entityName', GeneralNames().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('objectDigestInfo', ObjectDigestInfo().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))) +) + + +class AttributeCertificateInfo(univ.Sequence): + pass + + +AttributeCertificateInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', AttCertVersion()), + namedtype.NamedType('holder', Holder()), + namedtype.NamedType('issuer', AttCertIssuer()), + namedtype.NamedType('signature', AlgorithmIdentifier()), + namedtype.NamedType('serialNumber', CertificateSerialNumber()), + namedtype.NamedType('attrCertValidityPeriod', AttCertValidityPeriod()), + namedtype.NamedType('attributes', univ.SequenceOf(componentType=Attribute())), + namedtype.OptionalNamedType('issuerUniqueID', UniqueIdentifier()), + namedtype.OptionalNamedType('extensions', Extensions()) +) + + +class AttributeCertificate(univ.Sequence): + pass + + +AttributeCertificate.componentType = namedtype.NamedTypes( + namedtype.NamedType('acinfo', AttributeCertificateInfo()), + namedtype.NamedType('signatureAlgorithm', AlgorithmIdentifier()), + namedtype.NamedType('signatureValue', univ.BitString()) +) + + +class AttributeCertificateInfoV1(univ.Sequence): + pass + + +AttributeCertificateInfoV1.componentType = namedtype.NamedTypes( + namedtype.DefaultedNamedType('version', AttCertVersionV1().subtype(value=1)), + namedtype.NamedType('subject', univ.Choice(componentType=namedtype.NamedTypes( + namedtype.NamedType('baseCertificateID', IssuerSerial().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('subjectName', GeneralNames().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) + )) + ), + namedtype.NamedType('issuer', GeneralNames()), + namedtype.NamedType('signature', AlgorithmIdentifier()), + namedtype.NamedType('serialNumber', CertificateSerialNumber()), + namedtype.NamedType('attCertValidityPeriod', AttCertValidityPeriod()), + namedtype.NamedType('attributes', univ.SequenceOf(componentType=Attribute())), + namedtype.OptionalNamedType('issuerUniqueID', UniqueIdentifier()), + namedtype.OptionalNamedType('extensions', Extensions()) +) + + +class AttributeCertificateV1(univ.Sequence): + pass + + +AttributeCertificateV1.componentType = namedtype.NamedTypes( + namedtype.NamedType('acInfo', AttributeCertificateInfoV1()), + namedtype.NamedType('signatureAlgorithm', AlgorithmIdentifier()), + namedtype.NamedType('signature', univ.BitString()) +) + + +class AttributeCertificateV2(AttributeCertificate): + pass + + +class AuthDataTypeValues(univ.Integer): + pass + + +AuthDataTypeValues.namedValues = namedval.NamedValues( + ('kRB5-AUTHDATA-IF-RELEVANT', 1), + ('kRB5-AUTHDATA-INTENDED-FOR-SERVER', 2), + ('kRB5-AUTHDATA-INTENDED-FOR-APPLICATION-CLASS', 3), + ('kRB5-AUTHDATA-KDC-ISSUED', 4), + ('kRB5-AUTHDATA-AND-OR', 5), + ('kRB5-AUTHDATA-MANDATORY-TICKET-EXTENSIONS', 6), + ('kRB5-AUTHDATA-IN-TICKET-EXTENSIONS', 7), + ('kRB5-AUTHDATA-MANDATORY-FOR-KDC', 8), + ('kRB5-AUTHDATA-INITIAL-VERIFIED-CAS', 9), + ('kRB5-AUTHDATA-OSF-DCE', 64), + ('kRB5-AUTHDATA-SESAME', 65), + ('kRB5-AUTHDATA-OSF-DCE-PKI-CERTID', 66), + ('kRB5-AUTHDATA-WIN2K-PAC', 128), + ('kRB5-AUTHDATA-GSS-API-ETYPE-NEGOTIATION', 129), + ('kRB5-AUTHDATA-SIGNTICKET-OLDER', -17), + ('kRB5-AUTHDATA-SIGNTICKET-OLD', 142), + ('kRB5-AUTHDATA-SIGNTICKET', 512) +) + + +class AuthDataTypeSequence(univ.Sequence): + pass + + +AuthDataTypeSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', AuthDataTypeValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class DHNonce(univ.OctetString): + pass + + +class PKAuthenticator(univ.Sequence): + pass + + +PKAuthenticator.componentType = namedtype.NamedTypes( + namedtype.NamedType('cusec', univ.Integer().subtype(subtypeSpec=constraint.ValueRangeConstraint(0, 999999)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('ctime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('nonce', univ.Integer().subtype(subtypeSpec=constraint.ValueRangeConstraint(0, 4294967295)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('paChecksum', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.OptionalNamedType('freshnessToken', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))) +) + + +class SubjectPublicKeyInfo(univ.Sequence): + pass + + +SubjectPublicKeyInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('algorithm', AlgorithmIdentifier()), + namedtype.NamedType('subjectPublicKey', univ.BitString()) +) + + +class AuthPack(univ.Sequence): + pass + + +AuthPack.componentType = namedtype.NamedTypes( + namedtype.NamedType('pkAuthenticator', PKAuthenticator().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.OptionalNamedType('clientPublicValue', SubjectPublicKeyInfo().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))), + namedtype.OptionalNamedType('supportedCMSTypes', univ.SequenceOf(componentType=AlgorithmIdentifier()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('clientDHNonce', DHNonce().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))) +) + + +class PKAuthenticator_Win2k(univ.Sequence): + pass + + +PKAuthenticator_Win2k.componentType = namedtype.NamedTypes( + namedtype.NamedType('kdcName', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('kdcRealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('cusec', univ.Integer().subtype(subtypeSpec=constraint.ValueRangeConstraint(0, 4294967295)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.NamedType('ctime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.NamedType('nonce', univ.Integer().subtype(subtypeSpec=constraint.ValueRangeConstraint(-2147483648, 2147483647)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))) +) + + +class AuthPack_Win2k(univ.Sequence): + pass + + +AuthPack_Win2k.componentType = namedtype.NamedTypes( + namedtype.NamedType('pkAuthenticator', PKAuthenticator_Win2k().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))) +) + + +class EncryptionKey(univ.Sequence): + pass + + +EncryptionKey.componentType = namedtype.NamedTypes( + namedtype.NamedType('keytype', EncryptionType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('keyvalue', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class Microseconds(univ.Integer): + pass + + +Microseconds.subtypeSpec = constraint.ValueRangeConstraint(0, 999999) + + +class Authenticator(univ.Sequence): + pass + + +Authenticator.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 2)) +Authenticator.componentType = namedtype.NamedTypes( + namedtype.NamedType('authenticator-vno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('crealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('cname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.OptionalNamedType('cksum', Checksum().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))), + namedtype.NamedType('cusec', Microseconds().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))), + namedtype.NamedType('ctime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))), + namedtype.OptionalNamedType('subkey', EncryptionKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 6))), + namedtype.OptionalNamedType('seq-number', UInt32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))), + namedtype.OptionalNamedType('authorization-data', AuthorizationData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 8))) +) + + +class CMSAttributes(univ.SetOf): + pass + + +CMSAttributes.componentType = Attribute() + + +class CMSCBCParameter(univ.OctetString): + pass + + +class CMSVersion(univ.Integer): + pass + + +CMSVersion.namedValues = namedval.NamedValues( + ('v0', 0), + ('v1', 1), + ('v2', 2), + ('v3', 3), + ('v4', 4), + ('v5', 5) +) + + +class SerialNumber(univ.Integer): + pass + + +class CRLEntry(univ.Sequence): + pass + + +CRLEntry.componentType = namedtype.NamedTypes( + namedtype.NamedType('userCertificate', SerialNumber()), + namedtype.NamedType('revocationDate', useful.UTCTime()) +) + + +class Time(univ.Choice): + pass + + +Time.componentType = namedtype.NamedTypes( + namedtype.NamedType('utcTime', useful.UTCTime()), + namedtype.NamedType('generalTime', useful.GeneralizedTime()) +) + + +class Validity(univ.Sequence): + pass + + +Validity.componentType = namedtype.NamedTypes( + namedtype.NamedType('notBefore', Time()), + namedtype.NamedType('notAfter', Time()) +) + + +class Version(univ.Integer): + pass + + +Version.namedValues = namedval.NamedValues( + ('v1', 0), + ('v2', 1), + ('v3', 2) +) + + +class TBSCertificate(univ.Sequence): + pass + + +TBSCertificate.componentType = namedtype.NamedTypes( + namedtype.DefaultedNamedType('version', Version().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0)).subtype(value=1)), + namedtype.NamedType('serialNumber', CertificateSerialNumber()), + namedtype.NamedType('signature', AlgorithmIdentifier()), + namedtype.NamedType('issuer', Name()), + namedtype.NamedType('validity', Validity()), + namedtype.NamedType('subject', Name()), + namedtype.NamedType('subjectPublicKeyInfo', SubjectPublicKeyInfo()), + namedtype.OptionalNamedType('issuerUniqueID', UniqueIdentifier().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('subjectUniqueID', UniqueIdentifier().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('extensions', Extensions().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))) +) + + +class Certificate(univ.Sequence): + pass + + +Certificate.componentType = namedtype.NamedTypes( + namedtype.NamedType('tbsCertificate', TBSCertificate()), + namedtype.NamedType('signatureAlgorithm', AlgorithmIdentifier()), + namedtype.NamedType('signatureValue', univ.BitString()) +) + + +class UnauthAttributes(univ.SetOf): + pass + + +UnauthAttributes.componentType = Attribute() +UnauthAttributes.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class ExtendedCertificateInfo(univ.Sequence): + pass + + +ExtendedCertificateInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', CMSVersion()), + namedtype.NamedType('certificate', Certificate()), + namedtype.NamedType('attributes', UnauthAttributes()) +) + + +class Signature(univ.BitString): + pass + + +class SignatureAlgorithmIdentifier(AlgorithmIdentifier): + pass + + +class ExtendedCertificate(univ.Sequence): + pass + + +ExtendedCertificate.componentType = namedtype.NamedTypes( + namedtype.NamedType('extendedCertificateInfo', ExtendedCertificateInfo()), + namedtype.NamedType('signatureAlgorithm', SignatureAlgorithmIdentifier()), + namedtype.NamedType('signature', Signature()) +) + + +class OtherCertificateFormat(univ.Sequence): + pass + + +OtherCertificateFormat.componentType = namedtype.NamedTypes( + namedtype.NamedType('otherCertFormat', univ.ObjectIdentifier()), + namedtype.NamedType('otherCert', univ.Any()) +) + + +class CertificateChoices(univ.Choice): + pass + + +CertificateChoices.componentType = namedtype.NamedTypes( + namedtype.NamedType('certificate', Certificate()), + namedtype.NamedType('extendedCertificate', ExtendedCertificate().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('v1AttrCert', AttributeCertificateV1().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))), + namedtype.NamedType('v2AttrCert', AttributeCertificateV2().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.NamedType('other', OtherCertificateFormat().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))) +) + + +class TBSCertList(univ.Sequence): + pass + + +TBSCertList.componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('version', Version()), + namedtype.NamedType('signature', AlgorithmIdentifier()), + namedtype.NamedType('issuer', Name()), + namedtype.NamedType('thisUpdate', Time()), + namedtype.OptionalNamedType('nextUpdate', Time()), + namedtype.OptionalNamedType('revokedCertificates', univ.SequenceOf(componentType=univ.Sequence(componentType=namedtype.NamedTypes( + namedtype.NamedType('userCertificate', CertificateSerialNumber()), + namedtype.NamedType('revocationDate', Time()), + namedtype.OptionalNamedType('crlEntryExtensions', Extensions()) + )) + )), + namedtype.OptionalNamedType('crlExtensions', Extensions().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class CertificateList(univ.Sequence): + pass + + +CertificateList.componentType = namedtype.NamedTypes( + namedtype.NamedType('tbsCertList', TBSCertList()), + namedtype.NamedType('signatureAlgorithm', AlgorithmIdentifier()), + namedtype.NamedType('signatureValue', univ.BitString()) +) + + +class CertificateRevocationList(univ.Sequence): + pass + + +CertificateRevocationList.componentType = namedtype.NamedTypes( + namedtype.NamedType('signature', AlgorithmIdentifier()), + namedtype.NamedType('issuer', Name()), + namedtype.NamedType('lastUpdate', useful.UTCTime()), + namedtype.NamedType('nextUpdate', useful.UTCTime()), + namedtype.OptionalNamedType('revokedCertificates', univ.SequenceOf(componentType=CRLEntry())) +) + + +class CertificateRevocationLists(univ.SetOf): + pass + + +CertificateRevocationLists.componentType = CertificateRevocationList() + + +class CertificateSet(univ.SetOf): + pass + + +CertificateSet.componentType = CertificateChoices() + + +class ChangePasswdDataMS(univ.Sequence): + pass + + +ChangePasswdDataMS.componentType = namedtype.NamedTypes( + namedtype.NamedType('newpasswd', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('targname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))), + namedtype.OptionalNamedType('targrealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))) +) + + +class ChecksumTypeValues(univ.Integer): + pass + + +ChecksumTypeValues.namedValues = namedval.NamedValues( + ('kRB5-CKSUMTYPE-NONE', 0), + ('kRB5-CKSUMTYPE-CRC32', 1), + ('kRB5-CKSUMTYPE-RSA-MD4', 2), + ('kRB5-CKSUMTYPE-RSA-MD4-DES', 3), + ('kRB5-CKSUMTYPE-DES-MAC', 4), + ('kRB5-CKSUMTYPE-DES-MAC-K', 5), + ('kRB5-CKSUMTYPE-RSA-MD4-DES-K', 6), + ('kRB5-CKSUMTYPE-RSA-MD5', 7), + ('kRB5-CKSUMTYPE-RSA-MD5-DES', 8), + ('kRB5-CKSUMTYPE-RSA-MD5-DES3', 9), + ('kRB5-CKSUMTYPE-SHA1-OTHER', 10), + ('kRB5-CKSUMTYPE-HMAC-SHA1-DES3', 12), + ('kRB5-CKSUMTYPE-SHA1', 14), + ('kRB5-CKSUMTYPE-HMAC-SHA1-96-AES-128', 15), + ('kRB5-CKSUMTYPE-HMAC-SHA1-96-AES-256', 16), + ('kRB5-CKSUMTYPE-GSSAPI', 32771), + ('kRB5-CKSUMTYPE-HMAC-MD5', -138), + ('kRB5-CKSUMTYPE-HMAC-MD5-ENC', -1138) +) + + +class ChecksumTypeSequence(univ.Sequence): + pass + + +ChecksumTypeSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', ChecksumTypeValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class ContentEncryptionAlgorithmIdentifier(AlgorithmIdentifier): + pass + + +class ContentType(univ.ObjectIdentifier): + pass + + +class ContentInfo(univ.Sequence): + pass + + +ContentInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('contentType', ContentType()), + namedtype.OptionalNamedType('content', univ.Any().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class DHPublicKey(univ.Integer): + pass + + +class DHRepInfo(univ.Sequence): + pass + + +DHRepInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('dhSignedData', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('serverDHNonce', DHNonce().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class DigestAlgorithmIdentifier(AlgorithmIdentifier): + pass + + +class DigestAlgorithmIdentifiers(univ.SetOf): + pass + + +DigestAlgorithmIdentifiers.componentType = DigestAlgorithmIdentifier() + + +class ValidationParms(univ.Sequence): + pass + + +ValidationParms.componentType = namedtype.NamedTypes( + namedtype.NamedType('seed', univ.BitString()), + namedtype.NamedType('pgenCounter', univ.Integer()) +) + + +class DomainParameters(univ.Sequence): + pass + + +DomainParameters.componentType = namedtype.NamedTypes( + namedtype.NamedType('p', univ.Integer()), + namedtype.NamedType('g', univ.Integer()), + namedtype.OptionalNamedType('q', univ.Integer()), + namedtype.OptionalNamedType('j', univ.Integer()), + namedtype.OptionalNamedType('validationParms', ValidationParms()) +) + + +class ETYPE_INFO_ENTRY(univ.Sequence): + pass + + +ETYPE_INFO_ENTRY.componentType = namedtype.NamedTypes( + namedtype.NamedType('etype', EncryptionType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('salt', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class ETYPE_INFO(univ.SequenceOf): + pass + + +ETYPE_INFO.componentType = ETYPE_INFO_ENTRY() + + +class ETYPE_INFO2_ENTRY(univ.Sequence): + pass + + +ETYPE_INFO2_ENTRY.componentType = namedtype.NamedTypes( + namedtype.NamedType('etype', EncryptionType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('salt', KerberosString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('s2kparams', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))) +) + + +class ETYPE_INFO2(univ.SequenceOf): + pass + + +ETYPE_INFO2.componentType = ETYPE_INFO2_ENTRY() +ETYPE_INFO2.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class EncAPRepPart(univ.Sequence): + pass + + +EncAPRepPart.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 27)) +EncAPRepPart.componentType = namedtype.NamedTypes( + namedtype.NamedType('ctime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('cusec', Microseconds().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('subkey', EncryptionKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.OptionalNamedType('seq-number', UInt32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))) +) + + +class LastReq(univ.SequenceOf): + pass + + +LastReq.componentType = univ.Sequence(componentType=namedtype.NamedTypes( + namedtype.NamedType('lr-type', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('lr-value', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +)) + + +class METHOD_DATA(univ.SequenceOf): + pass + + +METHOD_DATA.componentType = PA_DATA() + + +class TicketFlags(KerberosFlags): + pass + + +class EncKDCRepPart(univ.Sequence): + pass + + +EncKDCRepPart.componentType = namedtype.NamedTypes( + namedtype.NamedType('key', EncryptionKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('last-req', LastReq().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('nonce', UInt32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('key-expiration', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.NamedType('flags', TicketFlags().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))), + namedtype.NamedType('authtime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))), + namedtype.OptionalNamedType('starttime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))), + namedtype.NamedType('endtime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))), + namedtype.OptionalNamedType('renew-till', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 8))), + namedtype.NamedType('srealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 9))), + namedtype.NamedType('sname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 10))), + namedtype.OptionalNamedType('caddr', HostAddresses().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 11))), + namedtype.OptionalNamedType('encrypted-pa-data', METHOD_DATA().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 12))) +) + + +class EncASRepPart(EncKDCRepPart): + pass + + +EncASRepPart.tagSet = EncKDCRepPart.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 25)) + + +class KrbCredInfo(univ.Sequence): + pass + + +KrbCredInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('key', EncryptionKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.OptionalNamedType('prealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('pname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.OptionalNamedType('flags', TicketFlags().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.OptionalNamedType('authtime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))), + namedtype.OptionalNamedType('starttime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))), + namedtype.OptionalNamedType('endtime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))), + namedtype.OptionalNamedType('renew-till', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))), + namedtype.OptionalNamedType('srealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 8))), + namedtype.OptionalNamedType('sname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 9))), + namedtype.OptionalNamedType('caddr', HostAddresses().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 10))) +) + + +class EncKrbCredPart(univ.Sequence): + pass + + +EncKrbCredPart.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 29)) +EncKrbCredPart.componentType = namedtype.NamedTypes( + namedtype.NamedType('ticket-info', univ.SequenceOf(componentType=KrbCredInfo()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('nonce', UInt32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('timestamp', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('usec', Microseconds().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.OptionalNamedType('s-address', HostAddress().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))), + namedtype.OptionalNamedType('r-address', HostAddress().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 5))) +) + + +class EncKrbPrivPart(univ.Sequence): + pass + + +EncKrbPrivPart.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 28)) +EncKrbPrivPart.componentType = namedtype.NamedTypes( + namedtype.NamedType('user-data', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('timestamp', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('usec', Microseconds().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('seq-number', UInt32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.NamedType('s-address', HostAddress().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))), + namedtype.OptionalNamedType('r-address', HostAddress().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 5))) +) + + +class EncTGSRepPart(EncKDCRepPart): + pass + + +EncTGSRepPart.tagSet = EncKDCRepPart.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 26)) + + +class TransitedEncoding(univ.Sequence): + pass + + +TransitedEncoding.componentType = namedtype.NamedTypes( + namedtype.NamedType('tr-type', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('contents', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class EncTicketPart(univ.Sequence): + pass + + +EncTicketPart.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 3)) +EncTicketPart.componentType = namedtype.NamedTypes( + namedtype.NamedType('flags', TicketFlags().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('key', EncryptionKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))), + namedtype.NamedType('crealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.NamedType('cname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))), + namedtype.NamedType('transited', TransitedEncoding().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))), + namedtype.NamedType('authtime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))), + namedtype.OptionalNamedType('starttime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))), + namedtype.NamedType('endtime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))), + namedtype.OptionalNamedType('renew-till', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 8))), + namedtype.OptionalNamedType('caddr', HostAddresses().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 9))), + namedtype.OptionalNamedType('authorization-data', AuthorizationData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 10))) +) + + +class EncapsulatedContentInfo(univ.Sequence): + pass + + +EncapsulatedContentInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('eContentType', ContentType()), + namedtype.OptionalNamedType('eContent', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class EncryptedContent(univ.OctetString): + pass + + +class EncryptedContentInfo(univ.Sequence): + pass + + +EncryptedContentInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('contentType', ContentType()), + namedtype.NamedType('contentEncryptionAlgorithm', ContentEncryptionAlgorithmIdentifier()), + namedtype.OptionalNamedType('encryptedContent', EncryptedContent().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class EncryptedKey(univ.OctetString): + pass + + +class EncryptionTypeValues(univ.Integer): + pass + + +EncryptionTypeValues.namedValues = namedval.NamedValues( + ('kRB5-ENCTYPE-NULL', 0), + ('kRB5-ENCTYPE-DES-CBC-CRC', 1), + ('kRB5-ENCTYPE-DES-CBC-MD4', 2), + ('kRB5-ENCTYPE-DES-CBC-MD5', 3), + ('kRB5-ENCTYPE-DES3-CBC-MD5', 5), + ('kRB5-ENCTYPE-OLD-DES3-CBC-SHA1', 7), + ('kRB5-ENCTYPE-SIGN-DSA-GENERATE', 8), + ('kRB5-ENCTYPE-ENCRYPT-RSA-PRIV', 9), + ('kRB5-ENCTYPE-ENCRYPT-RSA-PUB', 10), + ('kRB5-ENCTYPE-DES3-CBC-SHA1', 16), + ('kRB5-ENCTYPE-AES128-CTS-HMAC-SHA1-96', 17), + ('kRB5-ENCTYPE-AES256-CTS-HMAC-SHA1-96', 18), + ('kRB5-ENCTYPE-ARCFOUR-HMAC-MD5', 23), + ('kRB5-ENCTYPE-ARCFOUR-HMAC-MD5-56', 24), + ('kRB5-ENCTYPE-ENCTYPE-PK-CROSS', 48), + ('kRB5-ENCTYPE-ARCFOUR-MD4', -128), + ('kRB5-ENCTYPE-ARCFOUR-HMAC-OLD', -133), + ('kRB5-ENCTYPE-ARCFOUR-HMAC-OLD-EXP', -135), + ('kRB5-ENCTYPE-DUMMY', -1111) +) + + +class EncryptionTypeSequence(univ.Sequence): + pass + + +EncryptionTypeSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', EncryptionTypeValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class OtherRevocationInfoFormat(univ.Sequence): + pass + + +OtherRevocationInfoFormat.componentType = namedtype.NamedTypes( + namedtype.NamedType('otherRevInfoFormat', univ.ObjectIdentifier()), + namedtype.NamedType('otherRevInfo', univ.Any()) +) + + +class RevocationInfoChoice(univ.Choice): + pass + + +RevocationInfoChoice.componentType = namedtype.NamedTypes( + namedtype.NamedType('crl', CertificateList()), + namedtype.NamedType('other', OtherRevocationInfoFormat().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))) +) + + +class RevocationInfoChoices(univ.SetOf): + pass + + +RevocationInfoChoices.componentType = RevocationInfoChoice() + + +class OriginatorInfo(univ.Sequence): + pass + + +OriginatorInfo.componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('certs', CertificateSet().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('crls', RevocationInfoChoices().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class OtherKeyAttribute(univ.Sequence): + pass + + +OtherKeyAttribute.componentType = namedtype.NamedTypes( + namedtype.NamedType('keyAttrId', univ.ObjectIdentifier()), + namedtype.OptionalNamedType('keyAttr', univ.Any()) +) + + +class KEKIdentifier(univ.Sequence): + pass + + +KEKIdentifier.componentType = namedtype.NamedTypes( + namedtype.NamedType('keyIdentifier', univ.OctetString()), + namedtype.OptionalNamedType('date', useful.GeneralizedTime()), + namedtype.OptionalNamedType('other', OtherKeyAttribute()) +) + + +class KeyEncryptionAlgorithmIdentifier(AlgorithmIdentifier): + pass + + +class KEKRecipientInfo(univ.Sequence): + pass + + +KEKRecipientInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', CMSVersion()), + namedtype.NamedType('kekid', KEKIdentifier()), + namedtype.NamedType('keyEncryptionAlgorithm', KeyEncryptionAlgorithmIdentifier()), + namedtype.NamedType('encryptedKey', EncryptedKey()) +) + + +class IssuerAndSerialNumber(univ.Sequence): + pass + + +IssuerAndSerialNumber.componentType = namedtype.NamedTypes( + namedtype.NamedType('issuer', Name()), + namedtype.NamedType('serialNumber', CertificateSerialNumber()) +) + + +class OriginatorPublicKey(univ.Sequence): + pass + + +OriginatorPublicKey.componentType = namedtype.NamedTypes( + namedtype.NamedType('algorithm', AlgorithmIdentifier()), + namedtype.NamedType('publicKey', univ.BitString()) +) + + +class SubjectKeyIdentifier(univ.OctetString): + pass + + +class OriginatorIdentifierOrKey(univ.Choice): + pass + + +OriginatorIdentifierOrKey.componentType = namedtype.NamedTypes( + namedtype.NamedType('issuerAndSerialNumber', IssuerAndSerialNumber()), + namedtype.NamedType('subjectKeyIdentifier', SubjectKeyIdentifier().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('originatorKey', OriginatorPublicKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))) +) + + +class RecipientKeyIdentifier(univ.Sequence): + pass + + +RecipientKeyIdentifier.componentType = namedtype.NamedTypes( + namedtype.NamedType('subjectKeyIdentifier', SubjectKeyIdentifier()), + namedtype.OptionalNamedType('date', useful.GeneralizedTime()), + namedtype.OptionalNamedType('other', OtherKeyAttribute()) +) + + +class KeyAgreeRecipientIdentifier(univ.Choice): + pass + + +KeyAgreeRecipientIdentifier.componentType = namedtype.NamedTypes( + namedtype.NamedType('issuerAndSerialNumber', IssuerAndSerialNumber()), + namedtype.NamedType('rKeyId', RecipientKeyIdentifier().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))) +) + + +class RecipientEncryptedKey(univ.Sequence): + pass + + +RecipientEncryptedKey.componentType = namedtype.NamedTypes( + namedtype.NamedType('rid', KeyAgreeRecipientIdentifier()), + namedtype.NamedType('encryptedKey', EncryptedKey()) +) + + +class RecipientEncryptedKeys(univ.SequenceOf): + pass + + +RecipientEncryptedKeys.componentType = RecipientEncryptedKey() + + +class UserKeyingMaterial(univ.OctetString): + pass + + +class KeyAgreeRecipientInfo(univ.Sequence): + pass + + +KeyAgreeRecipientInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', CMSVersion()), + namedtype.NamedType('originator', OriginatorIdentifierOrKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.OptionalNamedType('ukm', UserKeyingMaterial().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('keyEncryptionAlgorithm', KeyEncryptionAlgorithmIdentifier()), + namedtype.NamedType('recipientEncryptedKeys', RecipientEncryptedKeys()) +) + + +class RecipientIdentifier(univ.Choice): + pass + + +RecipientIdentifier.componentType = namedtype.NamedTypes( + namedtype.NamedType('issuerAndSerialNumber', IssuerAndSerialNumber()), + namedtype.NamedType('subjectKeyIdentifier', SubjectKeyIdentifier().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class KeyTransRecipientInfo(univ.Sequence): + pass + + +KeyTransRecipientInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', CMSVersion()), + namedtype.NamedType('rid', RecipientIdentifier()), + namedtype.NamedType('keyEncryptionAlgorithm', KeyEncryptionAlgorithmIdentifier()), + namedtype.NamedType('encryptedKey', EncryptedKey()) +) + + +class OtherRecipientInfo(univ.Sequence): + pass + + +OtherRecipientInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('oriType', univ.ObjectIdentifier()), + namedtype.NamedType('oriValue', univ.Any()) +) + + +class KeyDerivationAlgorithmIdentifier(AlgorithmIdentifier): + pass + + +class PasswordRecipientInfo(univ.Sequence): + pass + + +PasswordRecipientInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', CMSVersion()), + namedtype.OptionalNamedType('keyDerivationAlgorithm', KeyDerivationAlgorithmIdentifier().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('keyEncryptionAlgorithm', KeyEncryptionAlgorithmIdentifier()), + namedtype.NamedType('encryptedKey', EncryptedKey()) +) + + +class RecipientInfo(univ.Choice): + pass + + +RecipientInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('ktri', KeyTransRecipientInfo()), + namedtype.NamedType('kari', KeyAgreeRecipientInfo().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))), + namedtype.NamedType('kekri', KEKRecipientInfo().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.NamedType('pwri', PasswordRecipientInfo().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))), + namedtype.NamedType('ori', OtherRecipientInfo().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))) +) + + +class RecipientInfos(univ.SetOf): + pass + + +RecipientInfos.componentType = RecipientInfo() +RecipientInfos.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class UnprotectedAttributes(univ.SetOf): + pass + + +UnprotectedAttributes.componentType = Attribute() +UnprotectedAttributes.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class EnvelopedData(univ.Sequence): + pass + + +EnvelopedData.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', CMSVersion()), + namedtype.OptionalNamedType('originatorInfo', OriginatorInfo().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('recipientInfos', RecipientInfos()), + namedtype.NamedType('encryptedContentInfo', EncryptedContentInfo()), + namedtype.OptionalNamedType('unprotectedAttrs', UnprotectedAttributes().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class ExtendedCertificateOrCertificate(univ.Choice): + pass + + +ExtendedCertificateOrCertificate.componentType = namedtype.NamedTypes( + namedtype.NamedType('certificate', Certificate()), + namedtype.NamedType('extendedCertificate', ExtendedCertificate().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))) +) + + +class ExtendedCertificatesAndCertificates(univ.SetOf): + pass + + +ExtendedCertificatesAndCertificates.componentType = ExtendedCertificateOrCertificate() + + +class FastOptions(univ.BitString): + pass + + +FastOptions.namedValues = namedval.NamedValues( + ('reserved', 0), + ('hide-client-names', 1), + ('kdc-follow-referrals', 16) +) + + +class KDCDHKeyInfo(univ.Sequence): + pass + + +KDCDHKeyInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('subjectPublicKey', univ.BitString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('nonce', univ.Integer().subtype(subtypeSpec=constraint.ValueRangeConstraint(0, 4294967295)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('dhKeyExpiration', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))) +) + + +class KDCOptionsValues(univ.BitString): + pass + + +KDCOptionsValues.namedValues = namedval.NamedValues( + ('reserved', 0), + ('forwardable', 1), + ('forwarded', 2), + ('proxiable', 3), + ('proxy', 4), + ('allow-postdate', 5), + ('postdated', 6), + ('unused7', 7), + ('renewable', 8), + ('unused9', 9), + ('unused10', 10), + ('opt-hardware-auth', 11), + ('unused12', 12), + ('unused13', 13), + ('cname-in-addl-tkt', 14), + ('canonicalize', 15), + ('disable-transited-check', 26), + ('renewable-ok', 27), + ('enc-tkt-in-skey', 28), + ('renew', 30), + ('validate', 31) +) + + +class KDCOptionsSequence(univ.Sequence): + pass + + +KDCOptionsSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', KDCOptionsValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class KERB_AD_RESTRICTION_ENTRY(univ.Sequence): + pass + + +KERB_AD_RESTRICTION_ENTRY.componentType = namedtype.NamedTypes( + namedtype.NamedType('restriction-type', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('restriction', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class KerbErrorDataType(univ.Integer): + pass + + +class KERB_ERROR_DATA(univ.Sequence): + pass + + +KERB_ERROR_DATA.componentType = namedtype.NamedTypes( + namedtype.NamedType('data-type', KerbErrorDataType().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('data-value', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))) +) + + +class KERB_KEY_LIST_REP(univ.SequenceOf): + pass + + +KERB_KEY_LIST_REP.componentType = EncryptionKey() + + +class KERB_KEY_LIST_REQ(univ.SequenceOf): + pass + + +KERB_KEY_LIST_REQ.componentType = EncryptionType() + + +class KERB_LOCAL(univ.OctetString): + pass + + +class KERB_PA_PAC_REQUEST(univ.Sequence): + pass + + +KERB_PA_PAC_REQUEST.componentType = namedtype.NamedTypes( + namedtype.NamedType('include-pac', univ.Boolean().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class KRB_CRED(univ.Sequence): + pass + + +KRB_CRED.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 22)) +KRB_CRED.componentType = namedtype.NamedTypes( + namedtype.NamedType('pvno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('msg-type', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(22)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('tickets', univ.SequenceOf(componentType=Ticket()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.NamedType('enc-part', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))) +) + + +class KRB_ERROR(univ.Sequence): + pass + + +KRB_ERROR.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 30)) +KRB_ERROR.componentType = namedtype.NamedTypes( + namedtype.NamedType('pvno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('msg-type', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(30)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('ctime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('cusec', Microseconds().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.NamedType('stime', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))), + namedtype.NamedType('susec', Microseconds().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))), + namedtype.NamedType('error-code', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))), + namedtype.OptionalNamedType('crealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))), + namedtype.OptionalNamedType('cname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 8))), + namedtype.NamedType('realm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 9))), + namedtype.NamedType('sname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 10))), + namedtype.OptionalNamedType('e-text', KerberosString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 11))), + namedtype.OptionalNamedType('e-data', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 12))) +) + + +class KRB_PRIV(univ.Sequence): + pass + + +KRB_PRIV.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 21)) +KRB_PRIV.componentType = namedtype.NamedTypes( + namedtype.NamedType('pvno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('msg-type', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(21)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('enc-part', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))) +) + + +class KRB_SAFE_BODY(univ.Sequence): + pass + + +KRB_SAFE_BODY.componentType = namedtype.NamedTypes( + namedtype.NamedType('user-data', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('timestamp', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('usec', Microseconds().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('seq-number', UInt32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.NamedType('s-address', HostAddress().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))), + namedtype.OptionalNamedType('r-address', HostAddress().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 5))) +) + + +class KRB_SAFE(univ.Sequence): + pass + + +KRB_SAFE.tagSet = univ.Sequence.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 20)) +KRB_SAFE.componentType = namedtype.NamedTypes( + namedtype.NamedType('pvno', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(5)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('msg-type', univ.Integer().subtype(subtypeSpec=constraint.SingleValueConstraint(20)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('safe-body', KRB_SAFE_BODY().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.NamedType('cksum', Checksum().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))) +) + + +class KRB5PrincipalName(univ.Sequence): + pass + + +KRB5PrincipalName.componentType = namedtype.NamedTypes( + namedtype.NamedType('realm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('principalName', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))) +) + + +class KerbErrorDataTypeValues(univ.Integer): + pass + + +KerbErrorDataTypeValues.namedValues = namedval.NamedValues( + ('kERB-AP-ERR-TYPE-SKEW-RECOVERY', 2), + ('kERB-ERR-TYPE-EXTENDED', 3) +) + + +class KerbErrorDataTypeSequence(univ.Sequence): + pass + + +KerbErrorDataTypeSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', KerbErrorDataTypeValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class KrbFastArmor(univ.Sequence): + pass + + +KrbFastArmor.componentType = namedtype.NamedTypes( + namedtype.NamedType('armor-type', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('armor-value', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class KrbFastArmoredRep(univ.Sequence): + pass + + +KrbFastArmoredRep.componentType = namedtype.NamedTypes( + namedtype.NamedType('enc-fast-rep', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))) +) + + +class KrbFastArmoredReq(univ.Sequence): + pass + + +KrbFastArmoredReq.componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('armor', KrbFastArmor().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('req-checksum', Checksum().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))), + namedtype.NamedType('enc-fast-req', EncryptedData().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))) +) + + +class KrbFastFinished(univ.Sequence): + pass + + +KrbFastFinished.componentType = namedtype.NamedTypes( + namedtype.NamedType('timestamp', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('usec', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('crealm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.NamedType('cname', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 3))), + namedtype.NamedType('ticket-checksum', Checksum().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 4))) +) + + +class KrbFastReq(univ.Sequence): + pass + + +KrbFastReq.componentType = namedtype.NamedTypes( + namedtype.NamedType('fast-options', FastOptions().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('padata', univ.SequenceOf(componentType=PA_DATA()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('req-body', KDC_REQ_BODY().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))) +) + + +class KrbFastResponse(univ.Sequence): + pass + + +KrbFastResponse.componentType = namedtype.NamedTypes( + namedtype.NamedType('padata', univ.SequenceOf(componentType=PA_DATA()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('strengthen-key', EncryptionKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))), + namedtype.OptionalNamedType('finished', KrbFastFinished().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.NamedType('nonce', UInt32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))) +) + + +class MS_UPN_SAN(char.UTF8String): + pass + + +class MessageDigest(univ.OctetString): + pass + + +class MessageTypeValues(univ.Integer): + pass + + +MessageTypeValues.namedValues = namedval.NamedValues( + ('krb-as-req', 10), + ('krb-as-rep', 11), + ('krb-tgs-req', 12), + ('krb-tgs-rep', 13), + ('krb-ap-req', 14), + ('krb-ap-rep', 15), + ('krb-safe', 20), + ('krb-priv', 21), + ('krb-cred', 22), + ('krb-error', 30) +) + + +class MessageTypeSequence(univ.Sequence): + pass + + +MessageTypeSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', MessageTypeValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class NameTypeValues(univ.Integer): + pass + + +NameTypeValues.namedValues = namedval.NamedValues( + ('kRB5-NT-UNKNOWN', 0), + ('kRB5-NT-PRINCIPAL', 1), + ('kRB5-NT-SRV-INST', 2), + ('kRB5-NT-SRV-HST', 3), + ('kRB5-NT-SRV-XHST', 4), + ('kRB5-NT-UID', 5), + ('kRB5-NT-X500-PRINCIPAL', 6), + ('kRB5-NT-SMTP-NAME', 7), + ('kRB5-NT-ENTERPRISE-PRINCIPAL', 10), + ('kRB5-NT-WELLKNOWN', 11), + ('kRB5-NT-ENT-PRINCIPAL-AND-ID', -130), + ('kRB5-NT-MS-PRINCIPAL', -128), + ('kRB5-NT-MS-PRINCIPAL-AND-ID', -129) +) + + +class NameTypeSequence(univ.Sequence): + pass + + +NameTypeSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', NameTypeValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class PA_ENC_TIMESTAMP(EncryptedData): + pass + + +class PA_ENC_TS_ENC(univ.Sequence): + pass + + +PA_ENC_TS_ENC.componentType = namedtype.NamedTypes( + namedtype.NamedType('patimestamp', KerberosTime().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('pausec', Microseconds().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class PA_FX_FAST_REPLY(univ.Choice): + pass + + +PA_FX_FAST_REPLY.componentType = namedtype.NamedTypes( + namedtype.NamedType('armored-data', KrbFastArmoredRep().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))) +) + + +class PA_FX_FAST_REQUEST(univ.Choice): + pass + + +PA_FX_FAST_REQUEST.componentType = namedtype.NamedTypes( + namedtype.NamedType('armored-data', KrbFastArmoredReq().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))) +) + + +class PACOptionFlags(KerberosFlags): + pass + + +class PA_PAC_OPTIONS(univ.Sequence): + pass + + +PA_PAC_OPTIONS.componentType = namedtype.NamedTypes( + namedtype.NamedType('options', PACOptionFlags().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class PA_PK_AS_REP(univ.Choice): + pass + + +PA_PK_AS_REP.componentType = namedtype.NamedTypes( + namedtype.NamedType('dhInfo', DHRepInfo().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('encKeyPack', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class PA_PK_AS_REP_Win2k(univ.Choice): + pass + + +PA_PK_AS_REP_Win2k.componentType = namedtype.NamedTypes( + namedtype.NamedType('dhSignedData', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('encKeyPack', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class PA_PK_AS_REQ(univ.Sequence): + pass + + +PA_PK_AS_REQ.componentType = namedtype.NamedTypes( + namedtype.NamedType('signedAuthPack', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('trustedCertifiers', univ.SequenceOf(componentType=ExternalPrincipalIdentifier()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.OptionalNamedType('kdcPkId', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))) +) + + +class TrustedCA_Win2k(univ.Choice): + pass + + +TrustedCA_Win2k.componentType = namedtype.NamedTypes( + namedtype.NamedType('caName', univ.Any().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('issuerAndSerial', IssuerAndSerialNumber().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))) +) + + +class PA_PK_AS_REQ_Win2k(univ.Sequence): + pass + + +PA_PK_AS_REQ_Win2k.componentType = namedtype.NamedTypes( + namedtype.NamedType('signedAuthPack', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('trustedCertifiers', univ.SequenceOf(componentType=TrustedCA_Win2k()).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))), + namedtype.OptionalNamedType('kdcCert', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))), + namedtype.OptionalNamedType('encryptionCert', univ.OctetString().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))) +) + + +class PA_S4U2Self(univ.Sequence): + pass + + +PA_S4U2Self.componentType = namedtype.NamedTypes( + namedtype.NamedType('name', PrincipalName().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('realm', Realm().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('cksum', Checksum().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2))), + namedtype.NamedType('auth', KerberosString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))) +) + + +class PA_SUPPORTED_ENCTYPES(Int32): + pass + + +class PACOptionFlagsValues(univ.BitString): + pass + + +PACOptionFlagsValues.namedValues = namedval.NamedValues( + ('claims', 0), + ('branch-aware', 1), + ('forward-to-full-dc', 2), + ('resource-based-constrained-delegation', 3) +) + + +class PACOptionFlagsSequence(univ.Sequence): + pass + + +PACOptionFlagsSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', PACOptionFlagsValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class PADataTypeValues(univ.Integer): + pass + + +PADataTypeValues.namedValues = namedval.NamedValues( + ('kRB5-PADATA-NONE', 0), + ('kRB5-PADATA-KDC-REQ', 1), + ('kRB5-PADATA-ENC-TIMESTAMP', 2), + ('kRB5-PADATA-PW-SALT', 3), + ('kRB5-PADATA-ENC-UNIX-TIME', 5), + ('kRB5-PADATA-SANDIA-SECUREID', 6), + ('kRB5-PADATA-SESAME', 7), + ('kRB5-PADATA-OSF-DCE', 8), + ('kRB5-PADATA-CYBERSAFE-SECUREID', 9), + ('kRB5-PADATA-AFS3-SALT', 10), + ('kRB5-PADATA-ETYPE-INFO', 11), + ('kRB5-PADATA-SAM-CHALLENGE', 12), + ('kRB5-PADATA-SAM-RESPONSE', 13), + ('kRB5-PADATA-PK-AS-REQ-19', 14), + ('kRB5-PADATA-PK-AS-REP-19', 15), + ('kRB5-PADATA-PK-AS-REQ', 16), + ('kRB5-PADATA-PK-AS-REP', 17), + ('kRB5-PADATA-PA-PK-OCSP-RESPONSE', 18), + ('kRB5-PADATA-ETYPE-INFO2', 19), + ('kRB5-PADATA-SVR-REFERRAL-INFO', 20), + ('kRB5-PADATA-SAM-REDIRECT', 21), + ('kRB5-PADATA-GET-FROM-TYPED-DATA', 22), + ('kRB5-PADATA-SAM-ETYPE-INFO', 23), + ('kRB5-PADATA-SERVER-REFERRAL', 25), + ('kRB5-PADATA-ALT-PRINC', 24), + ('kRB5-PADATA-SAM-CHALLENGE2', 30), + ('kRB5-PADATA-SAM-RESPONSE2', 31), + ('kRB5-PA-EXTRA-TGT', 41), + ('kRB5-PADATA-TD-KRB-PRINCIPAL', 102), + ('kRB5-PADATA-PK-TD-TRUSTED-CERTIFIERS', 104), + ('kRB5-PADATA-PK-TD-CERTIFICATE-INDEX', 105), + ('kRB5-PADATA-TD-APP-DEFINED-ERROR', 106), + ('kRB5-PADATA-TD-REQ-NONCE', 107), + ('kRB5-PADATA-TD-REQ-SEQ', 108), + ('kRB5-PADATA-PA-PAC-REQUEST', 128), + ('kRB5-PADATA-FOR-USER', 129), + ('kRB5-PADATA-FOR-X509-USER', 130), + ('kRB5-PADATA-FOR-CHECK-DUPS', 131), + ('kRB5-PADATA-AS-CHECKSUM', 132), + ('kRB5-PADATA-FX-COOKIE', 133), + ('kRB5-PADATA-AUTHENTICATION-SET', 134), + ('kRB5-PADATA-AUTH-SET-SELECTED', 135), + ('kRB5-PADATA-FX-FAST', 136), + ('kRB5-PADATA-FX-ERROR', 137), + ('kRB5-PADATA-ENCRYPTED-CHALLENGE', 138), + ('kRB5-PADATA-OTP-CHALLENGE', 141), + ('kRB5-PADATA-OTP-REQUEST', 142), + ('kBB5-PADATA-OTP-CONFIRM', 143), + ('kRB5-PADATA-OTP-PIN-CHANGE', 144), + ('kRB5-PADATA-EPAK-AS-REQ', 145), + ('kRB5-PADATA-EPAK-AS-REP', 146), + ('kRB5-PADATA-PKINIT-KX', 147), + ('kRB5-PADATA-PKU2U-NAME', 148), + ('kRB5-PADATA-REQ-ENC-PA-REP', 149), + ('kRB5-PADATA-AS-FRESHNESS', 150), + ('kRB5-PADATA-SUPPORTED-ETYPES', 165), + ('kRB5-PADATA-PAC-OPTIONS', 167), + ('kRB5-PADATA-GSS', 655) +) + + +class PADataTypeSequence(univ.Sequence): + pass + + +PADataTypeSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', PADataTypeValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class ReplyKeyPack(univ.Sequence): + pass + + +ReplyKeyPack.componentType = namedtype.NamedTypes( + namedtype.NamedType('replyKey', EncryptionKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('asChecksum', Checksum().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1))) +) + + +class ReplyKeyPack_Win2k(univ.Sequence): + pass + + +ReplyKeyPack_Win2k.componentType = namedtype.NamedTypes( + namedtype.NamedType('replyKey', EncryptionKey().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))), + namedtype.NamedType('nonce', univ.Integer().subtype(subtypeSpec=constraint.ValueRangeConstraint(-2147483648, 2147483647)).subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class SignatureValue(univ.OctetString): + pass + + +class SignedAttributes(univ.SetOf): + pass + + +SignedAttributes.componentType = Attribute() +SignedAttributes.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class SignerIdentifier(univ.Choice): + pass + + +SignerIdentifier.componentType = namedtype.NamedTypes( + namedtype.NamedType('issuerAndSerialNumber', IssuerAndSerialNumber()), + namedtype.NamedType('subjectKeyIdentifier', SubjectKeyIdentifier().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +class UnsignedAttributes(univ.SetOf): + pass + + +UnsignedAttributes.componentType = Attribute() +UnsignedAttributes.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class SignerInfo(univ.Sequence): + pass + + +SignerInfo.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', CMSVersion()), + namedtype.NamedType('sid', SignerIdentifier()), + namedtype.NamedType('digestAlgorithm', DigestAlgorithmIdentifier()), + namedtype.OptionalNamedType('signedAttrs', SignedAttributes().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.NamedType('signatureAlgorithm', SignatureAlgorithmIdentifier()), + namedtype.NamedType('signature', SignatureValue()), + namedtype.OptionalNamedType('unsignedAttrs', UnsignedAttributes().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +) + + +class SignerInfos(univ.SetOf): + pass + + +SignerInfos.componentType = SignerInfo() + + +class SignedData(univ.Sequence): + pass + + +SignedData.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', CMSVersion()), + namedtype.NamedType('digestAlgorithms', DigestAlgorithmIdentifiers()), + namedtype.NamedType('encapContentInfo', EncapsulatedContentInfo()), + namedtype.OptionalNamedType('certificates', CertificateSet().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('crls', RevocationInfoChoices().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('signerInfos', SignerInfos()) +) + + +class Version_RFC2315(univ.Integer): + pass + + +class SignedData_RFC2315(univ.Sequence): + pass + + +SignedData_RFC2315.componentType = namedtype.NamedTypes( + namedtype.NamedType('version', Version_RFC2315()), + namedtype.NamedType('digestAlgorithms', DigestAlgorithmIdentifiers()), + namedtype.NamedType('contentInfo', ContentInfo()), + namedtype.OptionalNamedType('certificates', CertificateSet().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('crls', RevocationInfoChoices().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))), + namedtype.NamedType('signerInfos', SignerInfos()) +) + + +class SubjectAltName(GeneralNames): + pass + + +class TD_DH_PARAMETERS(univ.SequenceOf): + pass + + +TD_DH_PARAMETERS.componentType = AlgorithmIdentifier() + + +class TD_INVALID_CERTIFICATES(univ.SequenceOf): + pass + + +TD_INVALID_CERTIFICATES.componentType = ExternalPrincipalIdentifier() + + +class TD_TRUSTED_CERTIFIERS(univ.SequenceOf): + pass + + +TD_TRUSTED_CERTIFIERS.componentType = ExternalPrincipalIdentifier() + + +class TGS_REP(KDC_REP): + pass + + +TGS_REP.tagSet = KDC_REP.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 13)) + + +class TGS_REQ(KDC_REQ): + pass + + +TGS_REQ.tagSet = KDC_REQ.tagSet.tagExplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 12)) + + +class TYPED_DATA(univ.SequenceOf): + pass + + +TYPED_DATA.componentType = univ.Sequence(componentType=namedtype.NamedTypes( + namedtype.NamedType('data-type', Int32().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))), + namedtype.OptionalNamedType('data-value', univ.OctetString().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))) +)) + +TYPED_DATA.subtypeSpec=constraint.ValueSizeConstraint(1, 256) + + +class TicketFlagsValues(univ.BitString): + pass + + +TicketFlagsValues.namedValues = namedval.NamedValues( + ('reserved', 0), + ('forwardable', 1), + ('forwarded', 2), + ('proxiable', 3), + ('proxy', 4), + ('may-postdate', 5), + ('postdated', 6), + ('invalid', 7), + ('renewable', 8), + ('initial', 9), + ('pre-authent', 10), + ('hw-authent', 11), + ('transited-policy-checked', 12), + ('ok-as-delegate', 13), + ('enc-pa-rep', 15) +) + + +class TicketFlagsSequence(univ.Sequence): + pass + + +TicketFlagsSequence.componentType = namedtype.NamedTypes( + namedtype.NamedType('dummy', TicketFlagsValues().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))) +) + + +nistAlgorithms = _OID(2, 16, 840, 1, 101, 3, 4) + + +aes = _OID(nistAlgorithms, 1) + + +aes256_CBC_PAD = _OID(aes, 42) + + +rsadsi = _OID(1, 2, 840, 113549) + + +encryptionAlgorithm = _OID(rsadsi, 3) + + +des_EDE3_CBC = _OID(encryptionAlgorithm, 7) + + +dhpublicnumber = _OID(1, 2, 840, 10046, 2, 1) + + +id_ce = _OID(2, 5, 29) + + +id_ce_subjectAltName = _OID(id_ce, 17) + + +id_contentType = _OID(1, 2, 840, 113549, 1, 9, 3) + + +id_data = _OID(1, 2, 840, 113549, 1, 7, 1) + + +id_envelopedData = _OID(1, 2, 840, 113549, 1, 7, 3) + + +id_krb5 = _OID(1, 3, 6, 1, 5, 2) + + +id_messageDigest = _OID(1, 2, 840, 113549, 1, 9, 4) + + +id_pkcs1_sha256WithRSAEncryption = _OID(1, 2, 840, 113549, 1, 1, 11) + + +id_pkinit = _OID(1, 3, 6, 1, 5, 2, 3) + + +id_pkinit_DHKeyData = _OID(id_pkinit, 2) + + +id_pkinit_authData = _OID(id_pkinit, 1) + + +id_pkinit_ms_san = _OID(1, 3, 6, 1, 4, 1, 311, 20, 2, 3) + + +id_pkinit_rkeyData = _OID(id_pkinit, 3) + + +id_sha1 = _OID(1, 3, 14, 3, 2, 26) + + +id_sha512 = _OID(2, 16, 840, 1, 101, 3, 4, 2, 3) + + +id_signedData = _OID(1, 2, 840, 113549, 1, 7, 2) + + +kdc_authentication = _OID(id_pkinit, 5) + + +md2 = _OID(1, 2, 840, 113549, 2, 2) + + +md5 = _OID(1, 2, 840, 113549, 2, 5) + + +rsaEncryption = _OID(1, 2, 840, 113549, 1, 1, 1) + + +sha1WithRSAEncryption = _OID(1, 2, 840, 113549, 1, 1, 5) + + +smartcard_logon = _OID(1, 3, 6, 1, 4, 1, 311, 20, 2, 2) + + +szOID_NTDS_CA_SECURITY_EXT = _OID(1, 3, 6, 1, 4, 1, 311, 25, 2) + + +szOID_NTDS_OBJECTSID = _OID(1, 3, 6, 1, 4, 1, 311, 25, 2, 1) + + +ub_common_name = univ.Integer(64) + + +ub_common_name_length = univ.Integer(64) + + +ub_e163_4_number_length = univ.Integer(15) + + +ub_e163_4_sub_address_length = univ.Integer(40) + + +ub_emailaddress_length = univ.Integer(255) + + +ub_integer_options = univ.Integer(256) + + +ub_locality_name = univ.Integer(128) + + +ub_match = univ.Integer(128) + + +ub_name = univ.Integer(32768) + + +ub_organization_name = univ.Integer(64) + + +ub_organizational_unit_name = univ.Integer(64) + + +ub_pds_name_length = univ.Integer(16) + + +ub_pds_parameter_length = univ.Integer(30) + + +ub_pds_physical_address_lines = univ.Integer(6) + + +ub_postal_code_length = univ.Integer(16) + + +ub_pseudonym = univ.Integer(128) + + +ub_serial_number = univ.Integer(64) + + +ub_state_name = univ.Integer(128) + + +ub_title = univ.Integer(64) + + +ub_unformatted_address_length = univ.Integer(180) + + diff --git a/python/samba/tests/krb5/rodc_tests.py b/python/samba/tests/krb5/rodc_tests.py new file mode 100755 index 0000000..71ef603 --- /dev/null +++ b/python/samba/tests/krb5/rodc_tests.py @@ -0,0 +1,77 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +class RodcKerberosTests(KDCBaseTest): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + # Ensure that an RODC correctly issues tickets signed with its krbtgt key + # and including the RODCIdentifier. + def test_rodc_ticket_signature(self): + user_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts={ + 'allowed_replication': True, + 'revealed_to_rodc': True + }) + target_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'allowed_replication': True, + 'revealed_to_rodc': True + }) + + krbtgt_creds = self.get_rodc_krbtgt_creds() + rodc_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + # Get a TGT from the RODC. + tgt = self.get_tgt(user_creds, to_rodc=True) + + # Ensure the PAC contains the expected checksums. + self.verify_ticket(tgt, rodc_key, service_ticket=False) + + # Get a service ticket from the RODC. + service_ticket = self.get_service_ticket(tgt, target_creds, + to_rodc=True) + + # Ensure the PAC contains the expected checksums. + self.verify_ticket(service_ticket, rodc_key, service_ticket=True, + expect_ticket_checksum=True, + expect_full_checksum=True) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/s4u_tests.py b/python/samba/tests/krb5/s4u_tests.py new file mode 100755 index 0000000..b91c412 --- /dev/null +++ b/python/samba/tests/krb5/s4u_tests.py @@ -0,0 +1,1838 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import functools +import time + +from samba import dsdb, ntstatus +from samba.dcerpc import krb5pac, lsa, security + +from samba.tests import env_get_var_value +from samba.tests.krb5.kcrypto import Cksumtype, Enctype +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.raw_testcase import ( + RawKerberosTest, + RodcPacEncryptionKey, + ZeroedChecksumKey +) +from samba.tests.krb5.rfc4120_constants import ( + AD_IF_RELEVANT, + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_BADMATCH, + KDC_ERR_BADOPTION, + KDC_ERR_BAD_INTEGRITY, + KDC_ERR_GENERIC, + KDC_ERR_INAPP_CKSUM, + KDC_ERR_MODIFIED, + KDC_ERR_SUMTYPE_NOSUPP, + KDC_ERR_TGT_REVOKED, + KU_AS_REP_ENC_PART, + KU_PA_ENC_TIMESTAMP, + KU_TGS_REP_ENC_PART_SUB_KEY, + KU_TGS_REQ_AUTH_DAT_SESSION, + KU_TGS_REQ_AUTH_DAT_SUBKEY, + NT_PRINCIPAL, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +SidType = RawKerberosTest.SidType + +global_asn1_print = False +global_hexdump = False + + +class S4UKerberosTests(KDCBaseTest): + + default_attrs = security.SE_GROUP_DEFAULT_FLAGS + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _test_s4u2self(self, pa_s4u2self_ctype=None): + service_creds = self.get_service_creds() + service = service_creds.get_username() + realm = service_creds.get_realm() + + cname = self.PrincipalName_create(name_type=1, names=[service]) + sname = self.PrincipalName_create(name_type=2, names=["krbtgt", realm]) + + till = self.get_KerberosTime(offset=36000) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + padata = None + + etypes = (18, 17, 23) + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + self.assertEqual(rep['msg-type'], 30) + self.assertEqual(rep['error-code'], 25) + rep_padata = self.der_decode( + rep['e-data'], asn1Spec=krb5_asn1.METHOD_DATA()) + + for pa in rep_padata: + if pa['padata-type'] == 19: + etype_info2 = pa['padata-value'] + break + + etype_info2 = self.der_decode( + etype_info2, asn1Spec=krb5_asn1.ETYPE_INFO2()) + + key = self.PasswordKey_from_etype_info2(service_creds, etype_info2[0]) + + (patime, pausec) = self.get_KerberosTimeWithUsec() + pa_ts = self.PA_ENC_TS_ENC_create(patime, pausec) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + pa_ts = self.EncryptedData_create(key, KU_PA_ENC_TIMESTAMP, pa_ts) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.EncryptedData()) + + pa_ts = self.PA_DATA_create(2, pa_ts) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + padata = [pa_ts] + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + msg_type = rep['msg-type'] + self.assertEqual(msg_type, 11) + + enc_part2 = key.decrypt(KU_AS_REP_ENC_PART, rep['enc-part']['cipher']) + # MIT KDC encodes both EncASRepPart and EncTGSRepPart with + # application tag 26 + try: + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncASRepPart()) + except Exception: + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncTGSRepPart()) + + # S4U2Self Request + sname = cname + + for_user_name = env_get_var_value('FOR_USER') + uname = self.PrincipalName_create(name_type=1, names=[for_user_name]) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + till = self.get_KerberosTime(offset=36000) + ticket = rep['ticket'] + ticket_session_key = self.EncryptionKey_import(enc_part2['key']) + pa_s4u = self.PA_S4U2Self_create(name=uname, realm=realm, + tgt_session_key=ticket_session_key, + ctype=pa_s4u2self_ctype) + padata = [pa_s4u] + + subkey = self.RandomKey(ticket_session_key.etype) + + (ctime, cusec) = self.get_KerberosTimeWithUsec() + + req = self.TGS_REQ_create(padata=padata, + cusec=cusec, + ctime=ctime, + ticket=ticket, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7ffffffe, + etypes=etypes, + addresses=None, + EncAuthorizationData=None, + EncAuthorizationData_key=None, + additional_tickets=None, + ticket_session_key=ticket_session_key, + authenticator_subkey=subkey) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + msg_type = rep['msg-type'] + if msg_type == 13: + enc_part2 = subkey.decrypt( + KU_TGS_REP_ENC_PART_SUB_KEY, rep['enc-part']['cipher']) + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncTGSRepPart()) + + return msg_type + + # Using the checksum type from the tgt_session_key happens to work + # everywhere + def test_s4u2self(self): + msg_type = self._test_s4u2self() + self.assertEqual(msg_type, 13) + + # Per spec, the checksum of PA-FOR-USER is HMAC_MD5, see [MS-SFU] 2.2.1 + def test_s4u2self_hmac_md5_checksum(self): + msg_type = self._test_s4u2self(pa_s4u2self_ctype=Cksumtype.HMAC_MD5) + self.assertEqual(msg_type, 13) + + def test_s4u2self_md5_unkeyed_checksum(self): + msg_type = self._test_s4u2self(pa_s4u2self_ctype=Cksumtype.MD5) + self.assertEqual(msg_type, 30) + + def test_s4u2self_sha1_unkeyed_checksum(self): + msg_type = self._test_s4u2self(pa_s4u2self_ctype=Cksumtype.SHA1) + self.assertEqual(msg_type, 30) + + def test_s4u2self_crc32_unkeyed_checksum(self): + msg_type = self._test_s4u2self(pa_s4u2self_ctype=Cksumtype.CRC32) + self.assertEqual(msg_type, 30) + + def _run_s4u2self_test(self, kdc_dict): + client_opts = kdc_dict.pop('client_opts', None) + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts=client_opts) + + service_opts = kdc_dict.pop('service_opts', None) + service_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts=service_opts) + + service_tgt = self.get_tgt(service_creds) + modify_service_tgt_fn = kdc_dict.pop('modify_service_tgt_fn', None) + if modify_service_tgt_fn is not None: + service_tgt = modify_service_tgt_fn(service_tgt) + + client_name = client_creds.get_username() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_name]) + + service_name = kdc_dict.pop('service_name', None) + if service_name is None: + service_name = service_creds.get_username()[:-1] + service_sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=['host', service_name]) + + realm = client_creds.get_realm() + + expected_flags = kdc_dict.pop('expected_flags', None) + if expected_flags is not None: + expected_flags = krb5_asn1.TicketFlags(expected_flags) + + unexpected_flags = kdc_dict.pop('unexpected_flags', None) + if unexpected_flags is not None: + unexpected_flags = krb5_asn1.TicketFlags(unexpected_flags) + + expected_error_mode = kdc_dict.pop('expected_error_mode', 0) + expect_status = kdc_dict.pop('expect_status', None) + expected_status = kdc_dict.pop('expected_status', None) + if expected_error_mode: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + self.assertIsNone(expect_status) + self.assertIsNone(expected_status) + + kdc_options = kdc_dict.pop('kdc_options', '0') + kdc_options = krb5_asn1.KDCOptions(kdc_options) + + service_decryption_key = self.TicketDecryptionKey_from_creds( + service_creds) + + authenticator_subkey = self.RandomKey(Enctype.AES256) + + etypes = kdc_dict.pop('etypes', (AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5)) + + expect_edata = kdc_dict.pop('expect_edata', None) + expected_groups = kdc_dict.pop('expected_groups', None) + unexpected_groups = kdc_dict.pop('unexpected_groups', None) + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + pa_s4u = self.PA_S4U2Self_create( + name=client_cname, + realm=realm, + tgt_session_key=service_tgt.session_key, + ctype=None) + + return [pa_s4u], req_body + + kdc_exchange_dict = self.tgs_exchange_dict( + expected_crealm=realm, + expected_cname=client_cname, + expected_srealm=realm, + expected_sname=service_sname, + expected_account_name=client_name, + expected_groups=expected_groups, + unexpected_groups=unexpected_groups, + expected_sid=client_creds.get_sid(), + expected_flags=expected_flags, + unexpected_flags=unexpected_flags, + ticket_decryption_key=service_decryption_key, + expect_ticket_checksum=True, + generate_padata_fn=generate_s4u2self_padata, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error_mode, + expect_status=expect_status, + expected_status=expected_status, + tgt=service_tgt, + authenticator_subkey=authenticator_subkey, + kdc_options=str(kdc_options), + expect_edata=expect_edata) + + self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=realm, + sname=service_sname, + etypes=etypes) + + if not expected_error_mode: + # Check that the ticket contains a PAC. + ticket = kdc_exchange_dict['rep_ticket_creds'] + + pac = self.get_ticket_pac(ticket) + self.assertIsNotNone(pac) + + # Ensure we used all the parameters given to us. + self.assertEqual({}, kdc_dict) + + # Test performing an S4U2Self operation with a forwardable ticket. The + # resulting ticket should have the 'forwardable' flag set. + def test_s4u2self_forwardable(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True), + 'expected_flags': 'forwardable' + }) + + # Test performing an S4U2Self operation with a forwardable ticket that does + # not contain a PAC. The request should fail. + def test_s4u2self_no_pac(self): + def forwardable_no_pac(ticket): + ticket = self.set_ticket_forwardable(ticket, flag=True) + return self.remove_ticket_pac(ticket) + + self._run_s4u2self_test( + { + 'expected_error_mode': KDC_ERR_TGT_REVOKED, + 'client_opts': { + 'not_delegated': False + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': forwardable_no_pac, + 'expected_flags': 'forwardable', + 'expect_edata': False + }) + + # Test performing an S4U2Self operation without requesting a forwardable + # ticket. The resulting ticket should not have the 'forwardable' flag set. + def test_s4u2self_without_forwardable(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True), + 'unexpected_flags': 'forwardable' + }) + + # Do an S4U2Self with a non-forwardable TGT. The 'forwardable' flag should + # not be set on the ticket. + def test_s4u2self_not_forwardable(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=False), + 'unexpected_flags': 'forwardable' + }) + + # Do an S4U2Self with the not_delegated flag set on the client. The + # 'forwardable' flag should not be set on the ticket. + def test_s4u2self_client_not_delegated(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': True + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True), + 'unexpected_flags': 'forwardable' + }) + + # Do an S4U2Self with a service not trusted to authenticate for delegation, + # but having an empty msDS-AllowedToDelegateTo attribute. The 'forwardable' + # flag should be set on the ticket. + def test_s4u2self_not_trusted_empty_allowed(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'service_opts': { + 'trusted_to_auth_for_delegation': False, + 'delegation_to_spn': () + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True), + 'expected_flags': 'forwardable' + }) + + # Do an S4U2Self with a service not trusted to authenticate for delegation + # and having a non-empty msDS-AllowedToDelegateTo attribute. The + # 'forwardable' flag should not be set on the ticket. + def test_s4u2self_not_trusted_nonempty_allowed(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'service_opts': { + 'trusted_to_auth_for_delegation': False, + 'delegation_to_spn': ('test',) + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True), + 'unexpected_flags': 'forwardable' + }) + + # Do an S4U2Self with a service trusted to authenticate for delegation and + # having an empty msDS-AllowedToDelegateTo attribute. The 'forwardable' + # flag should be set on the ticket. + def test_s4u2self_trusted_empty_allowed(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'service_opts': { + 'trusted_to_auth_for_delegation': True, + 'delegation_to_spn': () + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True), + 'expected_flags': 'forwardable' + }) + + # Do an S4U2Self with a service trusted to authenticate for delegation and + # having a non-empty msDS-AllowedToDelegateTo attribute. The 'forwardable' + # flag should be set on the ticket. + def test_s4u2self_trusted_nonempty_allowed(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'service_opts': { + 'trusted_to_auth_for_delegation': True, + 'delegation_to_spn': ('test',) + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True), + 'expected_flags': 'forwardable' + }) + + # Do an S4U2Self with the sname in the request different to that of the + # service. We expect an error. + def test_s4u2self_wrong_sname(self): + other_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts={ + 'trusted_to_auth_for_delegation': True, + 'id': 0 + }) + other_sname = other_creds.get_username()[:-1] + + self._run_s4u2self_test( + { + 'expected_error_mode': KDC_ERR_BADMATCH, + 'expect_edata': False, + 'client_opts': { + 'not_delegated': False + }, + 'service_opts': { + 'trusted_to_auth_for_delegation': True + }, + 'service_name': other_sname, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True) + }) + + # Do an S4U2Self where the service does not require authorization data. The + # resulting ticket should still contain a PAC. + def test_s4u2self_no_auth_data_required(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'service_opts': { + 'trusted_to_auth_for_delegation': True, + 'no_auth_data_required': True + }, + 'kdc_options': 'forwardable', + 'modify_service_tgt_fn': functools.partial( + self.set_ticket_forwardable, flag=True), + 'expected_flags': 'forwardable' + }) + + # Do an S4U2Self and check that the service asserted identity is part of + # the sids. + def test_s4u2self_asserted_identity(self): + self._run_s4u2self_test( + { + 'client_opts': { + 'not_delegated': False + }, + 'expected_groups': { + (security.SID_SERVICE_ASSERTED_IDENTITY, + SidType.EXTRA_SID, + self.default_attrs), + ... + }, + 'unexpected_groups': { + security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, + }, + }) + + def _run_delegation_test(self, kdc_dict): + s4u2self = kdc_dict.pop('s4u2self', False) + + authtime_delay = kdc_dict.pop('authtime_delay', 0) + + client_opts = kdc_dict.pop('client_opts', None) + client_creds = self.get_cached_creds( + account_type=self.AccountType.USER, + opts=client_opts) + + sid = client_creds.get_sid() + + service1_opts = kdc_dict.pop('service1_opts', {}) + service2_opts = kdc_dict.pop('service2_opts', {}) + + allow_delegation = kdc_dict.pop('allow_delegation', False) + allow_rbcd = kdc_dict.pop('allow_rbcd', False) + self.assertFalse(allow_delegation and allow_rbcd) + + if allow_rbcd: + service1_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts=service1_opts) + + self.assertNotIn('delegation_from_dn', service2_opts) + service2_opts['delegation_from_dn'] = str(service1_creds.get_dn()) + + service2_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts=service2_opts) + else: + service2_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts=service2_opts) + + if allow_delegation: + self.assertNotIn('delegation_to_spn', service1_opts) + service1_opts['delegation_to_spn'] = service2_creds.get_spn() + + service1_creds = self.get_cached_creds( + account_type=self.AccountType.COMPUTER, + opts=service1_opts) + + service1_tgt = self.get_tgt(service1_creds) + self.assertElementPresent(service1_tgt.ticket_private, 'authtime') + service1_tgt_authtime = self.getElementValue(service1_tgt.ticket_private, 'authtime') + + client_username = client_creds.get_username() + client_realm = client_creds.get_realm() + client_cname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=[client_username]) + + service1_name = service1_creds.get_username()[:-1] + service1_realm = service1_creds.get_realm() + service1_service = 'host' + service1_sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[service1_service, + service1_name]) + service1_decryption_key = self.TicketDecryptionKey_from_creds( + service1_creds) + + expect_pac = kdc_dict.pop('expect_pac', True) + + expected_groups = kdc_dict.pop('expected_groups', None) + unexpected_groups = kdc_dict.pop('unexpected_groups', None) + + client_tkt_options = kdc_dict.pop('client_tkt_options', 'forwardable') + expected_flags = krb5_asn1.TicketFlags(client_tkt_options) + + etypes = kdc_dict.pop('etypes', (AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5)) + + if s4u2self: + self.assertEqual(authtime_delay, 0) + + def generate_s4u2self_padata(_kdc_exchange_dict, + _callback_dict, + req_body): + pa_s4u = self.PA_S4U2Self_create( + name=client_cname, + realm=client_realm, + tgt_session_key=service1_tgt.session_key, + ctype=None) + + return [pa_s4u], req_body + + s4u2self_expected_flags = krb5_asn1.TicketFlags('forwardable') + s4u2self_unexpected_flags = krb5_asn1.TicketFlags('0') + + s4u2self_kdc_options = krb5_asn1.KDCOptions('forwardable') + + s4u2self_authenticator_subkey = self.RandomKey(Enctype.AES256) + s4u2self_kdc_exchange_dict = self.tgs_exchange_dict( + expected_crealm=client_realm, + expected_cname=client_cname, + expected_srealm=service1_realm, + expected_sname=service1_sname, + expected_account_name=client_username, + expected_groups=expected_groups, + unexpected_groups=unexpected_groups, + expected_sid=sid, + expected_flags=s4u2self_expected_flags, + unexpected_flags=s4u2self_unexpected_flags, + ticket_decryption_key=service1_decryption_key, + generate_padata_fn=generate_s4u2self_padata, + check_rep_fn=self.generic_check_kdc_rep, + check_kdc_private_fn=self.generic_check_kdc_private, + tgt=service1_tgt, + authenticator_subkey=s4u2self_authenticator_subkey, + kdc_options=str(s4u2self_kdc_options), + expect_edata=False) + + self._generic_kdc_exchange(s4u2self_kdc_exchange_dict, + cname=None, + realm=service1_realm, + sname=service1_sname, + etypes=etypes) + + client_service_tkt = s4u2self_kdc_exchange_dict['rep_ticket_creds'] + else: + if authtime_delay != 0: + time.sleep(authtime_delay) + fresh = True + else: + fresh = False + + client_tgt = self.get_tgt(client_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags, + fresh=fresh) + client_service_tkt = self.get_service_ticket( + client_tgt, + service1_creds, + kdc_options=client_tkt_options, + expected_flags=expected_flags, + fresh=fresh) + + modify_client_tkt_fn = kdc_dict.pop('modify_client_tkt_fn', None) + if modify_client_tkt_fn is not None: + client_service_tkt = modify_client_tkt_fn(client_service_tkt) + + self.assertElementPresent(client_service_tkt.ticket_private, 'authtime') + expected_authtime = self.getElementValue(client_service_tkt.ticket_private, 'authtime') + if authtime_delay > 1: + self.assertNotEqual(expected_authtime, service1_tgt_authtime) + + additional_tickets = [client_service_tkt.ticket] + + modify_service_tgt_fn = kdc_dict.pop('modify_service_tgt_fn', None) + if modify_service_tgt_fn is not None: + service1_tgt = modify_service_tgt_fn(service1_tgt) + + kdc_options = kdc_dict.pop('kdc_options', None) + if kdc_options is None: + kdc_options = str(krb5_asn1.KDCOptions('cname-in-addl-tkt')) + + service2_name = service2_creds.get_username()[:-1] + service2_realm = service2_creds.get_realm() + service2_service = 'host' + service2_sname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=[service2_service, + service2_name]) + service2_decryption_key = self.TicketDecryptionKey_from_creds( + service2_creds) + service2_etypes = service2_creds.tgs_supported_enctypes + + expected_error_mode = kdc_dict.pop('expected_error_mode') + expect_status = kdc_dict.pop('expect_status', None) + expected_status = kdc_dict.pop('expected_status', None) + if expected_error_mode: + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + self.assertIsNone(expect_status) + self.assertIsNone(expected_status) + + expect_edata = kdc_dict.pop('expect_edata', None) + if expect_edata is not None: + self.assertTrue(expected_error_mode) + + pac_options = kdc_dict.pop('pac_options', None) + + use_authenticator_subkey = kdc_dict.pop('use_authenticator_subkey', True) + if use_authenticator_subkey: + authenticator_subkey = self.RandomKey(Enctype.AES256) + else: + authenticator_subkey = None + + expected_proxy_target = service2_creds.get_spn() + + expected_transited_services = kdc_dict.pop( + 'expected_transited_services', []) + + transited_service = f'host/{service1_name}@{service1_realm}' + expected_transited_services.append(transited_service) + + kdc_exchange_dict = self.tgs_exchange_dict( + expected_crealm=client_realm, + expected_cname=client_cname, + expected_srealm=service2_realm, + expected_sname=service2_sname, + expected_account_name=client_username, + expected_groups=expected_groups, + unexpected_groups=unexpected_groups, + expected_sid=sid, + expected_supported_etypes=service2_etypes, + ticket_decryption_key=service2_decryption_key, + check_error_fn=check_error_fn, + check_rep_fn=check_rep_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error_mode, + expect_status=expect_status, + expected_status=expected_status, + callback_dict={}, + tgt=service1_tgt, + authenticator_subkey=authenticator_subkey, + kdc_options=kdc_options, + pac_options=pac_options, + expect_edata=expect_edata, + expected_proxy_target=expected_proxy_target, + expected_transited_services=expected_transited_services, + expect_pac=expect_pac) + + EncAuthorizationData = kdc_dict.pop('enc-authorization-data', None) + + if EncAuthorizationData is not None: + if authenticator_subkey is not None: + EncAuthorizationData_key = authenticator_subkey + EncAuthorizationData_usage = KU_TGS_REQ_AUTH_DAT_SUBKEY + else: + EncAuthorizationData_key = client_service_tkt.session_key + EncAuthorizationData_usage = KU_TGS_REQ_AUTH_DAT_SESSION + else: + EncAuthorizationData_key = None + EncAuthorizationData_usage = None + + self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=service2_realm, + sname=service2_sname, + etypes=etypes, + additional_tickets=additional_tickets, + EncAuthorizationData=EncAuthorizationData, + EncAuthorizationData_key=EncAuthorizationData_key, + EncAuthorizationData_usage=EncAuthorizationData_usage) + + if not expected_error_mode: + # Check whether the ticket contains a PAC. + ticket = kdc_exchange_dict['rep_ticket_creds'] + self.assertElementEqual(ticket.ticket_private, 'authtime', expected_authtime) + pac = self.get_ticket_pac(ticket, expect_pac=expect_pac) + ticket_auth_data = ticket.ticket_private.get('authorization-data') + expected_num_ticket_auth_data = 0 + if expect_pac: + self.assertIsNotNone(pac) + expected_num_ticket_auth_data += 1 + else: + self.assertIsNone(pac) + + if EncAuthorizationData is not None: + expected_num_ticket_auth_data += len(EncAuthorizationData) + + if expected_num_ticket_auth_data == 0: + self.assertIsNone(ticket_auth_data) + else: + self.assertIsNotNone(ticket_auth_data) + self.assertEqual(len(ticket_auth_data), + expected_num_ticket_auth_data) + + if EncAuthorizationData is not None: + enc_ad_plain = self.der_encode( + EncAuthorizationData, + asn1Spec=krb5_asn1.AuthorizationData()) + req_EncAuthorizationData = self.der_decode( + enc_ad_plain, + asn1Spec=krb5_asn1.AuthorizationData()) + + rep_EncAuthorizationData = ticket_auth_data.copy() + if expect_pac: + rep_EncAuthorizationData.pop(0) + self.assertEqual(rep_EncAuthorizationData, req_EncAuthorizationData) + + # Ensure we used all the parameters given to us. + self.assertEqual({}, kdc_dict) + + def skip_unless_fl2008(self): + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level < dsdb.DS_DOMAIN_FUNCTION_2008: + self.skipTest('RBCD requires FL2008') + + def test_constrained_delegation(self): + # Test constrained delegation. + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_delegation': True + }) + + def test_constrained_delegation_authtime(self): + # Test constrained delegation. + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_delegation': True, + 'authtime_delay': 2, + }) + + def test_constrained_delegation_with_enc_auth_data_subkey(self): + # Test constrained delegation. + EncAuthorizationData = [] + relevant_elems = [] + auth_data777 = self.AuthorizationData_create(777, b'AuthorizationData777') + relevant_elems.append(auth_data777) + auth_data999 = self.AuthorizationData_create(999, b'AuthorizationData999') + relevant_elems.append(auth_data999) + ad_relevant = self.der_encode(relevant_elems, asn1Spec=krb5_asn1.AD_IF_RELEVANT()) + ad_data = self.AuthorizationData_create(AD_IF_RELEVANT, ad_relevant) + EncAuthorizationData.append(ad_data) + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_delegation': True, + 'enc-authorization-data': EncAuthorizationData, + }) + + def test_constrained_delegation_with_enc_auth_data_no_subkey(self): + # Test constrained delegation. + EncAuthorizationData = [] + relevant_elems = [] + auth_data777 = self.AuthorizationData_create(777, b'AuthorizationData777') + relevant_elems.append(auth_data777) + auth_data999 = self.AuthorizationData_create(999, b'AuthorizationData999') + relevant_elems.append(auth_data999) + ad_relevant = self.der_encode(relevant_elems, asn1Spec=krb5_asn1.AD_IF_RELEVANT()) + ad_data = self.AuthorizationData_create(AD_IF_RELEVANT, ad_relevant) + EncAuthorizationData.append(ad_data) + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_delegation': True, + 'enc-authorization-data': EncAuthorizationData, + 'use_authenticator_subkey': False, + }) + + def test_constrained_delegation_authentication_asserted_identity(self): + # Test constrained delegation and check asserted identity is the + # authentication authority. Note that we should always find this + # SID for all the requests. Just S4U2Self will have a different SID. + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_delegation': True, + 'expected_groups': { + (security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, + SidType.EXTRA_SID, + self.default_attrs), + ... + }, + 'unexpected_groups': { + security.SID_SERVICE_ASSERTED_IDENTITY, + }, + }) + + def test_constrained_delegation_service_asserted_identity(self): + # Test constrained delegation and check asserted identity is the + # service sid is there. This is a S4U2Proxy + S4U2Self test. + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_delegation': True, + 's4u2self': True, + 'service1_opts': { + 'trusted_to_auth_for_delegation': True, + }, + 'expected_groups': { + (security.SID_SERVICE_ASSERTED_IDENTITY, + SidType.EXTRA_SID, + self.default_attrs), + ... + }, + 'unexpected_groups': { + security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY, + }, + }) + + def test_constrained_delegation_no_auth_data_required(self): + # Test constrained delegation. + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_delegation': True, + 'service2_opts': { + 'no_auth_data_required': True + }, + 'expect_pac': False + }) + + def test_constrained_delegation_existing_delegation_info(self): + # Test constrained delegation with an existing S4U_DELEGATION_INFO + # structure in the PAC. + + services = ['service1', 'service2', 'service3'] + + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_delegation': True, + 'modify_client_tkt_fn': functools.partial( + self.add_delegation_info, services=services), + 'expected_transited_services': services + }) + + def test_constrained_delegation_not_allowed(self): + # Test constrained delegation when the delegating service does not + # allow it. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_BADOPTION, + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_delegation': False + }) + + def test_constrained_delegation_no_client_pac(self): + # Test constrained delegation when the client service ticket does not + # contain a PAC. + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_TGT_REVOKED), + 'allow_delegation': True, + 'modify_client_tkt_fn': self.remove_ticket_pac, + 'expect_edata': False + }) + + def test_constrained_delegation_no_service_pac(self): + # Test constrained delegation when the service TGT does not contain a + # PAC. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_TGT_REVOKED, + 'allow_delegation': True, + 'modify_service_tgt_fn': self.remove_ticket_pac, + 'expect_edata': False + }) + + def test_constrained_delegation_no_client_pac_no_auth_data_required(self): + # Test constrained delegation when the client service ticket does not + # contain a PAC. + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_BADOPTION, + KDC_ERR_TGT_REVOKED), + 'allow_delegation': True, + 'modify_client_tkt_fn': self.remove_ticket_pac, + 'expect_edata': False, + 'service2_opts': { + 'no_auth_data_required': True + } + }) + + def test_constrained_delegation_no_service_pac_no_auth_data_required(self): + # Test constrained delegation when the service TGT does not contain a + # PAC. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_TGT_REVOKED, + 'allow_delegation': True, + 'modify_service_tgt_fn': self.remove_ticket_pac, + 'service2_opts': { + 'no_auth_data_required': True + }, + 'expect_pac': False, + 'expect_edata': False + }) + + def test_constrained_delegation_non_forwardable(self): + # Test constrained delegation with a non-forwardable ticket. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_BADOPTION, + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + 'allow_delegation': True, + 'modify_client_tkt_fn': functools.partial( + self.set_ticket_forwardable, flag=False) + }) + + def test_constrained_delegation_pac_options_rbcd(self): + # Test constrained delegation, but with the RBCD bit set in the PAC + # options. + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'pac_options': '0001', # supports RBCD + 'allow_delegation': True + }) + + def test_rbcd(self): + # Test resource-based constrained delegation. + self.skip_unless_fl2008() + + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + }) + + def test_rbcd_no_auth_data_required(self): + self.skip_unless_fl2008() + + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'service2_opts': { + 'no_auth_data_required': True + }, + 'expect_pac': False + }) + + def test_rbcd_existing_delegation_info(self): + self.skip_unless_fl2008() + + # Test constrained delegation with an existing S4U_DELEGATION_INFO + # structure in the PAC. + + services = ['service1', 'service2', 'service3'] + + self._run_delegation_test( + { + 'expected_error_mode': 0, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': functools.partial( + self.add_delegation_info, services=services), + 'expected_transited_services': services + }) + + def test_rbcd_not_allowed(self): + # Test resource-based constrained delegation when the target service + # does not allow it. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_BADOPTION, + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NOT_FOUND, + 'allow_rbcd': False, + 'pac_options': '0001' # supports RBCD + }) + + def test_rbcd_no_client_pac_a(self): + self.skip_unless_fl2008() + + # Test constrained delegation when the client service ticket does not + # contain a PAC, and an empty msDS-AllowedToDelegateTo attribute. + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_TGT_REVOKED), + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': self.remove_ticket_pac + }) + + def test_rbcd_no_client_pac_b(self): + self.skip_unless_fl2008() + + # Test constrained delegation when the client service ticket does not + # contain a PAC, and a non-empty msDS-AllowedToDelegateTo attribute. + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_TGT_REVOKED), + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NO_MATCH, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': self.remove_ticket_pac, + 'service1_opts': { + 'delegation_to_spn': ('host/test') + } + }) + + def test_rbcd_no_service_pac(self): + self.skip_unless_fl2008() + + # Test constrained delegation when the service TGT does not contain a + # PAC. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_TGT_REVOKED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_service_tgt_fn': self.remove_ticket_pac, + 'expect_edata': False + }) + + def test_rbcd_no_client_pac_no_auth_data_required_a(self): + self.skip_unless_fl2008() + + # Test constrained delegation when the client service ticket does not + # contain a PAC, and an empty msDS-AllowedToDelegateTo attribute. + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_TGT_REVOKED), + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': self.remove_ticket_pac, + 'service2_opts': { + 'no_auth_data_required': True + } + }) + + def test_rbcd_no_client_pac_no_auth_data_required_b(self): + self.skip_unless_fl2008() + + # Test constrained delegation when the client service ticket does not + # contain a PAC, and a non-empty msDS-AllowedToDelegateTo attribute. + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_TGT_REVOKED), + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NO_MATCH, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': self.remove_ticket_pac, + 'service1_opts': { + 'delegation_to_spn': ('host/test') + }, + 'service2_opts': { + 'no_auth_data_required': True + } + }) + + def test_rbcd_no_service_pac_no_auth_data_required(self): + self.skip_unless_fl2008() + + # Test constrained delegation when the service TGT does not contain a + # PAC. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_TGT_REVOKED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_service_tgt_fn': self.remove_ticket_pac, + 'service2_opts': { + 'no_auth_data_required': True + }, + 'expect_edata': False + }) + + def test_rbcd_non_forwardable(self): + self.skip_unless_fl2008() + + # Test resource-based constrained delegation with a non-forwardable + # ticket. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_BADOPTION, + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_ACCOUNT_RESTRICTION, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': functools.partial( + self.set_ticket_forwardable, flag=False) + }) + + def test_rbcd_no_pac_options_a(self): + self.skip_unless_fl2008() + + # Test resource-based constrained delegation without the RBCD bit set + # in the PAC options, and an empty msDS-AllowedToDelegateTo attribute. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_BADOPTION, + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_rbcd': True, + 'pac_options': '1' # does not support RBCD + }) + + def test_rbcd_no_pac_options_b(self): + self.skip_unless_fl2008() + + # Test resource-based constrained delegation without the RBCD bit set + # in the PAC options, and a non-empty msDS-AllowedToDelegateTo + # attribute. + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_BADOPTION, + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NO_MATCH, + 'allow_rbcd': True, + 'pac_options': '1', # does not support RBCD + 'service1_opts': { + 'delegation_to_spn': ('host/test') + } + }) + + def test_bronze_bit_constrained_delegation_old_checksum(self): + # Attempt to modify the ticket without updating the PAC checksums. + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_BAD_INTEGRITY), + 'allow_delegation': True, + 'client_tkt_options': '0', # non-forwardable ticket + 'modify_client_tkt_fn': functools.partial( + self.set_ticket_forwardable, + flag=True, update_pac_checksums=False), + 'expect_edata': False + }) + + def test_bronze_bit_rbcd_old_checksum(self): + self.skip_unless_fl2008() + + # Attempt to modify the ticket without updating the PAC checksums. + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_BAD_INTEGRITY), + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'client_tkt_options': '0', # non-forwardable ticket + 'modify_client_tkt_fn': functools.partial( + self.set_ticket_forwardable, + flag=True, update_pac_checksums=False) + }) + + def test_constrained_delegation_missing_client_checksum(self): + # Present a user ticket without the required checksums. + for checksum in self.pac_checksum_types: + with self.subTest(checksum=checksum): + if checksum == krb5pac.PAC_TYPE_TICKET_CHECKSUM: + expected_error_mode = (KDC_ERR_MODIFIED, + KDC_ERR_BADOPTION) + else: + expected_error_mode = KDC_ERR_GENERIC + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + 'allow_delegation': True, + 'modify_client_tkt_fn': functools.partial( + self.remove_pac_checksum, checksum=checksum), + 'expect_edata': False + }) + + def test_constrained_delegation_missing_service_checksum(self): + # Present the service's ticket without the required checksums. + for checksum in (krb5pac.PAC_TYPE_SRV_CHECKSUM, + krb5pac.PAC_TYPE_KDC_CHECKSUM): + with self.subTest(checksum=checksum): + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_GENERIC, + # We aren’t particular about whether or not we get an + # NTSTATUS. + 'expect_status': None, + 'expected_status': + ntstatus.NT_STATUS_INSUFFICIENT_RESOURCES, + 'allow_delegation': True, + 'modify_service_tgt_fn': functools.partial( + self.remove_pac_checksum, checksum=checksum) + }) + + def test_rbcd_missing_client_checksum(self): + self.skip_unless_fl2008() + + # Present a user ticket without the required checksums. + for checksum in self.pac_checksum_types: + with self.subTest(checksum=checksum): + if checksum == krb5pac.PAC_TYPE_TICKET_CHECKSUM: + expected_error_mode = (KDC_ERR_MODIFIED, + KDC_ERR_BADOPTION) + else: + expected_error_mode = KDC_ERR_GENERIC + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + # We aren’t particular about whether or not we get an + # NTSTATUS. + 'expect_status': None, + 'expected_status': + ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': functools.partial( + self.remove_pac_checksum, checksum=checksum) + }) + + def test_rbcd_missing_service_checksum(self): + self.skip_unless_fl2008() + + # Present the service's ticket without the required checksums. + for checksum in (krb5pac.PAC_TYPE_SRV_CHECKSUM, + krb5pac.PAC_TYPE_KDC_CHECKSUM): + with self.subTest(checksum=checksum): + self._run_delegation_test( + { + 'expected_error_mode': KDC_ERR_GENERIC, + # We aren’t particular about whether or not we get an + # NTSTATUS. + 'expect_status': None, + 'expected_status': + ntstatus.NT_STATUS_INSUFFICIENT_RESOURCES, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_service_tgt_fn': functools.partial( + self.remove_pac_checksum, checksum=checksum) + }) + + def test_constrained_delegation_zeroed_client_checksum(self): + # Present a user ticket with invalid checksums. + for checksum in self.pac_checksum_types: + with self.subTest(checksum=checksum): + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_BAD_INTEGRITY), + 'allow_delegation': True, + 'modify_client_tkt_fn': functools.partial( + self.zeroed_pac_checksum, checksum=checksum), + 'expect_edata': False + }) + + def test_constrained_delegation_zeroed_service_checksum(self): + # Present the service's ticket with invalid checksums. + for checksum in self.pac_checksum_types: + with self.subTest(checksum=checksum): + if checksum == krb5pac.PAC_TYPE_SRV_CHECKSUM: + expected_error_mode = (KDC_ERR_MODIFIED, + KDC_ERR_BAD_INTEGRITY) + # We aren’t particular about whether or not we get an + # NTSTATUS. + expect_status = None + expected_status = ntstatus.NT_STATUS_WRONG_PASSWORD + else: + expected_error_mode = 0 + expect_status = None + expected_status = None + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + 'expect_status': expect_status, + 'expected_status': expected_status, + 'allow_delegation': True, + 'modify_service_tgt_fn': functools.partial( + self.zeroed_pac_checksum, checksum=checksum) + }) + + def test_rbcd_zeroed_client_checksum(self): + self.skip_unless_fl2008() + + # Present a user ticket with invalid checksums. + for checksum in self.pac_checksum_types: + with self.subTest(checksum=checksum): + self._run_delegation_test( + { + 'expected_error_mode': (KDC_ERR_MODIFIED, + KDC_ERR_BAD_INTEGRITY), + # We aren’t particular about whether or not we get an + # NTSTATUS. + 'expect_status': None, + 'expected_status': + ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': functools.partial( + self.zeroed_pac_checksum, checksum=checksum) + }) + + def test_rbcd_zeroed_service_checksum(self): + self.skip_unless_fl2008() + + # Present the service's ticket with invalid checksums. + for checksum in self.pac_checksum_types: + with self.subTest(checksum=checksum): + if checksum == krb5pac.PAC_TYPE_SRV_CHECKSUM: + expected_error_mode = (KDC_ERR_MODIFIED, + KDC_ERR_BAD_INTEGRITY) + # We aren’t particular about whether or not we get an + # NTSTATUS. + expect_status = None + expected_status = ntstatus.NT_STATUS_WRONG_PASSWORD + else: + expected_error_mode = 0 + expect_status = None + expected_status = None + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + 'expect_status': expect_status, + 'expected_status': expected_status, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_service_tgt_fn': functools.partial( + self.zeroed_pac_checksum, checksum=checksum) + }) + + unkeyed_ctypes = {Cksumtype.MD5, Cksumtype.SHA1, Cksumtype.CRC32} + + def test_constrained_delegation_unkeyed_client_checksum(self): + # Present a user ticket with invalid checksums. + for checksum in self.pac_checksum_types: + for ctype in self.unkeyed_ctypes: + with self.subTest(checksum=checksum, ctype=ctype): + if (checksum == krb5pac.PAC_TYPE_SRV_CHECKSUM + and ctype == Cksumtype.SHA1): + expected_error_mode = (KDC_ERR_SUMTYPE_NOSUPP, + KDC_ERR_INAPP_CKSUM) + else: + expected_error_mode = (KDC_ERR_GENERIC, + KDC_ERR_INAPP_CKSUM) + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + 'allow_delegation': True, + 'modify_client_tkt_fn': functools.partial( + self.unkeyed_pac_checksum, + checksum=checksum, ctype=ctype), + 'expect_edata': False + }) + + def test_constrained_delegation_unkeyed_service_checksum(self): + # Present the service's ticket with invalid checksums. + for checksum in self.pac_checksum_types: + for ctype in self.unkeyed_ctypes: + with self.subTest(checksum=checksum, ctype=ctype): + if checksum == krb5pac.PAC_TYPE_SRV_CHECKSUM: + # We aren’t particular about whether or not we get an + # NTSTATUS. + expect_status = None + if ctype == Cksumtype.SHA1: + expected_error_mode = (KDC_ERR_SUMTYPE_NOSUPP, + KDC_ERR_INAPP_CKSUM) + expected_status = ntstatus.NT_STATUS_LOGON_FAILURE + else: + expected_error_mode = (KDC_ERR_GENERIC, + KDC_ERR_INAPP_CKSUM) + expected_status = ( + ntstatus.NT_STATUS_INSUFFICIENT_RESOURCES) + else: + expected_error_mode = 0 + expect_status = None + expected_status = None + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + 'expect_status': expect_status, + 'expected_status': expected_status, + 'allow_delegation': True, + 'modify_service_tgt_fn': functools.partial( + self.unkeyed_pac_checksum, + checksum=checksum, ctype=ctype) + }) + + def test_rbcd_unkeyed_client_checksum(self): + self.skip_unless_fl2008() + + # Present a user ticket with invalid checksums. + for checksum in self.pac_checksum_types: + for ctype in self.unkeyed_ctypes: + with self.subTest(checksum=checksum, ctype=ctype): + if (checksum == krb5pac.PAC_TYPE_SRV_CHECKSUM + and ctype == Cksumtype.SHA1): + expected_error_mode = (KDC_ERR_SUMTYPE_NOSUPP, + KDC_ERR_INAPP_CKSUM) + else: + expected_error_mode = (KDC_ERR_GENERIC, + KDC_ERR_INAPP_CKSUM) + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + # We aren’t particular about whether or not we get + # an NTSTATUS. + 'expect_status': None, + 'expected_status': + ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': functools.partial( + self.unkeyed_pac_checksum, + checksum=checksum, ctype=ctype) + }) + + def test_rbcd_unkeyed_service_checksum(self): + self.skip_unless_fl2008() + + # Present the service's ticket with invalid checksums. + for checksum in self.pac_checksum_types: + for ctype in self.unkeyed_ctypes: + with self.subTest(checksum=checksum, ctype=ctype): + if checksum == krb5pac.PAC_TYPE_SRV_CHECKSUM: + # We aren’t particular about whether or not we get an + # NTSTATUS. + expect_status = None + if ctype == Cksumtype.SHA1: + expected_error_mode = (KDC_ERR_SUMTYPE_NOSUPP, + KDC_ERR_INAPP_CKSUM) + expected_status = ntstatus.NT_STATUS_LOGON_FAILURE + else: + expected_error_mode = (KDC_ERR_GENERIC, + KDC_ERR_INAPP_CKSUM) + expected_status = ( + ntstatus.NT_STATUS_INSUFFICIENT_RESOURCES) + else: + expected_error_mode = 0 + expect_status = None + expected_status = None + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + 'expect_status': expect_status, + 'expected_status': expected_status, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_service_tgt_fn': functools.partial( + self.unkeyed_pac_checksum, + checksum=checksum, ctype=ctype) + }) + + def test_constrained_delegation_rc4_client_checksum(self): + # Present a user ticket with RC4 checksums. + samdb = self.get_samdb() + functional_level = self.get_domain_functional_level(samdb) + + if functional_level >= dsdb.DS_DOMAIN_FUNCTION_2008: + expected_error_mode = (KDC_ERR_GENERIC, + KDC_ERR_INAPP_CKSUM) + expect_edata = False + else: + expected_error_mode = 0 + expect_edata = None + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + 'allow_delegation': True, + 'modify_client_tkt_fn': self.rc4_pac_checksums, + 'expect_edata': expect_edata, + }) + + def test_rbcd_rc4_client_checksum(self): + self.skip_unless_fl2008() + + # Present a user ticket with RC4 checksums. + expected_error_mode = (KDC_ERR_GENERIC, + KDC_ERR_BADOPTION) + + self._run_delegation_test( + { + 'expected_error_mode': expected_error_mode, + # We aren’t particular about whether or not we get an NTSTATUS. + 'expect_status': None, + 'expected_status': ntstatus.NT_STATUS_NOT_SUPPORTED, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + 'modify_client_tkt_fn': self.rc4_pac_checksums, + }) + + def test_constrained_delegation_rodc_issued(self): + self._run_delegation_test( + { + # Test that RODC-issued constrained delegation tickets are + # accepted. + 'expected_error_mode': 0, + 'allow_delegation': True, + # Both tickets must be signed by the same RODC. + 'modify_client_tkt_fn': self.signed_by_rodc, + 'modify_service_tgt_fn': self.issued_by_rodc, + 'client_opts': { + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + }, + 'service1_opts': { + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + }, + }) + + def test_rbcd_rodc_issued(self): + self.skip_unless_fl2008() + + self._run_delegation_test( + { + # Test that RODC-issued constrained delegation tickets are + # accepted. + 'expected_error_mode': 0, + 'allow_rbcd': True, + 'pac_options': '0001', # supports RBCD + # Both tickets must be signed by the same RODC. + 'modify_client_tkt_fn': self.signed_by_rodc, + 'modify_service_tgt_fn': self.issued_by_rodc, + 'client_opts': { + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + }, + 'service1_opts': { + 'allowed_replication_mock': True, + 'revealed_to_mock_rodc': True, + }, + }) + + def remove_pac_checksum(self, ticket, checksum): + checksum_keys = self.get_krbtgt_checksum_key() + + return self.modified_ticket(ticket, + checksum_keys=checksum_keys, + include_checksums={checksum: False}) + + def zeroed_pac_checksum(self, ticket, checksum): + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + server_key = ticket.decryption_key + + checksum_keys = { + krb5pac.PAC_TYPE_SRV_CHECKSUM: server_key, + krb5pac.PAC_TYPE_KDC_CHECKSUM: krbtgt_key, + krb5pac.PAC_TYPE_TICKET_CHECKSUM: krbtgt_key, + } + + if checksum == krb5pac.PAC_TYPE_SRV_CHECKSUM: + zeroed_key = server_key + else: + zeroed_key = krbtgt_key + + checksum_keys[checksum] = ZeroedChecksumKey(zeroed_key.key, + zeroed_key.kvno) + + return self.modified_ticket(ticket, + checksum_keys=checksum_keys, + include_checksums={checksum: True}) + + def unkeyed_pac_checksum(self, ticket, checksum, ctype): + krbtgt_creds = self.get_krbtgt_creds() + krbtgt_key = self.TicketDecryptionKey_from_creds(krbtgt_creds) + + server_key = ticket.decryption_key + + checksum_keys = { + krb5pac.PAC_TYPE_SRV_CHECKSUM: server_key, + krb5pac.PAC_TYPE_KDC_CHECKSUM: krbtgt_key, + krb5pac.PAC_TYPE_TICKET_CHECKSUM: krbtgt_key, + krb5pac.PAC_TYPE_FULL_CHECKSUM: krbtgt_key, + } + + # Make a copy of the existing key and change the ctype. + key = checksum_keys[checksum] + new_key = RodcPacEncryptionKey(key.key, key.kvno) + new_key.ctype = ctype + checksum_keys[checksum] = new_key + + return self.modified_ticket(ticket, + checksum_keys=checksum_keys, + include_checksums={checksum: True}) + + def rc4_pac_checksums(self, ticket): + krbtgt_creds = self.get_krbtgt_creds() + rc4_krbtgt_key = self.TicketDecryptionKey_from_creds( + krbtgt_creds, etype=Enctype.RC4) + + server_key = ticket.decryption_key + + checksum_keys = { + krb5pac.PAC_TYPE_SRV_CHECKSUM: server_key, + krb5pac.PAC_TYPE_KDC_CHECKSUM: rc4_krbtgt_key, + krb5pac.PAC_TYPE_TICKET_CHECKSUM: rc4_krbtgt_key, + krb5pac.PAC_TYPE_FULL_CHECKSUM: rc4_krbtgt_key, + } + + include_checksums = { + krb5pac.PAC_TYPE_SRV_CHECKSUM: True, + krb5pac.PAC_TYPE_KDC_CHECKSUM: True, + krb5pac.PAC_TYPE_TICKET_CHECKSUM: True, + krb5pac.PAC_TYPE_FULL_CHECKSUM: True, + } + + return self.modified_ticket(ticket, + checksum_keys=checksum_keys, + include_checksums=include_checksums) + + def add_delegation_info(self, ticket, *, services): + def modify_pac_fn(pac): + pac_buffers = pac.buffers + self.assertNotIn(krb5pac.PAC_TYPE_CONSTRAINED_DELEGATION, + (buffer.type for buffer in pac_buffers)) + + transited_services = list(map(lsa.String, services)) + + delegation = krb5pac.PAC_CONSTRAINED_DELEGATION() + delegation.proxy_target = lsa.String('test_proxy_target') + delegation.transited_services = transited_services + delegation.num_transited_services = len(transited_services) + + info = krb5pac.PAC_CONSTRAINED_DELEGATION_CTR() + info.info = delegation + + pac_buffer = krb5pac.PAC_BUFFER() + pac_buffer.type = krb5pac.PAC_TYPE_CONSTRAINED_DELEGATION + pac_buffer.info = info + + pac_buffers.append(pac_buffer) + + pac.buffers = pac_buffers + pac.num_buffers += 1 + + return pac + + checksum_keys = self.get_krbtgt_checksum_key() + + return self.modified_ticket(ticket, + checksum_keys=checksum_keys, + modify_pac_fn=modify_pac_fn) + + def set_ticket_forwardable(self, ticket, flag, update_pac_checksums=True): + modify_fn = functools.partial(self.modify_ticket_flag, + flag='forwardable', + value=flag) + + if update_pac_checksums: + checksum_keys = self.get_krbtgt_checksum_key() + else: + checksum_keys = None + + return self.modified_ticket(ticket, + modify_fn=modify_fn, + checksum_keys=checksum_keys, + update_pac_checksums=update_pac_checksums) + + def remove_ticket_pac(self, ticket): + return self.modified_ticket(ticket, + exclude_pac=True) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/salt_tests.py b/python/samba/tests/krb5/salt_tests.py new file mode 100755 index 0000000..fcda533 --- /dev/null +++ b/python/samba/tests/krb5/salt_tests.py @@ -0,0 +1,469 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import ldb + +from samba.tests.krb5.as_req_tests import AsReqBaseTest +import samba.tests.krb5.kcrypto as kcrypto + +global_asn1_print = False +global_hexdump = False + + +class SaltTests(AsReqBaseTest): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _get_creds(self, *, + account_type, + opts=None): + try: + return self.get_cached_creds( + account_type=account_type, + opts=opts) + except ldb.LdbError: + self.fail() + + def _run_salt_test(self, client_creds): + expected_salt = self.get_salt(client_creds) + self.assertIsNotNone(expected_salt) + + etype_info2 = self._run_as_req_enc_timestamp(client_creds) + + self.assertEqual(etype_info2[0]['etype'], kcrypto.Enctype.AES256) + self.assertEqual(etype_info2[0]['salt'], expected_salt) + + def test_salt_at_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'name_suffix': 'foo@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_suffix': 'foo@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_suffix': 'foo@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_case_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'name_suffix': 'Foo@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_case_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_suffix': 'Foo@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_case_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_suffix': 'Foo@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_double_at_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'name_suffix': 'foo@@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_double_at_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_suffix': 'foo@@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_double_at_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_suffix': 'foo@@bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_start_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'name_prefix': '@foo'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_start_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_prefix': '@foo'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_start_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_prefix': '@foo'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_end_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'name_suffix': 'foo@'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_end_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_suffix': 'foo@'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_end_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_suffix': 'foo@', + 'add_dollar': True}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_end_no_dollar_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_suffix': 'foo@', + 'add_dollar': False}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_at_end_add_dollar_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_suffix': 'foo@', + 'add_dollar': True}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_no_dollar_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'add_dollar': False}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_add_dollar_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'add_dollar': True}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_dollar_mid_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_suffix': 'foo$bar', + 'add_dollar': False}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_dollar_mid_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_suffix': 'foo$bar', + 'add_dollar': True}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_dollar_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'name_suffix': 'foo$bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_dollar_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_suffix': 'foo$bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_dollar_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_suffix': 'foo$bar'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_dollar_end_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'name_suffix': 'foo$'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_dollar_end_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'name_suffix': 'foo$'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_dollar_end_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'name_suffix': 'foo$'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'foo0'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'foo1'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'foo24'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'host/foo2'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'host/foo3'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'host/foo25'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_realm_user(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'foo4@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_realm_mac(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'foo5@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_realm_managed_service(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'foo26@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_realm_user(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'host/foo6@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_realm_mac(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'host/foo7@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_realm_managed_service(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'host/foo27@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_dollar_realm_user(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'foo8$@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_dollar_realm_mac(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'foo9$@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_dollar_realm_managed_service(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'foo28$@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_dollar_realm_user(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'host/foo10$@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_dollar_realm_mac(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'host/foo11$@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_dollar_realm_managed_service(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'host/foo29$@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_other_realm_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'foo12@other.realm'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_other_realm_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'foo13@other.realm'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_other_realm_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'foo30@other.realm'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_other_realm_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'host/foo14@other.realm'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_other_realm_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'host/foo15@other.realm'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_other_realm_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'host/foo31@other.realm'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_case_user(self): + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'Foo16'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_case_mac(self): + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'Foo17'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_case_managed_service(self): + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'Foo32'}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_dollar_mid_realm_user(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'foo$18@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_dollar_mid_realm_mac(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'foo$19@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_dollar_mid_realm_managed_service(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'foo$33@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_dollar_mid_realm_user(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'host/foo$20@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_dollar_mid_realm_mac(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'host/foo$21@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_host_dollar_mid_realm_managed_service(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'host/foo$34@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_at_realm_user(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.USER, + opts={'upn': 'foo22@bar@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_at_realm_mac(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.COMPUTER, + opts={'upn': 'foo23@bar@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + def test_salt_upn_at_realm_managed_service(self): + realm = self.get_samdb().domain_dns_name() + client_creds = self._get_creds( + account_type=self.AccountType.MANAGED_SERVICE, + opts={'upn': 'foo35@bar@' + realm}) + self._run_as_req_enc_timestamp(client_creds) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/simple_tests.py b/python/samba/tests/krb5/simple_tests.py new file mode 100755 index 0000000..81587bb --- /dev/null +++ b/python/samba/tests/krb5/simple_tests.py @@ -0,0 +1,185 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.tests.krb5.raw_testcase import RawKerberosTest +from samba.tests.krb5.rfc4120_constants import ( + KU_AS_REP_ENC_PART, + KU_PA_ENC_TIMESTAMP, + KU_TGS_REP_ENC_PART_SUB_KEY, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 + +global_asn1_print = False +global_hexdump = False + + +class SimpleKerberosTests(RawKerberosTest): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def test_simple(self): + user_creds = self.get_user_creds() + user = user_creds.get_username() + krbtgt_creds = self.get_krbtgt_creds(require_keys=False) + krbtgt_account = krbtgt_creds.get_username() + realm = krbtgt_creds.get_realm() + + cname = self.PrincipalName_create(name_type=1, names=[user]) + sname = self.PrincipalName_create(name_type=2, names=[krbtgt_account, realm]) + + till = self.get_KerberosTime(offset=36000) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + padata = None + + etypes = (18, 17, 23) + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + self.assertEqual(rep['msg-type'], 30) + self.assertEqual(rep['error-code'], 25) + rep_padata = self.der_decode( + rep['e-data'], asn1Spec=krb5_asn1.METHOD_DATA()) + + for pa in rep_padata: + if pa['padata-type'] == 19: + etype_info2 = pa['padata-value'] + break + + etype_info2 = self.der_decode( + etype_info2, asn1Spec=krb5_asn1.ETYPE_INFO2()) + + key = self.PasswordKey_from_etype_info2(user_creds, etype_info2[0]) + + (patime, pausec) = self.get_KerberosTimeWithUsec() + pa_ts = self.PA_ENC_TS_ENC_create(patime, pausec) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + pa_ts = self.EncryptedData_create(key, KU_PA_ENC_TIMESTAMP, pa_ts) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.EncryptedData()) + + pa_ts = self.PA_DATA_create(2, pa_ts) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + padata = [pa_ts] + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + msg_type = rep['msg-type'] + self.assertEqual(msg_type, 11) + + enc_part2 = key.decrypt(KU_AS_REP_ENC_PART, rep['enc-part']['cipher']) + + # MIT KDC encodes both EncASRepPart and EncTGSRepPart with + # application tag 26 + try: + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncASRepPart()) + except Exception: + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncTGSRepPart()) + + # TGS Request + service_creds = self.get_service_creds(allow_missing_password=True) + service_name = service_creds.get_username() + + sname = self.PrincipalName_create( + name_type=2, names=["host", service_name]) + kdc_options = krb5_asn1.KDCOptions('forwardable') + till = self.get_KerberosTime(offset=36000) + ticket = rep['ticket'] + ticket_session_key = self.EncryptionKey_import(enc_part2['key']) + padata = [] + + subkey = self.RandomKey(ticket_session_key.etype) + + (ctime, cusec) = self.get_KerberosTimeWithUsec() + + req = self.TGS_REQ_create(padata=padata, + cusec=cusec, + ctime=ctime, + ticket=ticket, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7ffffffe, + etypes=etypes, + addresses=None, + EncAuthorizationData=None, + EncAuthorizationData_key=None, + additional_tickets=None, + ticket_session_key=ticket_session_key, + authenticator_subkey=subkey) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + msg_type = rep['msg-type'] + self.assertEqual(msg_type, 13) + + enc_part2 = subkey.decrypt( + KU_TGS_REP_ENC_PART_SUB_KEY, rep['enc-part']['cipher']) + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncTGSRepPart()) + + return + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/spn_tests.py b/python/samba/tests/krb5/spn_tests.py new file mode 100755 index 0000000..5bcc0bd --- /dev/null +++ b/python/samba/tests/krb5/spn_tests.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2020 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.tests import DynamicTestCase + +import ldb + +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.raw_testcase import KerberosCredentials +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_S_PRINCIPAL_UNKNOWN, + NT_PRINCIPAL, +) + +global_asn1_print = False +global_hexdump = False + + +@DynamicTestCase +class SpnTests(KDCBaseTest): + test_account_types = { + 'computer': KDCBaseTest.AccountType.COMPUTER, + 'server': KDCBaseTest.AccountType.SERVER, + 'rodc': KDCBaseTest.AccountType.RODC + } + test_spns = { + '2_part': 'ldap/{{account}}', + '3_part_our_domain': 'ldap/{{account}}/{netbios_domain_name}', + '3_part_our_realm': 'ldap/{{account}}/{dns_domain_name}', + '3_part_not_our_realm': 'ldap/{{account}}/test', + '3_part_instance': 'ldap/{{account}}:test/{dns_domain_name}' + } + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls._mock_rodc_creds = None + + @classmethod + def setUpDynamicTestCases(cls): + for account_type_name, account_type in cls.test_account_types.items(): + for spn_name, spn in cls.test_spns.items(): + tname = f'{spn_name}_spn_{account_type_name}' + targs = (account_type, spn) + cls.generate_dynamic_test('test_spn', tname, *targs) + + def _test_spn_with_args(self, account_type, spn): + target_creds = self._get_creds(account_type) + spn = self._format_spn(spn, target_creds) + + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=spn.split('/')) + + client_creds = self.get_client_creds() + tgt = self.get_tgt(client_creds) + + samdb = self.get_samdb() + netbios_domain_name = samdb.domain_netbios_name() + dns_domain_name = samdb.domain_dns_name() + + subkey = self.RandomKey(tgt.session_key.etype) + + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5,) + + if account_type is self.AccountType.SERVER: + ticket_etype = AES256_CTS_HMAC_SHA1_96 + else: + ticket_etype = None + decryption_key = self.TicketDecryptionKey_from_creds( + target_creds, etype=ticket_etype) + + if (spn.count('/') > 1 + and (spn.endswith(netbios_domain_name) + or spn.endswith(dns_domain_name)) + and account_type is not self.AccountType.SERVER + and account_type is not self.AccountType.RODC): + expected_error_mode = KDC_ERR_S_PRINCIPAL_UNKNOWN + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + expected_error_mode = 0 + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + kdc_exchange_dict = self.tgs_exchange_dict( + expected_crealm=tgt.crealm, + expected_cname=tgt.cname, + expected_srealm=tgt.srealm, + expected_sname=sname, + ticket_decryption_key=decryption_key, + check_rep_fn=check_rep_fn, + check_error_fn=check_error_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error_mode, + tgt=tgt, + authenticator_subkey=subkey, + kdc_options='0', + expect_edata=False) + + self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=tgt.srealm, + sname=sname, + etypes=etypes) + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _format_spns(self, spns, creds=None): + return map(lambda spn: self._format_spn(spn, creds), spns) + + def _format_spn(self, spn, creds=None): + samdb = self.get_samdb() + + spn = spn.format(netbios_domain_name=samdb.domain_netbios_name(), + dns_domain_name=samdb.domain_dns_name()) + + if creds is not None: + account_name = creds.get_username() + spn = spn.format(account=account_name) + + return spn + + def _get_creds(self, account_type): + spns = self._format_spns(self.test_spns.values()) + + if account_type is self.AccountType.RODC: + creds = self._mock_rodc_creds + if creds is None: + creds = self._get_mock_rodc_creds(spns) + type(self)._mock_rodc_creds = creds + else: + creds = self.get_cached_creds( + account_type=account_type, + opts={ + 'spn': spns + }) + + return creds + + def _get_mock_rodc_creds(self, spns): + rodc_ctx = self.get_mock_rodc_ctx() + + for spn in spns: + spn = spn.format(account=rodc_ctx.myname) + if spn not in rodc_ctx.SPNs: + rodc_ctx.SPNs.append(spn) + + samdb = self.get_samdb() + rodc_dn = ldb.Dn(samdb, rodc_ctx.acct_dn) + + msg = ldb.Message(rodc_dn) + msg['servicePrincipalName'] = ldb.MessageElement( + rodc_ctx.SPNs, + ldb.FLAG_MOD_REPLACE, + 'servicePrincipalName') + samdb.modify(msg) + + creds = KerberosCredentials() + creds.guess(self.get_lp()) + creds.set_realm(rodc_ctx.realm.upper()) + creds.set_domain(rodc_ctx.domain_name) + creds.set_password(rodc_ctx.acct_pass) + creds.set_username(rodc_ctx.myname) + creds.set_workstation(rodc_ctx.samname) + creds.set_dn(rodc_dn) + creds.set_spn(rodc_ctx.SPNs) + + res = samdb.search(base=rodc_dn, + scope=ldb.SCOPE_BASE, + attrs=['msDS-KeyVersionNumber']) + kvno = int(res[0].get('msDS-KeyVersionNumber', idx=0)) + creds.set_kvno(kvno) + + keys = self.get_keys(creds) + self.creds_set_keys(creds, keys) + + return creds + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/test_ccache.py b/python/samba/tests/krb5/test_ccache.py new file mode 100755 index 0000000..6413bfa --- /dev/null +++ b/python/samba/tests/krb5/test_ccache.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2021 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import ldb + +from ldb import SCOPE_SUBTREE +from samba import NTSTATUSError, gensec +from samba.auth import AuthContext +from samba.dcerpc import security +from samba.ndr import ndr_unpack +from samba.ntstatus import NT_STATUS_NO_IMPERSONATION_TOKEN + +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +class CcacheTests(KDCBaseTest): + """Test for authentication using Kerberos credentials stored in a + credentials cache file. + """ + + def test_ccache(self): + self._run_ccache_test() + + def test_ccache_rename(self): + self._run_ccache_test(rename=True) + + def test_ccache_no_pac(self): + self._run_ccache_test(include_pac=False, + expect_anon=True, allow_error=True) + + def _run_ccache_test(self, rename=False, include_pac=True, + expect_anon=False, allow_error=False): + # Create a user account and a machine account, along with a Kerberos + # credentials cache file where the service ticket authenticating the + # user are stored. + + mach_name = "ccachemac" + service = "host" + + samdb = self.get_samdb() + + # Create the user account. + user_credentials = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + user_name = user_credentials.get_username() + + # Create the machine account. + (mach_credentials, _) = self.create_account( + samdb, + mach_name, + account_type=self.AccountType.COMPUTER, + spn="%s/%s" % (service, + mach_name)) + + # Talk to the KDC to obtain the service ticket, which gets placed into + # the cache. The machine account name has to match the name in the + # ticket, to ensure that the krbtgt ticket doesn't also need to be + # stored. + (creds, cachefile) = self.create_ccache_with_user(user_credentials, + mach_credentials, + pac=include_pac) + # Remove the cached credentials file. + self.addCleanup(os.remove, cachefile.name) + + # Retrieve the user account's SID. + ldb_res = samdb.search(scope=SCOPE_SUBTREE, + expression="(sAMAccountName=%s)" % user_name, + attrs=["objectSid"]) + self.assertEqual(1, len(ldb_res)) + sid = ndr_unpack(security.dom_sid, ldb_res[0]["objectSid"][0]) + + if rename: + # Rename the account. + + new_name = self.get_new_username() + + msg = ldb.Message(user_credentials.get_dn()) + msg['sAMAccountName'] = ldb.MessageElement(new_name, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + + # Authenticate in-process to the machine account using the user's + # cached credentials. + + lp = self.get_lp() + lp.set('server role', 'active directory domain controller') + + settings = {} + settings["lp_ctx"] = lp + settings["target_hostname"] = mach_name + + gensec_client = gensec.Security.start_client(settings) + gensec_client.set_credentials(creds) + gensec_client.want_feature(gensec.FEATURE_SEAL) + gensec_client.start_mech_by_sasl_name("GSSAPI") + + auth_context = AuthContext(lp_ctx=lp, ldb=samdb, methods=[]) + + gensec_server = gensec.Security.start_server(settings, auth_context) + gensec_server.set_credentials(mach_credentials) + + gensec_server.start_mech_by_sasl_name("GSSAPI") + + client_finished = False + server_finished = False + server_to_client = b'' + + # Operate as both the client and the server to verify the user's + # credentials. + while not client_finished or not server_finished: + if not client_finished: + print("running client gensec_update") + (client_finished, client_to_server) = gensec_client.update( + server_to_client) + if not server_finished: + print("running server gensec_update") + (server_finished, server_to_client) = gensec_server.update( + client_to_server) + + # Ensure that the first SID contained within the obtained security + # token is the SID of the user we created. + + # Retrieve the SIDs from the security token. + try: + session = gensec_server.session_info() + except NTSTATUSError as e: + if not allow_error: + self.fail() + + enum, _ = e.args + self.assertEqual(NT_STATUS_NO_IMPERSONATION_TOKEN, enum) + return + + token = session.security_token + token_sids = token.sids + self.assertGreater(len(token_sids), 0) + + # Ensure that they match. + self.assertEqual(sid, token_sids[0]) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/test_idmap_nss.py b/python/samba/tests/krb5/test_idmap_nss.py new file mode 100755 index 0000000..1ee0201 --- /dev/null +++ b/python/samba/tests/krb5/test_idmap_nss.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2021 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, 'bin/python') +os.environ['PYTHONUNBUFFERED'] = '1' + +from ldb import SCOPE_SUBTREE +from samba import NTSTATUSError +from samba.credentials import DONT_USE_KERBEROS +from samba.dcerpc import security +from samba.ndr import ndr_unpack +from samba.ntstatus import ( + NT_STATUS_NO_IMPERSONATION_TOKEN, + NT_STATUS_LOGON_FAILURE +) +from samba.samba3 import libsmb_samba_internal as libsmb +from samba.samba3 import param as s3param + +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +class IdmapNssTests(KDCBaseTest): + + mappeduser_uid = 0xffff - 14 + mappeduser_sid = security.dom_sid(f'S-1-22-1-{mappeduser_uid}') + unmappeduser_uid = 0xffff - 15 + unmappeduser_sid = security.dom_sid(f'S-1-22-1-{unmappeduser_uid}') + + def get_mapped_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + c = self._get_krb5_creds(prefix='MAPPED', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys) + c.set_workstation('') + return c + + def get_unmapped_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + c = self._get_krb5_creds(prefix='UNMAPPED', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys) + c.set_workstation('') + return c + + def get_invalid_creds(self, + allow_missing_password=False, + allow_missing_keys=True): + c = self._get_krb5_creds(prefix='INVALID', + allow_missing_password=allow_missing_password, + allow_missing_keys=allow_missing_keys) + c.set_workstation('') + return c + + # Expect a mapping to the local user SID. + def test_mapped_user_kerberos(self): + user_creds = self.get_mapped_creds() + self._run_idmap_nss_test(user_creds, use_kerberos=True, + expected_first_sid=self.mappeduser_sid, + expected_uid=self.mappeduser_uid) + + # Expect a mapping to the local user SID. + def test_mapped_user_ntlm(self): + user_creds = self.get_mapped_creds() + self._run_idmap_nss_test(user_creds, use_kerberos=False, + expected_first_sid=self.mappeduser_sid, + expected_uid=self.mappeduser_uid) + + def test_mapped_user_no_pac_kerberos(self): + user_creds = self.get_mapped_creds() + self._run_idmap_nss_test( + user_creds, use_kerberos=True, remove_pac=True, + expected_error=NT_STATUS_NO_IMPERSONATION_TOKEN) + + def test_unmapped_user_kerberos(self): + user_creds = self.get_unmapped_creds() + self._run_idmap_nss_test(user_creds, use_kerberos=True, + expected_additional_sid=self.unmappeduser_sid, + expected_uid=self.unmappeduser_uid) + + def test_unmapped_user_ntlm(self): + user_creds = self.get_unmapped_creds() + self._run_idmap_nss_test(user_creds, use_kerberos=False, + expected_additional_sid=self.unmappeduser_sid, + expected_uid=self.unmappeduser_uid) + + def test_unmapped_user_no_pac_kerberos(self): + user_creds = self.get_unmapped_creds() + self._run_idmap_nss_test( + user_creds, use_kerberos=True, remove_pac=True, + expected_error=NT_STATUS_NO_IMPERSONATION_TOKEN) + + def test_invalid_user_kerberos(self): + user_creds = self.get_invalid_creds() + self._run_idmap_nss_test(user_creds, use_kerberos=True, + expected_error=NT_STATUS_LOGON_FAILURE) + + def test_invalid_user_ntlm(self): + user_creds = self.get_invalid_creds() + self._run_idmap_nss_test(user_creds, use_kerberos=False, + expected_error=NT_STATUS_LOGON_FAILURE) + + def test_invalid_user_no_pac_kerberos(self): + user_creds = self.get_invalid_creds() + self._run_idmap_nss_test( + user_creds, use_kerberos=True, remove_pac=True, + expected_error=NT_STATUS_NO_IMPERSONATION_TOKEN) + + def _run_idmap_nss_test(self, user_creds, + use_kerberos, + remove_pac=False, + expected_error=None, + expected_first_sid=None, + expected_additional_sid=None, + expected_uid=None): + if expected_first_sid is not None: + self.assertIsNotNone(expected_uid) + if expected_additional_sid is not None: + self.assertIsNotNone(expected_uid) + if expected_uid is not None: + self.assertIsNone(expected_error) + + if not use_kerberos: + self.assertFalse(remove_pac) + + samdb = self.get_samdb() + + server_name = self.host + service = 'cifs' + share = 'tmp' + + server_creds = self.get_server_creds() + + if expected_first_sid is None: + # Retrieve the user account's SID. + user_name = user_creds.get_username() + res = samdb.search(scope=SCOPE_SUBTREE, + expression=f'(sAMAccountName={user_name})', + attrs=['objectSid']) + self.assertEqual(1, len(res)) + + expected_first_sid = ndr_unpack(security.dom_sid, + res[0].get('objectSid', idx=0)) + + if use_kerberos: + # Talk to the KDC to obtain the service ticket, which gets placed + # into the cache. The machine account name has to match the name in + # the ticket, to ensure that the krbtgt ticket doesn't also need to + # be stored. + creds, cachefile = self.create_ccache_with_user( + user_creds, + server_creds, + service, + server_name, + pac=not remove_pac) + + # Remove the cached creds file. + self.addCleanup(os.remove, cachefile.name) + + # Set the Kerberos 5 creds cache environment variable. This is + # required because the codepath that gets run (gse_krb5) looks for + # it in here and not in the creds object. + krb5_ccname = os.environ.get('KRB5CCNAME', '') + self.addCleanup(os.environ.__setitem__, 'KRB5CCNAME', krb5_ccname) + os.environ['KRB5CCNAME'] = 'FILE:' + cachefile.name + else: + creds = user_creds + creds.set_kerberos_state(DONT_USE_KERBEROS) + + # Connect to a share and retrieve the user SID. + s3_lp = s3param.get_context() + s3_lp.load(self.get_lp().configfile) + + min_protocol = s3_lp.get('client min protocol') + self.addCleanup(s3_lp.set, 'client min protocol', min_protocol) + s3_lp.set('client min protocol', 'NT1') + + max_protocol = s3_lp.get('client max protocol') + self.addCleanup(s3_lp.set, 'client max protocol', max_protocol) + s3_lp.set('client max protocol', 'NT1') + + try: + conn = libsmb.Conn(server_name, share, lp=s3_lp, creds=creds) + except NTSTATUSError as e: + enum, _ = e.args + self.assertEqual(expected_error, enum) + return + else: + self.assertIsNone(expected_error) + + uid, gid, gids, sids, guest = conn.posix_whoami() + + # Ensure that they match. + self.assertEqual(expected_first_sid, sids[0]) + self.assertNotIn(expected_first_sid, sids[1:-1]) + + if expected_additional_sid: + self.assertNotEqual(expected_additional_sid, sids[0]) + self.assertIn(expected_additional_sid, sids) + + self.assertIsNotNone(expected_uid) + self.assertEqual(expected_uid, uid) + + +if __name__ == '__main__': + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/test_ldap.py b/python/samba/tests/krb5/test_ldap.py new file mode 100755 index 0000000..eaf79e7 --- /dev/null +++ b/python/samba/tests/krb5/test_ldap.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2021 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import ldb + +from ldb import LdbError, ERR_OPERATIONS_ERROR, SCOPE_BASE, SCOPE_SUBTREE +from samba.dcerpc import security +from samba.ndr import ndr_unpack +from samba.samdb import SamDB +from samba import credentials + +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +class LdapTests(KDCBaseTest): + """Test for LDAP authentication using Kerberos credentials stored in a + credentials cache file. + """ + + def test_ldap(self): + self._run_ldap_test() + + def test_ldap_rename(self): + self._run_ldap_test(rename=True) + + def test_ldap_no_pac(self): + self._run_ldap_test(include_pac=False, + expect_anon=True, allow_error=True) + + def _run_ldap_test(self, rename=False, include_pac=True, + expect_anon=False, allow_error=False): + # Create a user account and a machine account, along with a Kerberos + # credentials cache file where the service ticket authenticating the + # user are stored. + + samdb = self.get_samdb() + + mach_name = samdb.host_dns_name() + service = "ldap" + + # Create the user account. + user_credentials = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + user_name = user_credentials.get_username() + + mach_credentials = self.get_dc_creds() + + # Talk to the KDC to obtain the service ticket, which gets placed into + # the cache. The machine account name has to match the name in the + # ticket, to ensure that the krbtgt ticket doesn't also need to be + # stored. + (creds, cachefile) = self.create_ccache_with_user(user_credentials, + mach_credentials, + service, + mach_name, + pac=include_pac) + # Remove the cached credentials file. + self.addCleanup(os.remove, cachefile.name) + + # Retrieve the user account's SID. + ldb_res = samdb.search(scope=SCOPE_SUBTREE, + expression="(sAMAccountName=%s)" % user_name, + attrs=["objectSid"]) + self.assertEqual(1, len(ldb_res)) + sid = ndr_unpack(security.dom_sid, ldb_res[0]["objectSid"][0]) + + if rename: + # Rename the account. + + new_name = self.get_new_username() + + msg = ldb.Message(user_credentials.get_dn()) + msg['sAMAccountName'] = ldb.MessageElement(new_name, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + + # Authenticate in-process to the machine account using the user's + # cached credentials. + + # Connect to the machine account and retrieve the user SID. + try: + ldb_as_user = SamDB(url="ldap://%s" % mach_name, + credentials=creds, + lp=self.get_lp()) + except LdbError as e: + if not allow_error: + self.fail() + + enum, estr = e.args + self.assertEqual(ERR_OPERATIONS_ERROR, enum) + self.assertIn('NT_STATUS_NO_IMPERSONATION_TOKEN', estr) + return + + ldb_res = ldb_as_user.search('', + scope=SCOPE_BASE, + attrs=["tokenGroups"]) + self.assertEqual(1, len(ldb_res)) + + token_groups = ldb_res[0]["tokenGroups"] + token_sid = ndr_unpack(security.dom_sid, token_groups[0]) + + if expect_anon: + # Ensure we got an anonymous token. + self.assertEqual(security.SID_NT_ANONYMOUS, str(token_sid)) + token_sid = ndr_unpack(security.dom_sid, token_groups[1]) + self.assertEqual(security.SID_NT_NETWORK, str(token_sid)) + if len(token_groups) >= 3: + token_sid = ndr_unpack(security.dom_sid, token_groups[2]) + self.assertEqual(security.SID_NT_THIS_ORGANISATION, + str(token_sid)) + else: + # Ensure that they match. + self.assertEqual(sid, token_sid) + + def test_ldap_anonymous(self): + samdb = self.get_samdb() + mach_name = samdb.host_dns_name() + + anon_creds = credentials.Credentials() + anon_creds.set_anonymous() + + # Connect to the machine account and retrieve the user SID. + ldb_as_user = SamDB(url="ldap://%s" % mach_name, + credentials=anon_creds, + lp=self.get_lp()) + ldb_res = ldb_as_user.search('', + scope=SCOPE_BASE, + attrs=["tokenGroups"]) + self.assertEqual(1, len(ldb_res)) + + # Ensure we got an anonymous token. + token_sid = ndr_unpack(security.dom_sid, ldb_res[0]["tokenGroups"][0]) + self.assertEqual(security.SID_NT_ANONYMOUS, str(token_sid)) + self.assertEqual(len(ldb_res[0]["tokenGroups"]), 1) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/test_min_domain_uid.py b/python/samba/tests/krb5/test_min_domain_uid.py new file mode 100755 index 0000000..9cabb7c --- /dev/null +++ b/python/samba/tests/krb5/test_min_domain_uid.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Samuel Cabrero 2021 +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import pwd +import ctypes + +from samba.tests import env_get_var_value +from samba.samba3 import libsmb_samba_internal as libsmb +from samba.samba3 import param as s3param +from samba import NTSTATUSError, ntstatus + +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.credentials import MUST_USE_KERBEROS, DONT_USE_KERBEROS + +class SmbMinDomainUid(KDCBaseTest): + """Test for SMB authorization without NSS winbind. In such setup domain + accounts are mapped to local accounts using the 'username map' option. + """ + + def setUp(self): + super().setUp() + + # Create a user account, along with a Kerberos credentials cache file + # where the service ticket authenticating the user are stored. + self.samdb = self.get_samdb() + + self.mach_name = env_get_var_value('SERVER') + self.user_name = "root" + self.service = "cifs" + self.share = "tmp" + + # Create the user account. + (self.user_creds, _) = self.create_account(self.samdb, self.user_name) + + # Build the global inject file path + server_conf = env_get_var_value('SMB_CONF_PATH') + server_conf_dir = os.path.dirname(server_conf) + self.global_inject = os.path.join(server_conf_dir, "global_inject.conf") + + def _test_min_uid(self, creds): + # Assert unix root uid is less than 'idmap config ADDOMAIN' minimum + s3_lp = s3param.get_context() + s3_lp.load(self.get_lp().configfile) + + domain_range = s3_lp.get("idmap config * : range").split('-') + domain_range_low = int(domain_range[0]) + unix_root_pw = pwd.getpwnam(self.user_name) + self.assertLess(unix_root_pw.pw_uid, domain_range_low) + self.assertLess(unix_root_pw.pw_gid, domain_range_low) + + conn = libsmb.Conn(self.mach_name, self.share, lp=s3_lp, creds=creds) + # Disconnect + conn = None + + # Restrict access to local root account uid + with open(self.global_inject, 'w') as f: + f.write("min domain uid = %s\n" % (unix_root_pw.pw_uid + 1)) + + with self.assertRaises(NTSTATUSError) as cm: + conn = libsmb.Conn(self.mach_name, + self.share, + lp=s3_lp, + creds=creds) + code = ctypes.c_uint32(cm.exception.args[0]).value + self.assertEqual(code, ntstatus.NT_STATUS_INVALID_TOKEN) + + # check that the local root account uid is now allowed + with open(self.global_inject, 'w') as f: + f.write("min domain uid = %s\n" % unix_root_pw.pw_uid) + + conn = libsmb.Conn(self.mach_name, self.share, lp=s3_lp, creds=creds) + # Disconnect + del conn + + with open(self.global_inject, 'w') as f: + f.truncate() + + def test_min_domain_uid_krb5(self): + krb5_state = self.user_creds.get_kerberos_state() + self.user_creds.set_kerberos_state(MUST_USE_KERBEROS) + ret = self._test_min_uid(self.user_creds) + self.user_creds.set_kerberos_state(krb5_state) + return ret + + def test_min_domain_uid_ntlmssp(self): + krb5_state = self.user_creds.get_kerberos_state() + self.user_creds.set_kerberos_state(DONT_USE_KERBEROS) + ret = self._test_min_uid(self.user_creds) + self.user_creds.set_kerberos_state(krb5_state) + return ret + + def tearDown(self): + # Ensure no leftovers in global inject file + with open(self.global_inject, 'w') as f: + f.truncate() + + super().tearDown() + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/test_rpc.py b/python/samba/tests/krb5/test_rpc.py new file mode 100755 index 0000000..6faf2a0 --- /dev/null +++ b/python/samba/tests/krb5/test_rpc.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2021 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import ldb + +from samba import NTSTATUSError, credentials +from samba.dcerpc import lsa +from samba.ntstatus import ( + NT_STATUS_ACCESS_DENIED, + NT_STATUS_NO_IMPERSONATION_TOKEN +) + +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +class RpcTests(KDCBaseTest): + """Test for RPC authentication using Kerberos credentials stored in a + credentials cache file. + """ + + def test_rpc(self): + self._run_rpc_test() + + def test_rpc_rename(self): + self._run_rpc_test(rename=True) + + def test_rpc_no_pac(self): + self._run_rpc_test(include_pac=False, + expect_anon=True, allow_error=True) + + def _run_rpc_test(self, rename=False, include_pac=True, + expect_anon=False, allow_error=False): + # Create a user account and a machine account, along with a Kerberos + # credentials cache file where the service ticket authenticating the + # user are stored. + + samdb = self.get_samdb() + + mach_name = self.host + service = "cifs" + + # Create the user account. + user_credentials = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + user_name = user_credentials.get_username() + + mach_credentials = self.get_server_creds() + + # Talk to the KDC to obtain the service ticket, which gets placed into + # the cache. The machine account name has to match the name in the + # ticket, to ensure that the krbtgt ticket doesn't also need to be + # stored. + (creds, cachefile) = self.create_ccache_with_user(user_credentials, + mach_credentials, + service, + mach_name, + pac=include_pac) + # Remove the cached credentials file. + self.addCleanup(os.remove, cachefile.name) + + if rename: + # Rename the account. + + new_name = self.get_new_username() + + msg = ldb.Message(user_credentials.get_dn()) + msg['sAMAccountName'] = ldb.MessageElement(new_name, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + + # Authenticate in-process to the machine account using the user's + # cached credentials. + + binding_str = "ncacn_np:%s[\\pipe\\lsarpc]" % mach_name + try: + conn = lsa.lsarpc(binding_str, self.get_lp(), creds) + except NTSTATUSError as e: + if not allow_error: + self.fail() + + enum, _ = e.args + self.assertIn(enum, {NT_STATUS_ACCESS_DENIED, + NT_STATUS_NO_IMPERSONATION_TOKEN}) + return + + (account_name, _) = conn.GetUserName(None, None, None) + + if expect_anon: + self.assertNotEqual(user_name, account_name.string) + else: + self.assertEqual(user_name, account_name.string) + + def test_rpc_anonymous(self): + mach_name = self.host + + anon_creds = credentials.Credentials() + anon_creds.set_anonymous() + + binding_str = "ncacn_np:%s[\\pipe\\lsarpc]" % mach_name + conn = lsa.lsarpc(binding_str, self.get_lp(), anon_creds) + + (account_name, _) = conn.GetUserName(None, None, None) + + self.assertEqual('ANONYMOUS LOGON', account_name.string.upper()) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/test_smb.py b/python/samba/tests/krb5/test_smb.py new file mode 100755 index 0000000..f0a82a4 --- /dev/null +++ b/python/samba/tests/krb5/test_smb.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2021 Catalyst.Net Ltd +# +# 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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +import ldb + +from ldb import SCOPE_SUBTREE +from samba import NTSTATUSError +from samba.dcerpc import security +from samba.ndr import ndr_unpack +from samba.ntstatus import NT_STATUS_NO_IMPERSONATION_TOKEN +from samba.samba3 import libsmb_samba_internal as libsmb +from samba.samba3 import param as s3param + +from samba.tests.krb5.kdc_base_test import KDCBaseTest + +global_asn1_print = False +global_hexdump = False + + +class SmbTests(KDCBaseTest): + """Test for SMB authentication using Kerberos credentials stored in a + credentials cache file. + """ + + def test_smb(self): + self._run_smb_test() + + def test_smb_rename(self): + self._run_smb_test(rename=True) + + def test_smb_no_pac(self): + self._run_smb_test(include_pac=False, + expect_error=True) + + def _run_smb_test(self, rename=False, include_pac=True, + expect_error=False): + # Create a user account and a machine account, along with a Kerberos + # credentials cache file where the service ticket authenticating the + # user are stored. + + samdb = self.get_samdb() + + mach_name = samdb.host_dns_name() + service = "cifs" + share = "tmp" + + # Create the user account. + user_credentials = self.get_cached_creds( + account_type=self.AccountType.USER, + use_cache=False) + user_name = user_credentials.get_username() + + mach_credentials = self.get_dc_creds() + + mach_credentials = self.get_dc_creds() + + # Talk to the KDC to obtain the service ticket, which gets placed into + # the cache. The machine account name has to match the name in the + # ticket, to ensure that the krbtgt ticket doesn't also need to be + # stored. + (creds, cachefile) = self.create_ccache_with_user(user_credentials, + mach_credentials, + service, + mach_name, + pac=include_pac) + # Remove the cached credentials file. + self.addCleanup(os.remove, cachefile.name) + + # Retrieve the user account's SID. + ldb_res = samdb.search(scope=SCOPE_SUBTREE, + expression="(sAMAccountName=%s)" % user_name, + attrs=["objectSid"]) + self.assertEqual(1, len(ldb_res)) + sid = ndr_unpack(security.dom_sid, ldb_res[0]["objectSid"][0]) + + if rename: + # Rename the account. + + new_name = self.get_new_username() + + msg = ldb.Message(user_credentials.get_dn()) + msg['sAMAccountName'] = ldb.MessageElement(new_name, + ldb.FLAG_MOD_REPLACE, + 'sAMAccountName') + samdb.modify(msg) + + # Set the Kerberos 5 credentials cache environment variable. This is + # required because the codepath that gets run (gse_krb5) looks for it + # in here and not in the credentials object. + krb5_ccname = os.environ.get("KRB5CCNAME", "") + self.addCleanup(os.environ.__setitem__, "KRB5CCNAME", krb5_ccname) + os.environ["KRB5CCNAME"] = "FILE:" + cachefile.name + + # Authenticate in-process to the machine account using the user's + # cached credentials. + + # Connect to a share and retrieve the user SID. + s3_lp = s3param.get_context() + s3_lp.load(self.get_lp().configfile) + + min_protocol = s3_lp.get("client min protocol") + self.addCleanup(s3_lp.set, "client min protocol", min_protocol) + s3_lp.set("client min protocol", "NT1") + + max_protocol = s3_lp.get("client max protocol") + self.addCleanup(s3_lp.set, "client max protocol", max_protocol) + s3_lp.set("client max protocol", "NT1") + + try: + conn = libsmb.Conn(mach_name, share, lp=s3_lp, creds=creds) + except NTSTATUSError as e: + if not expect_error: + self.fail() + + enum, _ = e.args + self.assertEqual(NT_STATUS_NO_IMPERSONATION_TOKEN, enum) + return + else: + self.assertFalse(expect_error) + + (uid, gid, gids, sids, guest) = conn.posix_whoami() + + # Ensure that they match. + self.assertEqual(sid, sids[0]) + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/krb5/xrealm_tests.py b/python/samba/tests/krb5/xrealm_tests.py new file mode 100755 index 0000000..70e06f8 --- /dev/null +++ b/python/samba/tests/krb5/xrealm_tests.py @@ -0,0 +1,187 @@ +#!/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 <http://www.gnu.org/licenses/>. +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.tests.krb5.raw_testcase import RawKerberosTest +from samba.tests.krb5.rfc4120_constants import ( + KU_PA_ENC_TIMESTAMP, + KU_AS_REP_ENC_PART, + KU_TGS_REP_ENC_PART_SUB_KEY, +) +import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 +import samba.tests + +global_asn1_print = False +global_hexdump = False + + +class XrealmKerberosTests(RawKerberosTest): + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def test_xrealm(self): + user_creds = self.get_user_creds() + user = user_creds.get_username() + realm = user_creds.get_realm() + + cname = self.PrincipalName_create(name_type=1, names=[user]) + sname = self.PrincipalName_create(name_type=2, names=["krbtgt", realm]) + + till = self.get_KerberosTime(offset=36000) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + padata = None + + etypes = (18, 17, 23) + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + self.assertEqual(rep['msg-type'], 30) + self.assertEqual(rep['error-code'], 25) + rep_padata = self.der_decode( + rep['e-data'], asn1Spec=krb5_asn1.METHOD_DATA()) + + for pa in rep_padata: + if pa['padata-type'] == 19: + etype_info2 = pa['padata-value'] + break + + etype_info2 = self.der_decode( + etype_info2, asn1Spec=krb5_asn1.ETYPE_INFO2()) + + key = self.PasswordKey_from_etype_info2(user_creds, etype_info2[0]) + + (patime, pausec) = self.get_KerberosTimeWithUsec() + pa_ts = self.PA_ENC_TS_ENC_create(patime, pausec) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.PA_ENC_TS_ENC()) + + pa_ts = self.EncryptedData_create(key, KU_PA_ENC_TIMESTAMP, pa_ts) + pa_ts = self.der_encode(pa_ts, asn1Spec=krb5_asn1.EncryptedData()) + + pa_ts = self.PA_DATA_create(2, pa_ts) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + padata = [pa_ts] + + req = self.AS_REQ_create(padata=padata, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7fffffff, + etypes=etypes, + addresses=None, + additional_tickets=None) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + msg_type = rep['msg-type'] + self.assertEqual(msg_type, 11) + + enc_part2 = key.decrypt(KU_AS_REP_ENC_PART, rep['enc-part']['cipher']) + + # MIT KDC encodes both EncASRepPart and EncTGSRepPart with + # application tag 26 + try: + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncASRepPart()) + except Exception: + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncTGSRepPart()) + + # TGS Request (for cross-realm TGT) + trust_realm = samba.tests.env_get_var_value('TRUST_REALM') + sname = self.PrincipalName_create( + name_type=2, names=["krbtgt", trust_realm]) + + kdc_options = krb5_asn1.KDCOptions('forwardable') + till = self.get_KerberosTime(offset=36000) + ticket = rep['ticket'] + ticket_session_key = self.EncryptionKey_import(enc_part2['key']) + padata = [] + + subkey = self.RandomKey(ticket_session_key.etype) + + (ctime, cusec) = self.get_KerberosTimeWithUsec() + + req = self.TGS_REQ_create(padata=padata, + cusec=cusec, + ctime=ctime, + ticket=ticket, + kdc_options=str(kdc_options), + cname=cname, + realm=realm, + sname=sname, + from_time=None, + till_time=till, + renew_time=None, + nonce=0x7ffffffe, + etypes=etypes, + addresses=None, + EncAuthorizationData=None, + EncAuthorizationData_key=None, + additional_tickets=None, + ticket_session_key=ticket_session_key, + authenticator_subkey=subkey) + rep = self.send_recv_transaction(req) + self.assertIsNotNone(rep) + + msg_type = rep['msg-type'] + self.assertEqual(msg_type, 13) + + enc_part2 = subkey.decrypt( + KU_TGS_REP_ENC_PART_SUB_KEY, rep['enc-part']['cipher']) + enc_part2 = self.der_decode( + enc_part2, asn1Spec=krb5_asn1.EncTGSRepPart()) + + # Check the forwardable flag + fwd_pos = len(tuple(krb5_asn1.TicketFlags('forwardable'))) - 1 + assert(krb5_asn1.TicketFlags(enc_part2['flags'])[fwd_pos]) + + return + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() |