]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
ldb: add "policy hints" controls to be used by password_hash module
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>
Wed, 24 Sep 2025 23:45:30 +0000 (11:45 +1200)
committerDouglas Bagnall <dbagnall@samba.org>
Thu, 15 Jan 2026 01:48:37 +0000 (01:48 +0000)
These won't have any effect yet, but soon they will allow a privileged
account to perform a password reset that respects constraints on
password history, age, and length, as if the reset was an ordinary
password change (that is, where the user provides the old password).

A normal user can't reset their own password using this, if the
organisation is using a remote service (e.g. Entra ID or Keycloak) to
manage passwords, that service can use a policy hints control to
ensure it follows AD password policy.

Entra ID Self Service Password Reset (SSPR) uses the deprecated OID.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=12020

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
lib/ldb/common/ldb_controls.c
lib/ldb/include/ldb.h
source4/libcli/ldap/ldap_controls.c

index 3bf89d844f9037ee91dabf53717eefbd11535aed..a80c89964c3ddecba05c9ad981c1abc67ad805c4 100644 (file)
@@ -1296,6 +1296,52 @@ struct ldb_control *ldb_parse_control_from_string(struct ldb_context *ldb, TALLO
                ctrl->data = control;
                return ctrl;
        }
+
+       if (LDB_CONTROL_CMP(control_strings, LDB_CONTROL_POLICY_HINTS_NAME) == 0) {
+               const char *p = NULL;
+               int crit, val, ret;
+
+               p = &(control_strings[sizeof(LDB_CONTROL_POLICY_HINTS_NAME)]);
+               ret = sscanf(p, "%d:%d", &crit, &val);
+               if ((ret != 2) || (crit < 0) || (crit > 1)) {
+                       ldb_set_errstring(ldb,
+                                         "invalid pwd_policy_hints control syntax\n"
+                                         " syntax: crit(b):flags(n)\n"
+                                         "   note: b = boolean, n = number");
+                       talloc_free(ctrl);
+                       return NULL;
+               }
+
+               ctrl->oid = LDB_CONTROL_POLICY_HINTS_OID;
+               ctrl->critical = crit;
+               ctrl->data = talloc(ctrl, int);
+               *((int*)ctrl->data) = val;
+               return ctrl;
+       }
+
+       if (LDB_CONTROL_CMP(control_strings, LDB_CONTROL_POLICY_HINTS_DEPRECATED_NAME) == 0) {
+               const char *p = NULL;
+               int crit, val, ret;
+
+               p = &(control_strings[sizeof(LDB_CONTROL_POLICY_HINTS_DEPRECATED_NAME)]);
+               ret = sscanf(p, "%d:%d", &crit, &val);
+               if ((ret != 2) || (crit < 0) || (crit > 1)) {
+                       ldb_set_errstring(ldb,
+                                         "invalid pwd_policy_hints control syntax\n"
+                                         " syntax: crit(b):flags(n)\n"
+                                         "   note: b = boolean, n = number");
+                       talloc_free(ctrl);
+                       return NULL;
+               }
+
+               ctrl->oid = LDB_CONTROL_POLICY_HINTS_DEPRECATED_OID;
+               ctrl->critical = crit;
+               ctrl->data = talloc(ctrl, int);
+               *((int*)ctrl->data) = val;
+               return ctrl;
+       }
+
+
        /*
         * When no matching control has been found.
         */
index 0d1d455d0cffecdaa0b4fcc4a222b3f93deb1f07..4cd6ada194eb78176ccbcc5b4274f215e1d512d8 100644 (file)
@@ -808,6 +808,36 @@ typedef int (*ldb_qsort_cmp_fn_t) (void *v1, void *v2, void *opaque);
 */
 #define LDB_EXTENDED_WHOAMI_OID                "1.3.6.1.4.1.4203.1.11.3"
 
+/**
+   OID used to enforce password history length constraints in password reset
+
+   If this is control as the value 0x1 on a password set operation,
+   the password history constraints in MS-SAMR 3.1.1.7.1 "General
+   Password Policy" apply as if this was a password change. This may
+   be used by Entra ID or Keycloak.
+
+   LDB_CONTROL_POLICY_HINTS_DEPRECATED_OID (below) does exactly the
+   same thing and seems to be more widely used in practice.
+*/
+#define LDB_CONTROL_POLICY_HINTS_OID           "1.2.840.113556.1.4.2239"
+#define LDB_CONTROL_POLICY_HINTS_NAME           "policy_hints"
+
+/**
+   Another OID used to enforce password history length constraints in password reset
+
+   This works just like LDB_CONTROL_POLICY_HINTS_OID (above): if the
+   control value is 0x1 on a password set operation, the password
+   history constraints in MS-SAMR 3.1.1.7.1 "General Password Policy"
+   apply as if this was a password change.
+
+   This is used by Entra ID in a password change.
+
+   It is also the OID for the ms-DS-Required-Domain-Behavior-Version
+   attribute), which is unlikely to cause confusion given the contexts.
+*/
+#define LDB_CONTROL_POLICY_HINTS_DEPRECATED_OID      "1.2.840.113556.1.4.2066"
+#define LDB_CONTROL_POLICY_HINTS_DEPRECATED_NAME     "policy_hints_deprecated"
+
 struct ldb_sd_flags_control {
        /*
         * request the owner    0x00000001
index 8d3a40f602e31b3f0bd0ebebaf6dcc152b29dd53..a38c0846ab7e39ea468caf06bdd0752345076225 100644 (file)
@@ -684,6 +684,42 @@ static bool decode_vlv_response(void *mem_ctx, DATA_BLOB in, void *_out)
        return true;
 }
 
+static bool decode_policy_hints_request(void *mem_ctx, DATA_BLOB in, void *_out)
+{
+       void **out = (void **)_out;
+       int *valp = NULL;
+       int val;
+       struct asn1_data *data = asn1_init(mem_ctx, ASN1_MAX_TREE_DEPTH);
+       if (!data) {
+               return false;
+       }
+
+       if (!asn1_load(data, in)) {
+               return false;
+       }
+
+       if (!asn1_start_tag(data, ASN1_SEQUENCE(0))) {
+               return false;
+       }
+
+       if (!asn1_read_Integer(data, &val)) {
+               return false;
+       }
+
+       if (!asn1_end_tag(data)) {
+               return false;
+       }
+
+       valp = talloc(mem_ctx, int);
+       if (valp == NULL) {
+               return false;
+       }
+       *valp = val;
+       *out = valp;
+
+       return true;
+}
+
 static bool encode_server_sort_response(void *mem_ctx, void *in, DATA_BLOB *out)
 {
        struct ldb_sort_resp_control *lsrc = talloc_get_type(in, struct ldb_sort_resp_control);
@@ -1208,6 +1244,40 @@ static bool decode_openldap_dereference(void *mem_ctx, DATA_BLOB in, void *_out)
        return true;
 }
 
+
+
+static bool encode_policy_hints_request(void *mem_ctx, void *in, DATA_BLOB *out)
+{
+       int *val = talloc_get_type(in, int);
+       struct asn1_data *data = asn1_init(mem_ctx, ASN1_MAX_TREE_DEPTH);
+       if (!data) {
+               return false;
+       }
+
+       if (!asn1_push_tag(data, ASN1_SEQUENCE(0))) {
+               return false;
+       }
+
+       if (!asn1_write_Integer(data, *val)) {
+               return false;
+       }
+
+       if (!asn1_pop_tag(data)) {
+               return false;
+       }
+
+       if (!asn1_extract_blob(data, mem_ctx, out)) {
+               return false;
+       }
+
+       talloc_free(data);
+
+       return true;
+}
+
+
+
+
 static bool encode_flag_request(void *mem_ctx, void *in, DATA_BLOB *out)
 {
        if (in) {
@@ -1251,6 +1321,8 @@ static const struct ldap_control_handler ldap_known_controls[] = {
        { LDB_CONTROL_RELAX_OID, decode_flag_request, encode_flag_request },
        { DSDB_OPENLDAP_DEREFERENCE_CONTROL, decode_openldap_dereference, encode_openldap_dereference },
        { LDB_CONTROL_VERIFY_NAME_OID, decode_verify_name_request, encode_verify_name_request },
+       { LDB_CONTROL_POLICY_HINTS_OID, decode_policy_hints_request, encode_policy_hints_request },
+       { LDB_CONTROL_POLICY_HINTS_DEPRECATED_OID, decode_policy_hints_request, encode_policy_hints_request },
 
        /* the following are internal only, with a network
           representation */