The branch, master has been updated
       via  f52ea1082bf s4:kdc:sdb_to_hdb key trust support
       via  bb8c760709a tests:krb5 Add key trust tests
       via  4d9c130a14b s4:kdc:db-glue binary dn changes
       via  ace784516c8 s4:auth:sam Fix key trust attribute name
      from  cf4f13f6830 smbd: Fixing CID 1509052 for time_t in DEBUG statement

https://git.samba.org/?p=samba.git;a=shortlog;h=master


- Log -----------------------------------------------------------------
commit f52ea1082bf245f6bfd424b6ba76c74881df97b5
Author: Gary Lockyer <[email protected]>
Date:   Mon Aug 11 12:00:03 2025 +1200

    s4:kdc:sdb_to_hdb key trust support
    
    Convert key trust public keys contained in the clients sdb records, and add
    to the HDB_Ext_KeyTrust extension on the clients HDB record
    
    Signed-off-by: Gary Lockyer <[email protected]>
    Reviewed-by: Jennifer Sutton <[email protected]>
    
    Autobuild-User(master): Douglas Bagnall <[email protected]>
    Autobuild-Date(master): Tue Sep 16 23:23:42 UTC 2025 on atb-devel-224

commit bb8c760709a769a882db87a65826f989bbf810b7
Author: Gary Lockyer <[email protected]>
Date:   Mon Aug 11 11:44:14 2025 +1200

    tests:krb5 Add key trust tests
    
    Add tests to verify Key Trust authentication
    
    Signed-off-by: Gary Lockyer <[email protected]>
    Reviewed-by: Jennifer Sutton <[email protected]>

commit 4d9c130a14b83615fa165ae8f1b4f5b5bb53caf8
Author: Gary Lockyer <[email protected]>
Date:   Mon Aug 11 16:09:46 2025 +1200

    s4:kdc:db-glue binary dn changes
    
    msDS-KeyCredentialLink will be stored as a BinaryDN and not a binary blob.
    This commit updates db-glue and it's tests accordingly.
    
    Signed-off-by: Gary Lockyer <[email protected]>
    Reviewed-by: Jennifer Sutton <[email protected]>

commit ace784516c84df2e012747d1094db0bd056cfc12
Author: Gary Lockyer <[email protected]>
Date:   Mon Aug 11 10:28:35 2025 +1200

    s4:auth:sam Fix key trust attribute name
    
    Use the correct attribute name for the key trust credentials,
    msDS-KeyCredentialLink.
    
    Signed-off-by: Gary Lockyer <[email protected]>
    Reviewed-by: Jennifer Sutton <[email protected]>

-----------------------------------------------------------------------

Summary of changes:
 python/samba/tests/krb5/kdc_base_test.py   |   7 +-
 python/samba/tests/krb5/key_trust_tests.py | 497 +++++++++++++++++++++++++++++
 selftest/knownfail_mit_kdc                 |   4 +
 selftest/tests.py                          |   2 +
 source4/auth/sam.c                         |   2 +-
 source4/kdc/db-glue.c                      |  34 +-
 source4/kdc/sdb_to_hdb.c                   | 127 ++++++++
 source4/kdc/tests/db-glue-test.c           | 143 +++++++--
 source4/kdc/tests/sdb_to_hdb_test.c        | 176 ++++++++++
 source4/kdc/wscript_build                  |  11 +
 source4/selftest/tests.py                  |   4 +
 11 files changed, 972 insertions(+), 35 deletions(-)
 create mode 100755 python/samba/tests/krb5/key_trust_tests.py
 create mode 100644 source4/kdc/tests/sdb_to_hdb_test.c


Changeset truncated at 500 lines:

diff --git a/python/samba/tests/krb5/kdc_base_test.py 
b/python/samba/tests/krb5/kdc_base_test.py
index a233d37506b..4cf87fa0ba5 100644
--- a/python/samba/tests/krb5/kdc_base_test.py
+++ b/python/samba/tests/krb5/kdc_base_test.py
@@ -2536,6 +2536,7 @@ class KDCBaseTest(TestCaseInTempDir, RawKerberosTest):
             'logon_hours': None,
             'smartcard_required': False,
             'enabled': True,
+            'keycredlink' : None,
         }
 
         account_opts = {
@@ -2595,7 +2596,8 @@ class KDCBaseTest(TestCaseInTempDir, RawKerberosTest):
                             assigned_silo,
                             logon_hours,
                             smartcard_required,
-                            enabled):
+                            enabled,
+                            keycredlink):
         self.assertNotEqual(account_type, self.AccountType.TRUST)
         if account_type is self.AccountType.USER:
             self.assertIsNone(delegation_to_spn)
@@ -2667,6 +2669,9 @@ class KDCBaseTest(TestCaseInTempDir, RawKerberosTest):
         if assigned_silo is not None:
             details['msDS-AssignedAuthNPolicySilo'] = assigned_silo
 
+        if keycredlink is not None:
+            details['msDS-KeyCredentialLink'] = keycredlink
+
         if selective_auth_allowed_sid is not None:
             if account_type is self.AccountType.GROUP_MANAGED_SERVICE:
                 oclass = "msDS-GroupManagedServiceAccount"
diff --git a/python/samba/tests/krb5/key_trust_tests.py 
b/python/samba/tests/krb5/key_trust_tests.py
new file mode 100755
index 00000000000..78410b822eb
--- /dev/null
+++ b/python/samba/tests/krb5/key_trust_tests.py
@@ -0,0 +1,497 @@
+#!/usr/bin/env python3
+# Unix SMB/CIFS implementation.
+#
+# Tests for Key Trust authentication
+#
+# Based on pkinit_tests.py
+#
+# Copyright (C) Gary Lockyer <[email protected]> 2025
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import sys
+import os
+
+sys.path.insert(0, "bin/python")
+os.environ["PYTHONUNBUFFERED"] = "1"
+
+from datetime import datetime, timedelta
+
+from cryptography import x509
+from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa, padding
+from cryptography.x509.oid import NameOID
+
+from samba.dcerpc import security
+from samba.domain.models import User
+from samba.key_credential_link import create_key_credential_link
+from samba.tests.krb5 import kcrypto
+from samba.tests.krb5.kdc_base_test import KDCBaseTest
+from samba.tests.krb5.raw_testcase import PkInit, RawKerberosTest
+from samba.tests.krb5.rfc4120_constants import (
+    DES_EDE3_CBC,
+    KDC_ERR_CLIENT_NOT_TRUSTED,
+    NT_PRINCIPAL,
+    PADATA_PK_AS_REQ,
+)
+import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1
+
+SidType = RawKerberosTest.SidType
+
+global_asn1_print = False
+global_hexdump = False
+
+
+class KeyTrustTests(KDCBaseTest):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+
+    def setUp(self):
+        super().setUp()
+        self.do_asn1_print = global_asn1_print
+        self.do_hexdump = global_hexdump
+
+    def test_key_trust_user(self):
+        """
+        Test key trust logon for a normal account
+        """
+        client_creds = self._get_creds()
+        target_creds = self.get_service_creds()
+        certificate = self._create_certificate(client_creds)
+        public_key = certificate.public_bytes(Encoding.DER)
+        self._add_key_cred_link(client_creds, public_key)
+
+        self._pkinit_req(client_creds, target_creds, certificate)
+
+    def test_key_trust_user_mismatched_keys(self):
+        """
+        Test key trust logon for a normal account, where the certificate
+        public key does not match the msDS-KeyCredentialLink value
+        """
+        client_creds = self._get_creds()
+        target_creds = self.get_service_creds()
+        certificate = self._create_certificate(client_creds)
+
+        private_key = rsa.generate_private_key(
+            public_exponent=65537, key_size=2048, backend=default_backend()
+        )
+        public_key = private_key.public_key().public_bytes(
+            encoding=Encoding.DER, format=PublicFormat.SubjectPublicKeyInfo
+        )
+        self._add_key_cred_link(client_creds, public_key)
+
+        self._pkinit_req(
+            client_creds,
+            target_creds,
+            certificate,
+            expect_error=KDC_ERR_CLIENT_NOT_TRUSTED,
+        )
+
+    def test_key_trust_user_no_keys(self):
+        """
+        Test key trust logon for a normal account,
+        with no msDS-KeyCredentialLink
+        """
+        client_creds = self._get_creds()
+        target_creds = self.get_service_creds()
+        certificate = self._create_certificate(client_creds)
+
+        self._pkinit_req(
+            client_creds,
+            target_creds,
+            certificate,
+            expect_error=KDC_ERR_CLIENT_NOT_TRUSTED,
+        )
+
+    def test_key_trust_computer(self):
+        """
+        Test key trust logon with a computer account.
+        """
+        client_creds = self._get_creds(self.AccountType.COMPUTER)
+        target_creds = self.get_service_creds()
+        certificate = self._create_certificate(client_creds)
+        public_key = certificate.public_bytes(Encoding.DER)
+        self._add_key_cred_link(client_creds, public_key)
+
+        self._pkinit_req(client_creds, target_creds, certificate)
+
+    def test_key_trust_computer_mismatched_keys(self):
+        """
+        Test key trust logon for a computer account, where the certificate
+        public key does not match the msDS-KeyCredentialLink value
+        """
+        client_creds = self._get_creds(self.AccountType.COMPUTER)
+        target_creds = self.get_service_creds()
+        certificate = self._create_certificate(client_creds)
+
+        private_key = rsa.generate_private_key(
+            public_exponent=65537, key_size=2048, backend=default_backend()
+        )
+        public_key = private_key.public_key().public_bytes(
+            encoding=Encoding.DER, format=PublicFormat.SubjectPublicKeyInfo
+        )
+        self._add_key_cred_link(client_creds, public_key)
+
+        self._pkinit_req(
+            client_creds,
+            target_creds,
+            certificate,
+            expect_error=KDC_ERR_CLIENT_NOT_TRUSTED,
+        )
+
+    def test_key_trust_computer_no_keys(self):
+        """
+        Test key trust logon with a computer account,
+        with no msDS-KeyCredentialLink
+        """
+        client_creds = self._get_creds(self.AccountType.COMPUTER)
+        target_creds = self.get_service_creds()
+        certificate = self._create_certificate(client_creds)
+
+        self._pkinit_req(
+            client_creds,
+            target_creds,
+            certificate,
+            expect_error=KDC_ERR_CLIENT_NOT_TRUSTED,
+        )
+
+    def _get_creds(self, account_type=KDCBaseTest.AccountType.USER, 
use_cache=False):
+        """
+        Return credentials with an account having a UPN for performing
+        PK-INIT.
+
+        Modified from the version in python/samba/tests/krb5/pkinit_tests.py
+        """
+        samdb = self.get_samdb()
+        realm = samdb.domain_dns_name().upper()
+
+        opts = {"upn": f"{{account}}.{realm}@{realm}"}
+
+        return self.get_cached_creds(
+            account_type=account_type, opts=opts, use_cache=use_cache
+        )
+
+    def _add_key_cred_link(self, creds, public_key):
+        """
+        Update the msDS-KeyCredentialLink for the user specified in creds with
+        the supplied public key
+        """
+        link = create_key_credential_link(self.get_samdb(), creds.get_dn(), 
public_key)
+
+        user = User.find(self.get_samdb(), creds.get_username())
+        self.assertIsNotNone(user)
+        if user is not None:
+            user.key_credential_link = link
+            user.save(self.get_samdb())
+
+    def _create_certificate(self, creds):
+        """
+        Create a new self signed certificate
+
+        Modified from the version in python/samba/tests/krb5/pkinit_tests.py
+        """
+
+        certificate_signature = hashes.SHA256
+
+        user_name = creds.get_username()
+
+        builder = x509.CertificateBuilder()
+
+        # Add the subject name.
+        cert_name = f"{user_name}@{creds.get_realm().lower()}"
+        subject_name = x509.Name(
+            [
+                # This name can be anything; it isn’t needed to authorize the
+                # user. The SubjectAlternativeName is used for that instead.
+                x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
+                x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, 
"SambaState"),
+                x509.NameAttribute(NameOID.ORGANIZATION_NAME, 
"SambaSelfTesting"),
+                x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Users"),
+                x509.NameAttribute(NameOID.COMMON_NAME, f"{cert_name}"),
+            ]
+        )
+        builder = builder.subject_name(subject_name)
+
+        # The new certificate is self signed
+        builder = builder.issuer_name(subject_name)
+
+        one_day = timedelta(1, 0, 0)
+
+        # Put the certificate start time in the past to avoid issues where the
+        # KDC considers the certificate to be invalid due to clock skew. Note
+        # that if the certificate predates the existence of the account in AD,
+        # Windows will refuse authentication unless a strong mapping is
+        # present (in the certificate, or in AD).
+        # See 
https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16#ID0EFR
+        builder = builder.not_valid_before(datetime.today() - one_day)
+        builder = builder.not_valid_after(datetime.today() + (one_day * 30))
+
+        builder = builder.serial_number(x509.random_serial_number())
+
+        public_key = creds.get_public_key()
+        builder = builder.public_key(public_key)
+
+        # Add the SubjectAlternativeName. Windows uses this to map the account
+        # to the certificate.
+        id_pkinit_ms_san = 
x509.ObjectIdentifier(str(krb5_asn1.id_pkinit_ms_san))
+        encoded_upn = self.der_encode(creds.get_upn(), 
asn1Spec=krb5_asn1.MS_UPN_SAN())
+        ms_upn_san = x509.OtherName(id_pkinit_ms_san, encoded_upn)
+        builder = builder.add_extension(
+            x509.SubjectAlternativeName([ms_upn_san]),
+            critical=False,
+        )
+
+        # The key identifier is used to identify the certificate.
+        subject_key_id = x509.SubjectKeyIdentifier.from_public_key(public_key)
+        builder = builder.add_extension(
+            subject_key_id,
+            critical=True,
+        )
+
+        # Add the key usages for which this certificate is valid.
+        builder = builder.add_extension(
+            # Heimdal requires that the certificate be valid for digital
+            # signatures.
+            x509.KeyUsage(digital_signature=True,
+                          content_commitment=False,
+                          key_encipherment=False,
+                          data_encipherment=False,
+                          key_agreement=False,
+                          key_cert_sign=False,
+                          crl_sign=False,
+                          encipher_only=False,
+                          decipher_only=False),
+                          critical=True,
+        )
+
+        certificate = builder.sign(
+            private_key=creds.get_private_key(),
+            algorithm=certificate_signature(),
+            backend=default_backend(),
+        )
+
+        return certificate
+
+    def _pkinit_req(
+        self,
+        creds,
+        target_creds,
+        certificate,
+        expect_error=0,
+    ):
+        """
+        Modified from the version in python/samba/tests/krb5/pkinit_tests.py
+        """
+        signature_algorithm = krb5_asn1.sha1WithRSAEncryption
+        signature_algorithm_id = 
self.AlgorithmIdentifier_create(signature_algorithm)
+
+        private_key = creds.get_private_key()
+
+        preauth_key = private_key
+
+        pk_nonce = self.get_Nonce()
+
+        def generate_pk_padata(_kdc_exchange_dict, _callback_dict, req_body):
+            # Suppress unused parameter warnings
+            _ = (_kdc_exchange_dict, _callback_dict)
+
+            checksum_blob = self.der_encode(req_body, 
asn1Spec=krb5_asn1.KDC_REQ_BODY())
+
+            # Calculate the SHA1 checksum over the KDC-REQ-BODY. This checksum
+            # is required to be present in the authenticator, and must be SHA1.
+            digest = hashes.Hash(hashes.SHA1(), default_backend())
+            digest.update(checksum_blob)
+            digest = digest.finalize()
+
+            ctime, cusec = self.get_KerberosTimeWithUsec()
+
+            # Create the authenticator, which shows that we had possession of
+            # the private key at some point.
+            authenticator_obj = self.PKAuthenticator_create(
+                cusec, ctime, pk_nonce, pa_checksum=digest
+            )
+
+            client_public_value = None
+
+            # An optional set of algorithms supported by the client in
+            # decreasing order of preference. For whatever reason, if this
+            # field is missing or empty, Windows will respond with a slightly
+            # differently encoded ReplyKeyPack, wrapping it first in a
+            # ContentInfo structure.
+            supported_cms_types = [
+                self.AlgorithmIdentifier_create(
+                    krb5_asn1.id_pkcs1_sha256WithRSAEncryption
+                ),
+            ]
+            auth_pack_obj = self.AuthPack_create(
+                authenticator_obj,
+                client_public_value=client_public_value,
+                supported_cms_types=supported_cms_types,
+            )
+
+            asn1_spec = krb5_asn1.AuthPack
+            auth_pack = self.der_encode(auth_pack_obj, asn1Spec=asn1_spec())
+
+            signature_hash = self.hash_from_algorithm(signature_algorithm)
+
+            pad = padding.PKCS1v15()
+            signed = private_key.sign(
+                auth_pack, padding=pad, algorithm=signature_hash()
+            )
+
+            encap_content_info_obj = self.EncapsulatedContentInfo_create(
+                krb5_asn1.id_pkinit_authData, auth_pack
+            )
+
+            subject_key_id = certificate.extensions.get_extension_for_oid(
+                x509.ExtensionOID.SUBJECT_KEY_IDENTIFIER
+            )
+            signer_identifier = self.SignerIdentifier_create(
+                subject_key_id=subject_key_id.value.digest
+            )
+
+            signer_info = self.SignerInfo_create(
+                signer_identifier,
+                signature_algorithm_id,
+                signature_algorithm_id,
+                signed,
+                signed_attrs=[
+                    # Note: these attributes are optional.
+                    krb5_asn1.id_pkinit_authData,
+                    krb5_asn1.id_messageDigest,
+                ],
+            )
+
+            encoded_cert = certificate.public_bytes(serialization.Encoding.DER)
+            decoded_cert = self.der_decode(
+                encoded_cert, asn1Spec=krb5_asn1.CertificateChoices()
+            )
+
+            signed_auth_pack = self.SignedData_create(
+                [signature_algorithm_id],
+                encap_content_info_obj,
+                signer_infos=[signer_info],
+                certificates=[decoded_cert],
+                crls=None,
+            )
+
+            signed_auth_pack = self.der_encode(
+                signed_auth_pack, asn1Spec=krb5_asn1.SignedData()
+            )
+
+            pk_as_req = self.PK_AS_REQ_create(signed_auth_pack)
+
+            pa_type = PADATA_PK_AS_REQ
+            padata = [self.PA_DATA_create(pa_type, pk_as_req)]
+
+            return padata, req_body
+
+        user_name = creds.get_username()
+        cname = self.PrincipalName_create(
+            name_type=NT_PRINCIPAL, names=user_name.split("/")
+        )
+
+        target_name = target_creds.get_username()
+        target_realm = target_creds.get_realm()
+
+        target_sname = self.PrincipalName_create(
+            name_type=NT_PRINCIPAL, names=["host", target_name[:-1]]
+        )
+        expected_sname = self.PrincipalName_create(
+            name_type=NT_PRINCIPAL, names=[target_name]
+        )
+
+        if expect_error:
+            check_error_fn = self.generic_check_kdc_error
+            check_rep_fn = None
+
+            expected_sname = target_sname
+        else:
+            check_error_fn = None
+            check_rep_fn = self.generic_check_kdc_rep
+
+        kdc_options = "forwardable,renewable,canonicalize,renewable-ok"
+        kdc_options = krb5_asn1.KDCOptions(kdc_options)
+
+        ticket_decryption_key = 
self.TicketDecryptionKey_from_creds(target_creds)
+
+        expected_groups = None
+        unexpected_groups = {
+            (
+                security.SID_FRESH_PUBLIC_KEY_IDENTITY,
+                SidType.EXTRA_SID,
+                security.SE_GROUP_DEFAULT_FLAGS,
+            )
+        }
+
+        kdc_exchange_dict = self.as_exchange_dict(
+            creds=creds,
+            client_cert=certificate,
+            expected_crealm=creds.get_realm(),
+            expected_cname=cname,
+            expected_srealm=target_realm,
+            expected_sname=expected_sname,
+            expected_supported_etypes=target_creds.tgs_supported_enctypes,
+            expected_groups=expected_groups,
+            unexpected_groups=unexpected_groups,
+            ticket_decryption_key=ticket_decryption_key,
+            generate_padata_fn=generate_pk_padata,
+            check_error_fn=check_error_fn,
+            check_rep_fn=check_rep_fn,
+            check_kdc_private_fn=self.generic_check_kdc_private,
+            expected_error_mode=expect_error,
+            expected_salt=creds.get_salt(),
+            preauth_key=preauth_key,
+            kdc_options=str(kdc_options),
+            using_pkinit=PkInit.PUBLIC_KEY,
+            pk_nonce=pk_nonce,


-- 
Samba Shared Repository

Reply via email to