#!/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 .
#
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 preceeding. 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 = self.get_objectSid(samdb, user_dn)
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)
@staticmethod
def freeze(m):
return frozenset((k, v) for k, v in m.items())
@classmethod
def setUpDynamicTestCases(cls):
FILTER = env_get_var_value('FILTER', allow_missing=True)
for case in cls.cases:
name = case.pop('name')
if FILTER and not re.search(FILTER, name):
continue
name = re.sub(r'\W+', '_', name)
# 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')
if FILTER and not re.search(FILTER, name):
continue
name = re.sub(r'\W+', '_', name)
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',
},
{
# This test fails on Windows, which for an integer syntax claim
# issues corrupt data shifted four bytes to the right.
'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',
},
{
# This test fails on Windows, which for an integer syntax claim
# issues corrupt data that cannot be NDR unpacked.
'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': '',
'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 = self.get_objectSid(samdb, user_dn)
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 = self.get_objectSid(samdb, mach_dn)
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 tgs_to_krbtgt:
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=not tgs_to_krbtgt,
expected_device_groups=tgs_device_expected_mapped,
expect_device_claims=not tgs_to_krbtgt,
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 still 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),
(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)]),
},
},
{
# 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_id = self.get_new_username()
silo_dn = self.create_authn_silo(silo_id, 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_id,),
},
}
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()