summaryrefslogtreecommitdiffstats
path: root/python/samba/tests/krb5
diff options
context:
space:
mode:
Diffstat (limited to 'python/samba/tests/krb5')
-rwxr-xr-xpython/samba/tests/krb5/alias_tests.py202
-rwxr-xr-xpython/samba/tests/krb5/as_canonicalization_tests.py474
-rwxr-xr-xpython/samba/tests/krb5/as_req_tests.py606
-rwxr-xr-xpython/samba/tests/krb5/authn_policy_tests.py8903
-rwxr-xr-xpython/samba/tests/krb5/claims_in_pac.py490
-rwxr-xr-xpython/samba/tests/krb5/claims_tests.py2032
-rwxr-xr-xpython/samba/tests/krb5/compatability_tests.py227
-rwxr-xr-xpython/samba/tests/krb5/conditional_ace_tests.py5588
-rwxr-xr-xpython/samba/tests/krb5/device_tests.py2211
-rwxr-xr-xpython/samba/tests/krb5/etype_tests.py597
-rwxr-xr-xpython/samba/tests/krb5/fast_tests.py2108
-rwxr-xr-xpython/samba/tests/krb5/gkdi_tests.py745
-rwxr-xr-xpython/samba/tests/krb5/group_tests.py1967
-rwxr-xr-xpython/samba/tests/krb5/kcrypto.py969
-rw-r--r--python/samba/tests/krb5/kdc_base_test.py3755
-rwxr-xr-xpython/samba/tests/krb5/kdc_tests.py228
-rwxr-xr-xpython/samba/tests/krb5/kdc_tgs_tests.py3506
-rwxr-xr-xpython/samba/tests/krb5/kdc_tgt_tests.py86
-rwxr-xr-xpython/samba/tests/krb5/kpasswd_tests.py983
-rwxr-xr-xpython/samba/tests/krb5/lockout_tests.py1137
-rwxr-xr-xpython/samba/tests/krb5/ms_kile_client_principal_lookup_tests.py818
-rwxr-xr-xpython/samba/tests/krb5/nt_hash_tests.py142
-rwxr-xr-xpython/samba/tests/krb5/pac_align_tests.py93
-rwxr-xr-xpython/samba/tests/krb5/pkinit_tests.py1211
-rwxr-xr-xpython/samba/tests/krb5/protected_users_tests.py1053
-rwxr-xr-xpython/samba/tests/krb5/pyasn1_regen.sh42
-rw-r--r--python/samba/tests/krb5/raw_testcase.py6221
-rw-r--r--python/samba/tests/krb5/rfc4120.asn11908
-rw-r--r--python/samba/tests/krb5/rfc4120_constants.py247
-rw-r--r--python/samba/tests/krb5/rfc4120_pyasn1.py92
-rw-r--r--python/samba/tests/krb5/rfc4120_pyasn1_generated.py2690
-rwxr-xr-xpython/samba/tests/krb5/rodc_tests.py77
-rwxr-xr-xpython/samba/tests/krb5/s4u_tests.py1838
-rwxr-xr-xpython/samba/tests/krb5/salt_tests.py469
-rwxr-xr-xpython/samba/tests/krb5/simple_tests.py185
-rwxr-xr-xpython/samba/tests/krb5/spn_tests.py212
-rwxr-xr-xpython/samba/tests/krb5/test_ccache.py173
-rwxr-xr-xpython/samba/tests/krb5/test_idmap_nss.py232
-rwxr-xr-xpython/samba/tests/krb5/test_ldap.py168
-rwxr-xr-xpython/samba/tests/krb5/test_min_domain_uid.py122
-rwxr-xr-xpython/samba/tests/krb5/test_rpc.py138
-rwxr-xr-xpython/samba/tests/krb5/test_smb.py153
-rwxr-xr-xpython/samba/tests/krb5/xrealm_tests.py187
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()