mirror of
https://github.com/samba-team/samba.git
synced 2025-02-01 05:47:28 +03:00
f29693d131
We want to use the PAC returned NT hash in the UF_SMARTCARD_REQUIRED case as it will usually be random bytes so we can not just assert on the value any more. Signed-off-by: Andrew Bartlett <abartlet@samba.org> Reviewed-by: Jo Sutton <josutton@catalyst.net.nz>
6250 lines
245 KiB
Python
6250 lines
245 KiB
Python
# 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,
|
||
)
|
||
from samba.dsdb import (
|
||
UF_SMARTCARD_REQUIRED
|
||
)
|
||
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_KEY_EXPIRED,
|
||
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',
|
||
'user_account_control'
|
||
]
|
||
|
||
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.user_account_control = 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)
|
||
|
||
def set_user_account_control(self, value):
|
||
self.user_account_control = 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 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 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 and \
|
||
self.getElementValue(ticket_private,
|
||
'caddr') != []:
|
||
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 and \
|
||
self.getElementValue(ticket_private,
|
||
'caddr') != []:
|
||
self.assertElementMissing(ticket_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)
|
||
if creds.user_account_control & UF_SMARTCARD_REQUIRED:
|
||
self.assertNotEqual(creds.get_nt_hash(), nt_password)
|
||
else:
|
||
self.assertEqual(creds.get_nt_hash(), nt_password)
|
||
|
||
kdc_exchange_dict['nt_hash_from_pac'] = ntlm_package.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_KEY_EXPIRED:
|
||
expected_patypes += (PADATA_PK_AS_REP,)
|
||
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
|