The branch, master has been updated
       via  70bd05c2251 dsdb:audit log: cmocka unit tests for KCL
       via  c9e752ab18f dsdb:audit: bump minor version of password logs (1.1 -> 
1.2)
       via  139caa8adb2 dsdb:audit: log if msDS-KeyCredentialLink changed
       via  0c4853fd858 dsdb:audit: password_change loggers take a new flag 
argument
       via  976ff207da3 dsdb: audit: replace local min() with libreplace MIN()
       via  387731687bf dsdb:audit:test: test msDS-KeyCredentialLink change 
logging
       via  d094cd9fea8 librpc:idl: add windows event 5136 object change
      from  3d9b1dd3552 winbindd: Simplify parse_domain_user()

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


- Log -----------------------------------------------------------------
commit 70bd05c225153222c889fb4ef5e5c611d6631a18
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Sun Aug 31 21:24:34 2025 +1200

    dsdb:audit log: cmocka unit tests for KCL
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>
    
    Autobuild-User(master): Douglas Bagnall <dbagn...@samba.org>
    Autobuild-Date(master): Wed Sep  3 03:13:47 UTC 2025 on atb-devel-224

commit c9e752ab18f43758d704951f7f31e39dafa6fdb4
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Thu Aug 28 17:09:43 2025 +1200

    dsdb:audit: bump minor version of password logs (1.1 -> 1.2)
    
    In https://wiki.samba.org/index.php/Interpreting_JSON_Audit_Logs we say:
    
    > a version number for the JSON format. It has two parts.
    >
    >    major: incremented if fields change meaning
    >    minor: incremented if a field is added
    >
    > A change in possible values does not usually trigger a version
    > change. This is obviously true for client supplied data, but also
    > applies to e.g. passwordType, where the set of supported password
    > formats can change over time without changing the JSON version.
    
    The last paragraph explicitly exempts us from a version change for
    adding a new possible value ("Public key change") to the "action"
    field. On the other hand we have expanded the scope of the log, which
    deserves some version recognition.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 139caa8adb2396e36eb195c593e91d7e37038315
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Thu Aug 28 17:02:34 2025 +1200

    dsdb:audit: log if msDS-KeyCredentialLink changed
    
    As noted in the comments, by "changed" we mean "set" or "unset".
    Explicitly re-setting to the current value will be logged as if it
    were a change.
    
    This follows the behaviour of the password fields.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 0c4853fd8580413542a1a5fcc5fcb82191983689
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Thu Aug 28 17:03:51 2025 +1200

    dsdb:audit: password_change loggers take a new flag argument
    
    This will allow a key credential link change to be logged, but we
    don't do that anywhere in this commit.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 976ff207da315f05ed1cc967f1eb3c1e16aadbe6
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Wed Aug 27 16:02:56 2025 +1200

    dsdb: audit: replace local min() with libreplace MIN()
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 387731687bfcaddf7c1190899ad64a5ee35891ef
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Fri Aug 29 09:57:48 2025 +1200

    dsdb:audit:test: test msDS-KeyCredentialLink change logging
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit d094cd9fea8cfeb49bb6eeebef8dd706fc47ce51
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Thu Aug 28 12:37:13 2025 +1200

    librpc:idl: add windows event 5136 object change
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

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

Summary of changes:
 librpc/idl/windows_event_ids.idl                   |  12 +-
 python/samba/tests/audit_log_pass_change.py        | 289 ++++++++++++++++++++-
 source4/dsdb/samdb/ldb_modules/audit_log.c         | 104 +++++++-
 source4/dsdb/samdb/ldb_modules/audit_util.c        |   4 +-
 .../dsdb/samdb/ldb_modules/tests/test_audit_log.c  | 163 +++++++++++-
 .../ldb_modules/tests/test_audit_log_errors.c      |  10 +-
 6 files changed, 555 insertions(+), 27 deletions(-)


Changeset truncated at 500 lines:

diff --git a/librpc/idl/windows_event_ids.idl b/librpc/idl/windows_event_ids.idl
index f482800d897..04f0cc46720 100644
--- a/librpc/idl/windows_event_ids.idl
+++ b/librpc/idl/windows_event_ids.idl
@@ -25,7 +25,17 @@ interface windows_events
                EVT_ID_USER_ADDED_TO_UNIVERSAL_SEC_GROUP                = 4756,
                EVT_ID_USER_REMOVED_FROM_UNIVERSAL_SEC_GROUP            = 4757,
                EVT_ID_USER_ADDED_TO_UNIVERSAL_GROUP                    = 4761,
-               EVT_ID_USER_REMOVED_FROM_UNIVERSAL_GROUP                = 4762
+               EVT_ID_USER_REMOVED_FROM_UNIVERSAL_GROUP                = 4762,
+               /*
+                * Any change to any object will cause event 5136 in
+                * Windows AD -- if that object has a SACL asking for
+                * auditing.
+                *
+                * This event is used for msDS-KeyCredentialLink
+                * changes which do not have a specific event code.
+                */
+               EVT_ID_DIRECTORY_OBJECT_CHANGE                          = 5136
+
        } event_id_type;
 
        /* See 
https://learn.microsoft.com/en-us/windows-server/security/credentials-protection-and-management/authentication-policies-and-authentication-policy-silos#BKMK_ErrorandEvents
 */
diff --git a/python/samba/tests/audit_log_pass_change.py 
b/python/samba/tests/audit_log_pass_change.py
index 1039e17a180..2aa4be48ce0 100644
--- a/python/samba/tests/audit_log_pass_change.py
+++ b/python/samba/tests/audit_log_pass_change.py
@@ -26,10 +26,14 @@ import os
 from samba.tests.audit_log_base import AuditLogTestBase
 from samba.tests import delete_force
 from samba.net import Net
-from ldb import ERR_INSUFFICIENT_ACCESS_RIGHTS
+from ldb import (LdbError,
+                 ERR_INSUFFICIENT_ACCESS_RIGHTS,
+                 ERR_INVALID_DN_SYNTAX,
+                 ERR_NO_SUCH_ATTRIBUTE)
 from samba.dcerpc.windows_event_ids import (
     EVT_ID_PASSWORD_CHANGE,
-    EVT_ID_PASSWORD_RESET
+    EVT_ID_PASSWORD_RESET,
+    EVT_ID_DIRECTORY_OBJECT_CHANGE
 )
 
 
@@ -329,3 +333,284 @@ class AuditLogPassChangeTests(AuditLogTestBase):
         self.assertEqual(service_description, "LDAP")
         self.assertTrue(self.is_guid(audit["sessionId"]))
         self.assertTrue(self.is_guid(audit["transactionId"]))
+
+
+    # These are some valid BinaryDN prefixes for
+    # msDS-KeyCredentialLink values.
+    kcl_prefixes = [
+        ("B:772:000200002000012849ABF23C432F5428756A641827A6CD2E4A4F40"
+         "9F4DE57BA152B7C2F31E46EC200002A5499B70EC0FBEF4BCB78AF3878D08"
+         "2B2D43527BBAA230462707947C128BAF4226010330820122300D06092A86"
+         "4886F70D01010105000382010F003082010A02820101009A16283EA51D94"
+         "11FD54C73615D90E4B3B8DAF152D699A79C3B217DDBA7787DE419E3E31E6"
+         "012088FD9B4EC0EA9199E91EBE2F99F48A4E7084D819CF76C67C58F5050E"
+         "E5428B09676CA475EADFAD91FBEF2835820BE4CDE3F3A3B33DB6A0C75956"
+         "47CA8489566EBCF6B748DC637EC2D34D255E008DAF93F3DC77478B7526DE"
+         "5A6B4638FC50A622233C2BBA935A0E2BBA6DDCE32D539E57AD12FAA7BA33"
+         "1C92D32E54E05290132E66D25476458FDDFBF4940DA97F19B63C30EFE0EF"
+         "1F77A2751CD8EAA7EF6DFA54FF0B3500D9E66A1ECC9D435FF0206EA27047"
+         "8A37CD9F949EB6CA82925A138C5852C5C763AD6EA60003FCE5C7D486382B"
+         "8E347B8CF6645F449902030100010100040108000900CB9A52E817DC01:"),
+
+        ("B:772:00020000200001E251886DDA8335FA6A9B311F1FE4ABCDA7C04F05"
+         "33668BA7B143760C675703BB200002E324A0CCE2393C603686A0C7828B53"
+         "77A9B5E8C639131F657C3CEB39EDED7D4F26010330820122300D06092A86"
+         "4886F70D01010105000382010F003082010A0282010100A5DF595B4E0F36"
+         "9A4EF8CD5A2F6AF9322460C14175DDB7DE5F3A494A7591EC1096A5150800"
+         "F80401F7D8739C8900165035E231E85AA0E39A9C75BE760764BCDC82A4CB"
+         "955D84AFA47A8CACC35BAB6775478BF214A81726263D79CAA1623EA60DAB"
+         "2C61CAE29905FE4A89467736F47ADCA27170DBC77F5A82E28075E4D44FB9"
+         "76FFF5400B1AFC21B57C52718864253D57E780DEF8F40C990D30FB253831"
+         "BC95B01EEFFB312E52F7B773CFB1FF9E48A40B9C82E6B176464087C862F5"
+         "EA1D38809CF6454A8F169B993FFA857D1D928E4488EB13C947EE847D30F1"
+         "7CF1FAA85DE6AFD07ED82504C024E0CEA5B47E02515E57C3258963E80137"
+         "15EB0D3B126067A79F02030100010100040108000900CB9A52E817DC01:"),
+
+        ("B:772:0002000020000118ADEEC2CBCC63FCA44F332CAE39275E12CB881F"
+         "68B582ED2F3982718832126120000269F60B033A8697DB69835A356C9263"
+         "1CF87FDE6E448F3426285B3978E85C385C26010330820122300D06092A86"
+         "4886F70D01010105000382010F003082010A0282010100CADFE83AC8FCE1"
+         "B7A999AE162DC6BD6CC53D686E6F0CB866AFADCF64F736249E2CB4F438E2"
+         "78B636C4151C540BC4677821E66E6D88E6875A6B9B4F473D41C1C05376A4"
+         "F929076E515698F076F1F1BBEB25AAE062C9973D6436E3E4B48F74B5C0A5"
+         "B1FAD5026B8AB9E021849FFF8D18E54643C5F5FB57590BDC1CBC747C53CE"
+         "A696D5379EE3678E8AE015E2AE4AEA5B79F03E953D415986197F00D876E1"
+         "D23A28CC944901FCE5A902C9671CC46D7AA7F349F4F80CEF74FF035AEAEE"
+         "30D6FD8F907B08C9A23031B9E44AC84A4010BFA06A468D94B034DDF202A7"
+         "1B20663864703AC8205CE7C60B5FC18253C94CB8389381E05DB70B08BCCC"
+         "EE58A5E81BA2FE18AD02030100010100040108000980613353E817DC01:"),
+    ]
+
+    def test_ldap_key_credential_link(self):
+        """Test logging of msDS-KeyCredentialLink public key changes.
+        """
+        # The restrictions on msDS-KeyCredentialLink changes work
+        # different from those on the password attributes. We don't
+        # care about dsHeuristics, minPwdAge, etc, but we do care
+        # about things like GUID_DRS_DS_VALIDATED_WRITE_COMPUTER and
+        # ACLs.
+        #
+        # To avoid all the set-up cost of making a fresh DB and user,
+        # we use sub-tests in this test.
+        #
+        # See source4/dsdb/tests/pythpn/key_credential_link.py for
+        # deeper tests of the restrictions on setting this attribute.
+
+        dn = f"cn={USER_NAME},cn=users,{self.base_dn}"
+        self.discardSetupMessages(dn)
+
+        kcls = [f"{x}{dn}" for x in self.kcl_prefixes]
+        session_id = self.get_session()
+        service_description = self.get_service_description()
+        self.assertEqual(service_description, "LDAP")
+        transactions_seen = set()
+
+        with self.subTest("initial setup"):
+            self.ldb.modify_ldif(
+                f"dn: {dn}\n"
+                "changetype: modify\n"
+                "add: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: {kcls[0]}\n")
+            messages = self.waitForMessages(1, dn=dn)
+            print("Received %d messages" % len(messages))
+            self.assertEqual(1, len(messages))
+            audit = messages[0]["passwordChange"]
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, audit["eventId"])
+            self.assertEqual("Public key change", audit["action"])
+            self.assertEqual(dn, audit["dn"])
+            self.assertIn(self.remoteAddress, audit["remoteAddress"])
+            self.assertEqual(session_id, audit["sessionId"])
+            transactions_seen.add(audit["transactionId"])
+            self.assertEqual(0, audit["statusCode"])
+            self.discardMessages()
+
+        with self.subTest("replace"):
+            self.ldb.modify_ldif(
+                f"dn: {dn}\n"
+                "changetype: modify\n"
+                "replace: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: {kcls[1]}\n")
+            messages = self.waitForMessages(1, dn=dn)
+            print("Received %d messages" % len(messages))
+            self.assertEqual(1, len(messages))
+            audit = messages[0]["passwordChange"]
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, audit["eventId"])
+            self.assertEqual("Public key change", audit["action"])
+            self.assertEqual(dn, audit["dn"])
+            self.assertIn(self.remoteAddress, audit["remoteAddress"])
+            self.assertTrue(self.is_guid(audit["sessionId"]))
+            transactions_seen.add(audit["transactionId"])
+            self.discardMessages()
+
+        with self.subTest("constrained replace"):
+            self.ldb.modify_ldif(
+                f"dn: {dn}\n"
+                "changetype: modify\n"
+                "delete: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: {kcls[1]}\n"
+                "add: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: {kcls[2]}\n")
+            messages = self.waitForMessages(1, dn=dn)
+            print("Received %d messages" % len(messages))
+            self.assertEqual(1, len(messages))
+            audit = messages[0]["passwordChange"]
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, audit["eventId"])
+            self.assertEqual("Public key change", audit["action"])
+            self.assertEqual(dn, audit["dn"])
+            self.assertIn(self.remoteAddress, audit["remoteAddress"])
+            self.assertEqual(session_id, audit["sessionId"])
+            self.assertEqual(0, audit["statusCode"])
+            transactions_seen.add(audit["transactionId"])
+            self.discardMessages()
+
+        with self.subTest("identical replace"):
+            # replacing the KCL with itself still sends the message.
+            self.ldb.modify_ldif(
+                f"dn: {dn}\n"
+                "changetype: modify\n"
+                "replace: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: {kcls[2]}\n")
+            messages = self.waitForMessages(1, dn=dn)
+            print("Received %d messages" % len(messages))
+            self.assertEqual(1, len(messages))
+            audit = messages[0]["passwordChange"]
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, audit["eventId"])
+            self.assertEqual("Public key change", audit["action"])
+            self.assertEqual(dn, audit["dn"])
+            self.assertIn(self.remoteAddress, audit["remoteAddress"])
+            self.assertEqual(session_id, audit["sessionId"])
+            self.assertEqual(0, audit["statusCode"])
+            transactions_seen.add(audit["transactionId"])
+            self.discardMessages()
+
+        with self.subTest("replace KCL AND password"):
+            # there should be two messages
+            self.ldb.modify_ldif(
+                f"dn: {dn}\n"
+                "changetype: modify\n"
+                "replace: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: {kcls[0]}\n"
+                "replace: userPassword\n"
+                "userPassword: gruffalo3.\n")
+            messages = self.waitForMessages(2, dn=dn)
+            self.assertEqual(2, len(messages))
+            pwd_audit = messages[0]["passwordChange"]
+            kcl_audit = messages[1]["passwordChange"]
+            # we send the password message first, but we don't need to
+            # depend on that.
+            if pwd_audit["eventId"] == EVT_ID_DIRECTORY_OBJECT_CHANGE:
+                kcl_audit, pwd_audit = pwd_audit, kcl_audit
+            del audit
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, 
kcl_audit["eventId"])
+            self.assertEqual(EVT_ID_PASSWORD_RESET, pwd_audit["eventId"])
+            self.assertEqual("Public key change", kcl_audit["action"])
+            self.assertEqual("Reset", pwd_audit["action"])
+            # if we delete the action and eventId, the rest of
+            # structures should be the same (sessionId, transactionId,
+            # version, etc). Timestamps are in the outer message.
+            del pwd_audit["eventId"]
+            del pwd_audit["action"]
+            del kcl_audit["eventId"]
+            del kcl_audit["action"]
+            self.assertEqual(kcl_audit, pwd_audit)
+            transactions_seen.add(pwd_audit["transactionId"])
+            self.discardMessages()
+
+        with self.subTest("delete"):
+            # replacing the KCL with itself still sends the message.
+            self.ldb.modify_ldif(
+                f"dn: {dn}\n"
+                "changetype: modify\n"
+                "delete: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: {kcls[0]}\n")
+            messages = self.waitForMessages(1, dn=dn)
+            print("Received %d messages" % len(messages))
+            self.assertEqual(1, len(messages))
+            audit = messages[0]["passwordChange"]
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, audit["eventId"])
+            self.assertEqual("Public key change", audit["action"])
+            self.assertEqual(dn, audit["dn"])
+            self.assertIn(self.remoteAddress, audit["remoteAddress"])
+            self.assertEqual(session_id, audit["sessionId"])
+            self.assertEqual(0, audit["statusCode"])
+            transactions_seen.add(audit["transactionId"])
+            self.discardMessages()
+
+        with self.subTest("delete that which does not exist"):
+            # still sends a message
+            with self.assertRaises(LdbError) as e:
+                self.ldb.modify_ldif(
+                    f"dn: {dn}\n"
+                    "changetype: modify\n"
+                    "delete: msDS-keyCredentialLink\n"
+                    f"msDS-keyCredentialLink: {kcls[2]}\n")
+            self.assertEqual(e.exception.args[0], ERR_NO_SUCH_ATTRIBUTE)
+            messages = self.waitForMessages(1, dn=dn)
+            print("Received %d messages" % len(messages))
+            # We still get the message on a failed attempt
+            self.assertEqual(1, len(messages))
+            audit = messages[0]["passwordChange"]
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, audit["eventId"])
+            self.assertEqual("Public key change", audit["action"])
+            self.assertEqual(dn, audit["dn"])
+            self.assertIn(self.remoteAddress, audit["remoteAddress"])
+            self.assertEqual(session_id, audit["sessionId"])
+            transactions_seen.add(audit["transactionId"])
+            self.discardMessages()
+            with self.subTest("check status code"):
+                self.assertEqual(ERR_NO_SUCH_ATTRIBUTE, audit["statusCode"])
+                self.assertEqual("No such attribute", audit["status"])
+
+        with self.subTest("add bad KCL DN value"):
+            self.ldb.modify_ldif(
+                f"dn: {dn}\n"
+                "changetype: modify\n"
+                "replace: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: B:4:f1ea:{dn}\n")
+            messages = self.waitForMessages(1, dn=dn)
+            self.discardMessages()
+            audit = messages[0]["passwordChange"]
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, audit["eventId"])
+            self.assertEqual("Public key change", audit["action"])
+            self.assertEqual(dn, audit["dn"])
+            self.assertIn(self.remoteAddress, audit["remoteAddress"])
+            self.assertEqual(session_id, audit["sessionId"])
+            self.assertEqual(0, audit["statusCode"])
+            transactions_seen.add(audit["transactionId"])
+
+        # these should all have been separate transactions
+        with self.subTest("check transactions"):
+            self.assertEqual(len(transactions_seen), 8)
+            for t in transactions_seen:
+                self.assertTrue(self.is_guid(t))
+
+        with self.subTest("add a second DN value"):
+            # should this fail?
+            self.ldb.modify_ldif(
+                f"dn: {dn}\n"
+                "changetype: modify\n"
+                "add: msDS-keyCredentialLink\n"
+                f"msDS-keyCredentialLink: B:4:f1ee:{dn}\n")
+            messages = self.waitForMessages(1, dn=dn)
+            self.discardMessages()
+            audit = messages[0]["passwordChange"]
+            self.assertEqual(EVT_ID_DIRECTORY_OBJECT_CHANGE, audit["eventId"])
+            self.assertEqual("Public key change", audit["action"])
+            self.assertEqual(dn, audit["dn"])
+            self.assertIn(self.remoteAddress, audit["remoteAddress"])
+            self.assertEqual(session_id, audit["sessionId"])
+            self.assertEqual(0, audit["statusCode"])
+
+        with self.subTest("add bad Binary DN value"):
+            for bad_dn in ('B:6:f1ea:{dn}', 'flea', dn):
+                with self.assertRaises(LdbError) as e:
+                    self.ldb.modify_ldif(
+                        f"dn: {dn}\n"
+                        "changetype: modify\n"
+                        "replace: msDS-keyCredentialLink\n"
+                        f"msDS-keyCredentialLink: {bad_dn}\n")
+                self.assertEqual(e.exception.args[0], ERR_INVALID_DN_SYNTAX)
+            # no messages from those the 3 bad DNs
+            # because DN syntax check comes first
+            messages = self.waitForMessages(1, dn=dn)
+            self.assertEqual(0, len(messages))
diff --git a/source4/dsdb/samdb/ldb_modules/audit_log.c 
b/source4/dsdb/samdb/ldb_modules/audit_log.c
index 7cc3ff67d77..dd730c82e0a 100644
--- a/source4/dsdb/samdb/ldb_modules/audit_log.c
+++ b/source4/dsdb/samdb/ldb_modules/audit_log.c
@@ -44,7 +44,7 @@
 #define PASSWORD_JSON_TYPE "passwordChange"
 #define PASSWORD_HR_TAG "Password Change"
 #define PASSWORD_MAJOR 1
-#define PASSWORD_MINOR 1
+#define PASSWORD_MINOR 2
 #define PASSWORD_LOG_LVL 5
 
 #define TRANSACTION_JSON_TYPE "dsdbTransaction"
@@ -65,8 +65,6 @@
  */
 #define MAX_LENGTH 1024
 
-#define min(a, b) (((a)>(b))?(b):(a))
-
 /*
  * Private data for the module, stored in the ldb_module private data
  */
@@ -121,6 +119,44 @@ static bool has_password_changed(const struct ldb_message 
*message)
        }
        return false;
 }
+/*
+ * @brief Has a public key been set or unset in this message.
+ *
+ * We treat msDS-KeyCredentialLink a bit like a password change,
+ * because it changes the remote certificate that is accepted.
+ *
+ * While this is not a secret, it is significant from a security point
+ * of view because, as openssh likes to say, IT IS POSSIBLE THAT
+ * SOMEONE IS DOING SOMETHING NASTY by changing trusted keys.
+ *
+ * A real password change only matters for this reason too. But a
+ * *read* of the password hash is a security event in a way that a
+ * read of msDS-KeyCredentialLink is not.
+ *
+ * That's why we don't add just public keys to DSDB_PASSWORD_ATTRIBUTES,
+ * which is used elsewhere to check secrecy.
+ *
+ * This does not actually check that the message will change the
+ * database -- a message setting msDS-KeyCredentialLink to its current
+ * value will still be logged as a change.
+ *
+ * @return true if the message contains a public key, which currently
+ * just means msDS-KeyCredentialLink.
+ */
+static bool has_public_key_changed(const struct ldb_message *message)
+{
+       unsigned int i;
+       if (message == NULL) {
+               return false;
+       }
+       for (i = 0; i<message->num_elements; i++) {
+               if (ldb_attr_cmp(message->elements[i].name,
+                                "msDS-KeyCredentialLink") == 0) {
+                       return true;
+               }
+       }
+       return false;
+}
 
 /*
  * @brief get the password change windows event id
@@ -483,7 +519,8 @@ failure:
 static struct json_object password_change_json(
        struct ldb_module *module,
        const struct ldb_request *request,
-       const struct ldb_reply *reply)
+       const struct ldb_reply *reply,
+       bool public_key_changed)
 {
        struct ldb_context *ldb = NULL;
        const struct dom_sid *sid = NULL;
@@ -504,10 +541,14 @@ static struct json_object password_change_json(
        remote = dsdb_audit_get_remote_address(ldb);
        sid = dsdb_audit_get_user_sid(module);
        dn = dsdb_audit_get_primary_dn(request);
-       action = get_password_action(request, reply);
        unique_session_token = dsdb_audit_get_unique_session_token(module);
-       event_id = get_password_windows_event_id(request, reply);
-
+       if (public_key_changed) {
+               action = "Public key change";
+               event_id = EVT_ID_DIRECTORY_OBJECT_CHANGE;
+       } else  {
+               action = get_password_action(request, reply);
+               event_id = get_password_windows_event_id(request, reply);
+       }
        audit = json_new_object();
        if (json_is_invalid(&audit)) {
                goto failure;
@@ -773,7 +814,8 @@ static char *password_change_human_readable(
        TALLOC_CTX *mem_ctx,
        struct ldb_module *module,
        const struct ldb_request *request,
-       const struct ldb_reply *reply)
+       const struct ldb_reply *reply,
+       bool is_public_key_change)
 {
        struct ldb_context *ldb = NULL;
        const char *remote_host = NULL;
@@ -791,7 +833,12 @@ static char *password_change_human_readable(
        remote_host = dsdb_audit_get_remote_host(ldb, ctx);
        sid = dsdb_audit_get_user_sid(module);
        timestamp = audit_get_timestamp(ctx);
-       action = get_password_action(request, reply);
+
+       if (is_public_key_change) {
+               action = "Public key change";
+       } else {
+               action = get_password_action(request, reply);
+       }
        dn = dsdb_audit_get_primary_dn(request);
 
        log_entry = talloc_asprintf(
@@ -881,7 +928,7 @@ static char *log_attributes(
                        }
 
                        v = message->elements[i].values[j];
-                       length = min(MAX_LENGTH, v.length);
+                       length = MIN(MAX_LENGTH, v.length);
                        use_b64_encode = ldb_should_b64_encode(ldb, &v);
                        if (use_b64_encode) {
                                const char *encoded = ldb_base64_encode(
@@ -1141,6 +1188,7 @@ static void log_standard_operation(
 
        const struct ldb_message *message = dsdb_audit_get_message(request);
        bool password_changed = has_password_changed(message);
+       bool public_key_changed = has_public_key_changed(message);
        struct audit_private *audit_private =
                talloc_get_type_abort(ldb_module_get_private(module),
                                      struct audit_private);
@@ -1168,7 +1216,23 @@ static void log_standard_operation(
                                ctx,
                                module,
                                request,
-                               reply);
+                               reply,
+                               false);
+                       audit_log_human_text(
+                               PASSWORD_HR_TAG,
+                               entry,
+                               DBGC_DSDB_PWD_AUDIT,
+                               PASSWORD_LOG_LVL);
+                       TALLOC_FREE(entry);
+               }
+               if (public_key_changed) {
+                       char *entry = NULL;
+                       entry = password_change_human_readable(
+                               ctx,
+                               module,
+                               request,
+                               reply,
+                               true);
                        audit_log_human_text(
                                PASSWORD_HR_TAG,
                                entry,
@@ -1201,7 +1265,23 @@ static void log_standard_operation(
                 && audit_private->send_password_events)) {
                if (password_changed) {
                        struct json_object json;
-                       json = password_change_json(module, request, reply);
+                       json = password_change_json(module, request, reply, 
false);
+                       audit_log_json(
+                               &json,
+                               DBGC_DSDB_PWD_AUDIT_JSON,
+                               PASSWORD_LOG_LVL);


-- 
Samba Shared Repository

Reply via email to