]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
tests/krb5: Add method to verify ticket PAC checksums
authorJoseph Sutton <josephsutton@catalyst.net.nz>
Fri, 17 Sep 2021 02:56:51 +0000 (14:56 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Thu, 23 Sep 2021 18:32:29 +0000 (18:32 +0000)
BUG: https://bugzilla.samba.org/show_bug.cgi?id=14642

Signed-off-by: Joseph Sutton <josephsutton@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/tests/krb5/raw_testcase.py

index e213b5eef9b918bedbe0e397d18427f377cbfe1e..265789cdb262174b25611942293ec1eb94a02fd0 100644 (file)
@@ -34,8 +34,9 @@ from pyasn1.codec.native.encoder import encode as pyasn1_native_encode
 from pyasn1.codec.ber.encoder import BitStringEncoder
 
 from samba.credentials import Credentials
-from samba.dcerpc import security
+from samba.dcerpc import krb5pac, security
 from samba.gensec import FEATURE_SEAL
+from samba.ndr import ndr_pack, ndr_unpack
 
 import samba.tests
 from samba.tests import TestCaseInTempDir
@@ -418,6 +419,10 @@ class KerberosTicketCreds:
 class RawKerberosTest(TestCaseInTempDir):
     """A raw Kerberos Test case."""
 
+    pac_checksum_types = {krb5pac.PAC_TYPE_SRV_CHECKSUM,
+                          krb5pac.PAC_TYPE_KDC_CHECKSUM,
+                          krb5pac.PAC_TYPE_TICKET_CHECKSUM}
+
     etypes_to_test = (
         {"value": -1111, "name": "dummy", },
         {"value": kcrypto.Enctype.AES256, "name": "aes128", },
@@ -2900,6 +2905,114 @@ class RawKerberosTest(TestCaseInTempDir):
                                         ticket_blob)
         self.assertEqual(expected_checksum, checksum)
 
+    def verify_ticket(self, ticket, krbtgt_key, expect_pac=True):
+        # Check if the ticket is a TGT.
+        sname = ticket.ticket['sname']
+        is_tgt = sname['name-string'][0] == b'krbtgt'
+
+        # 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 = {}
+
+        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_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)
+
+        # Verify the signatures.
+
+        server_checksum, server_ctype = checksums[
+            krb5pac.PAC_TYPE_SRV_CHECKSUM]
+        Krb5EncryptionKey.verify_checksum(key,
+                                          KU_NON_KERB_CKSUM_SALT,
+                                          pac_data,
+                                          server_ctype,
+                                          server_checksum)
+
+        kdc_checksum, kdc_ctype = checksums[
+            krb5pac.PAC_TYPE_KDC_CHECKSUM]
+        krbtgt_key.verify_checksum(KU_NON_KERB_CKSUM_SALT,
+                                   server_checksum,
+                                   kdc_ctype,
+                                   kdc_checksum)
+
+        if is_tgt:
+            self.assertNotIn(krb5pac.PAC_TYPE_TICKET_CHECKSUM, checksums)
+        else:
+            ticket_checksum, ticket_ctype = checksums[
+                krb5pac.PAC_TYPE_TICKET_CHECKSUM]
+            enc_part['authorization-data'] = auth_data
+            enc_part = self.der_encode(enc_part,
+                                       asn1Spec=krb5_asn1.EncTicketPart())
+
+            krbtgt_key.verify_checksum(KU_NON_KERB_CKSUM_SALT,
+                                       enc_part,
+                                       ticket_ctype,
+                                       ticket_checksum)
+
     def replace_pac(self, auth_data, new_pac, expect_pac=True):
         if new_pac is not None:
             self.assertElementEqual(new_pac, 'ad-type', AD_WIN2K_PAC)
@@ -2943,6 +3056,9 @@ class RawKerberosTest(TestCaseInTempDir):
 
         return new_auth_data, old_pac
 
+    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'])