s4:dsdb: Add functions for Group Managed Service Accounts implementation
authorJo Sutton <josutton@catalyst.net.nz>
Tue, 13 Feb 2024 03:09:57 +0000 (16:09 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Tue, 16 Apr 2024 03:58:31 +0000 (03:58 +0000)
Signed-off-by: Jo Sutton <josutton@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
source4/dsdb/gmsa/gkdi.c
source4/dsdb/gmsa/gkdi.h
source4/dsdb/gmsa/util.c [new file with mode: 0644]
source4/dsdb/gmsa/util.h [new file with mode: 0644]
source4/dsdb/wscript_build

index 3a79a5eb5fc8b4c9240cd3701e0747fe8c7002d1..917b13559587bf4d6429869f20f7084f62ab73a0 100644 (file)
 #include "librpc/gen_ndr/gkdi.h"
 #include "librpc/gen_ndr/ndr_gkdi.h"
 
+NTSTATUS gkdi_root_key_from_msg(TALLOC_CTX *mem_ctx,
+                               const struct GUID root_key_id,
+                               const struct ldb_message *const msg,
+                               const struct ProvRootKey **const root_key_out)
+{
+       NTSTATUS status = NT_STATUS_OK;
+       struct ldb_val root_key_data = {};
+       struct KdfAlgorithm kdf_algorithm = {};
+
+       const int version = ldb_msg_find_attr_as_int(msg, "msKds-Version", 0);
+       const NTTIME create_time = samdb_result_nttime(msg,
+                                                      "msKds-CreateTime",
+                                                      0);
+       const NTTIME use_start_time = samdb_result_nttime(msg,
+                                                         "msKds-UseStartTime",
+                                                         0);
+       const char *domain_id = ldb_msg_find_attr_as_string(msg,
+                                                           "msKds-DomainID",
+                                                           NULL);
+
+       {
+               const struct ldb_val *root_key_val = ldb_msg_find_ldb_val(
+                       msg, "msKds-RootKeyData");
+               if (root_key_val != NULL) {
+                       root_key_data = *root_key_val;
+               }
+       }
+
+       {
+               const char *algorithm_id = ldb_msg_find_attr_as_string(
+                       msg, "msKds-KDFAlgorithmID", NULL);
+               const struct ldb_val *kdf_param_val = ldb_msg_find_ldb_val(
+                       msg, "msKds-KDFParam");
+               status = kdf_algorithm_from_params(algorithm_id,
+                                                  kdf_param_val,
+                                                  &kdf_algorithm);
+               if (!NT_STATUS_IS_OK(status)) {
+                       goto out;
+               }
+       }
+
+       status = ProvRootKey(mem_ctx,
+                            root_key_id,
+                            version,
+                            root_key_data,
+                            create_time,
+                            use_start_time,
+                            domain_id,
+                            kdf_algorithm,
+                            root_key_out);
+       if (!NT_STATUS_IS_OK(status)) {
+               goto out;
+       }
+
+out:
+       return status;
+}
+
+/*
+ * Calculate an appropriate useStartTime for a root key created at
+ * ‘current_time’.
+ *
+ * This function goes unused.
+ */
+NTTIME gkdi_root_key_use_start_time(const NTTIME current_time)
+{
+       const NTTIME start_time = gkdi_get_interval_start_time(current_time);
+
+       return start_time + gkdi_key_cycle_duration + gkdi_max_clock_skew;
+}
+
 static int gkdi_create_root_key(TALLOC_CTX *mem_ctx,
                                struct ldb_context *const ldb,
                                const NTTIME current_time,
@@ -504,3 +575,180 @@ out:
        talloc_free(tmp_ctx);
        return ret;
 }
+
+int gkdi_root_key_from_id(TALLOC_CTX *mem_ctx,
+                         struct ldb_context *const ldb,
+                         const struct GUID *const root_key_id,
+                         const struct ldb_message **const root_key_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       struct ldb_dn *root_key_dn = NULL;
+       struct ldb_result *res = NULL;
+       int ret = LDB_SUCCESS;
+
+       *root_key_out = NULL;
+
+       tmp_ctx = talloc_new(mem_ctx);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       root_key_dn = samdb_gkdi_root_key_dn(ldb, tmp_ctx, root_key_id);
+       if (root_key_dn == NULL) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+       ret = dsdb_search_dn(
+               ldb, tmp_ctx, &res, root_key_dn, root_key_attrs, 0);
+       if (ret) {
+               goto out;
+       }
+
+       if (res->count != 1) {
+               ret = dsdb_werror(ldb,
+                                 LDB_ERR_NO_SUCH_OBJECT,
+                                 W_ERROR(HRES_ERROR_V(HRES_NTE_NO_KEY)),
+                                 "failed to find root key");
+               goto out;
+       }
+
+       *root_key_out = talloc_steal(mem_ctx, res->msgs[0]);
+
+out:
+       talloc_free(tmp_ctx);
+       return ret;
+}
+
+int gkdi_most_recently_created_root_key(
+       TALLOC_CTX *mem_ctx,
+       struct ldb_context *const ldb,
+       _UNUSED_ const NTTIME current_time,
+       const NTTIME not_after,
+       struct GUID *const root_key_id_out,
+       const struct ldb_message **const root_key_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       struct ldb_result *res = NULL;
+       int ret = LDB_SUCCESS;
+
+       *root_key_out = NULL;
+
+       tmp_ctx = talloc_new(mem_ctx);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       {
+               struct ldb_dn *root_key_container_dn = NULL;
+
+               root_key_container_dn = samdb_gkdi_root_key_container_dn(
+                       ldb, tmp_ctx);
+               if (root_key_container_dn == NULL) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+
+               ret = dsdb_search(ldb,
+                                 tmp_ctx,
+                                 &res,
+                                 root_key_container_dn,
+                                 LDB_SCOPE_ONELEVEL,
+                                 root_key_attrs,
+                                 0,
+                                 "(msKds-UseStartTime<=%" PRIu64 ")",
+                                 not_after);
+               if (ret) {
+                       goto out;
+               }
+       }
+
+       /*
+        * Windows just gives up if there are more than 1000 root keys in the
+        * container.
+        */
+
+       {
+               struct root_key_candidate {
+                       struct GUID id;
+                       const struct ldb_message *key;
+                       NTTIME create_time;
+               } most_recent_key = {
+                       .key = NULL,
+               };
+               unsigned i;
+
+               for (i = 0; i < res->count; ++i) {
+                       struct root_key_candidate key = {
+                               .key = res->msgs[i],
+                       };
+                       const struct ldb_val *rdn_val = NULL;
+                       bool ok;
+
+                       key.create_time = samdb_result_nttime(
+                               key.key, "msKds-CreateTime", 0);
+                       if (key.create_time < most_recent_key.create_time) {
+                               /* We already have a more recent key. */
+                               continue;
+                       }
+
+                       rdn_val = ldb_dn_get_rdn_val(key.key->dn);
+                       if (rdn_val == NULL) {
+                               continue;
+                       }
+
+                       if (rdn_val->length != 36) {
+                               /*
+                                * Check the RDN is the right length — 36 is the
+                                * length of a UUID.
+                                */
+                               continue;
+                       }
+
+                       ok = parse_guid_string((const char *)rdn_val->data,
+                                              &key.id);
+                       if (!ok) {
+                               /* The RDN is not a correctly formatted GUID. */
+                               continue;
+                       }
+
+                       /*
+                        * We’ve found a new candidate for the most recent root
+                        * key.
+                        */
+                       most_recent_key = key;
+               }
+
+               if (most_recent_key.key == NULL) {
+                       /*
+                        * We were not able to find a suitable root key, but
+                        * there is a possibility that a key we create now will
+                        * do: if gkdi_root_key_use_start_time(current_time) ≤
+                        * not_after, then a newly‐created key will satisfy our
+                        * caller’s requirements.
+                        *
+                        * Unfortunately, with gMSAs this (I believe) will never
+                        * be the case. It’s too late to call
+                        * gkdi_new_root_key() — the new key will be a bit *too*
+                        * new to be usable for a gMSA.
+                        */
+
+                       ret = dsdb_werror(ldb,
+                                         LDB_ERR_NO_SUCH_OBJECT,
+                                         W_ERROR(HRES_ERROR_V(
+                                                 HRES_NTE_NO_KEY)),
+                                         "failed to find a suitable root key");
+                       goto out;
+               }
+
+               /* Return the root key that we found. */
+               *root_key_id_out = most_recent_key.id;
+               *root_key_out = talloc_steal(mem_ctx, most_recent_key.key);
+       }
+
+out:
+       talloc_free(tmp_ctx);
+       return ret;
+}
index e6e3606444534d054347185df4690aa0ccb52529..4c5394167fd4a9faa8b1e0e49935a49549ee734a 100644 (file)
 #include "librpc/gen_ndr/misc.h"
 
 struct ldb_message;
+struct ProvRootKey;
+NTSTATUS gkdi_root_key_from_msg(TALLOC_CTX *mem_ctx,
+                               const struct GUID root_key_id,
+                               const struct ldb_message *const msg,
+                               const struct ProvRootKey **const root_key_out);
+
+/*
+ * Calculate an appropriate useStartTime for a root key created at
+ * ‘current_time’.
+ *
+ * This function goes unused.
+ */
+NTTIME gkdi_root_key_use_start_time(const NTTIME current_time);
 
 /*
  * Create and return a new GKDI root key.
@@ -42,5 +55,17 @@ int gkdi_new_root_key(TALLOC_CTX *mem_ctx,
                      struct GUID *const root_key_id_out,
                      const struct ldb_message **const root_key_out);
 
+int gkdi_root_key_from_id(TALLOC_CTX *mem_ctx,
+                         struct ldb_context *const ldb,
+                         const struct GUID *const root_key_id,
+                         const struct ldb_message **const root_key_out);
+
+int gkdi_most_recently_created_root_key(
+       TALLOC_CTX *mem_ctx,
+       struct ldb_context *const ldb,
+       const NTTIME current_time,
+       const NTTIME not_after,
+       struct GUID *const root_key_id_out,
+       const struct ldb_message **const root_key_out);
 
 #endif /* DSDB_GMSA_GKDI_H */
diff --git a/source4/dsdb/gmsa/util.c b/source4/dsdb/gmsa/util.c
new file mode 100644 (file)
index 0000000..30ea532
--- /dev/null
@@ -0,0 +1,1414 @@
+/*
+   Unix SMB/CIFS implementation.
+   msDS-ManagedPassword attribute for Group Managed Service Accounts
+
+   Copyright (C) Catalyst.Net Ltd 2024
+
+   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 <https://www.gnu.org/licenses/>.
+*/
+
+#include "includes.h"
+#include "ldb.h"
+#include "ldb_module.h"
+#include "ldb_errors.h"
+#include "ldb_private.h"
+#include "lib/crypto/gkdi.h"
+#include "lib/crypto/gmsa.h"
+#include "lib/util/data_blob.h"
+#include "lib/util/fault.h"
+#include "lib/util/time.h"
+#include "libcli/security/access_check.h"
+#include "librpc/gen_ndr/auth.h"
+#include "librpc/gen_ndr/ndr_gkdi.h"
+#include "librpc/gen_ndr/ndr_gmsa.h"
+#include "librpc/gen_ndr/ndr_security.h"
+#include "dsdb/common/util.h"
+#include "dsdb/gmsa/gkdi.h"
+#include "dsdb/gmsa/util.h"
+#include "dsdb/samdb/samdb.h"
+
+#undef strcasecmp
+
+enum RootKeyType {
+       ROOT_KEY_NONE,
+       ROOT_KEY_SPECIFIC,
+       ROOT_KEY_NONSPECIFIC,
+       ROOT_KEY_OBTAINED,
+};
+
+struct RootKey {
+       TALLOC_CTX *mem_ctx;
+       enum RootKeyType type;
+       union {
+               struct KeyEnvelopeId specific;
+               struct {
+                       NTTIME key_start_time;
+               } nonspecific;
+               struct {
+                       struct gmsa_update_pwd_part key;
+                       struct gmsa_null_terminated_password *password;
+               } obtained;
+       } u;
+};
+
+static const struct RootKey empty_root_key = {.type = ROOT_KEY_NONE};
+
+int gmsa_allowed_to_view_managed_password(TALLOC_CTX *mem_ctx,
+                                         struct ldb_context *ldb,
+                                         const struct ldb_message *msg,
+                                         const struct dom_sid *account_sid,
+                                         bool *allowed_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       struct security_descriptor group_msa_membership_sd = {};
+       const struct security_token *user_token = NULL;
+       NTSTATUS status = NT_STATUS_OK;
+       int ret = LDB_SUCCESS;
+
+       if (allowed_out == NULL) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+       *allowed_out = false;
+
+       {
+               const struct auth_session_info *session_info = ldb_get_opaque(
+                       ldb, DSDB_SESSION_INFO);
+               const enum security_user_level level =
+                       security_session_user_level(session_info, NULL);
+
+               if (level == SECURITY_SYSTEM) {
+                       *allowed_out = true;
+                       ret = LDB_SUCCESS;
+                       goto out;
+               }
+
+               if (session_info == NULL) {
+                       ret = dsdb_werror(ldb,
+                                         LDB_ERR_OPERATIONS_ERROR,
+                                         WERR_DS_CANT_RETRIEVE_ATTS,
+                                         "no right to view attribute");
+                       goto out;
+               }
+
+               user_token = session_info->security_token;
+       }
+
+       tmp_ctx = talloc_new(msg);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       {
+               const struct ldb_val *group_msa_membership = NULL;
+               enum ndr_err_code err;
+
+               /* [MS-ADTS] 3.1.1.4.4: Extended Access Checks. */
+               group_msa_membership = ldb_msg_find_ldb_val(
+                       msg, "msDS-GroupMSAMembership");
+               if (group_msa_membership == NULL) {
+                       ret = dsdb_werror(ldb,
+                                         LDB_ERR_OPERATIONS_ERROR,
+                                         WERR_DS_CANT_RETRIEVE_ATTS,
+                                         "no right to view attribute");
+                       goto out;
+               }
+
+               err = ndr_pull_struct_blob_all(
+                       group_msa_membership,
+                       tmp_ctx,
+                       &group_msa_membership_sd,
+                       (ndr_pull_flags_fn_t)ndr_pull_security_descriptor);
+               if (!NDR_ERR_CODE_IS_SUCCESS(err)) {
+                       status = ndr_map_error2ntstatus(err);
+                       DBG_WARNING("msDS-GroupMSAMembership pull failed: %s\n",
+                                   nt_errstr(status));
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+       }
+
+       {
+               const uint32_t access_desired = SEC_ADS_READ_PROP;
+               uint32_t access_granted = 0;
+
+               status = sec_access_check_ds(&group_msa_membership_sd,
+                                            user_token,
+                                            access_desired,
+                                            &access_granted,
+                                            NULL,
+                                            account_sid);
+               if (NT_STATUS_EQUAL(status, NT_STATUS_ACCESS_DENIED)) {
+                       /*
+                        * The principal is not allowed to view the managed
+                        * password.
+                        */
+               } else if (!NT_STATUS_IS_OK(status)) {
+                       DBG_WARNING("msDS-GroupMSAMembership: "
+                                   "sec_access_check_ds(access_desired=%#08x, "
+                                   "access_granted:%#08x) failed with: %s\n",
+                                   access_desired,
+                                   access_granted,
+                                   nt_errstr(status));
+
+                       ret = dsdb_werror(
+                               ldb,
+                               LDB_ERR_OPERATIONS_ERROR,
+                               WERR_DS_CANT_RETRIEVE_ATTS,
+                               "access check to view managed password failed");
+                       goto out;
+               } else {
+                       /* Cool, the principal may view the password. */
+                       *allowed_out = true;
+               }
+       }
+
+out:
+       TALLOC_FREE(tmp_ctx);
+       return ret;
+}
+
+static NTSTATUS gmsa_managed_pwd_id(struct ldb_context *ldb,
+                                   TALLOC_CTX *mem_ctx,
+                                   const struct ldb_val *pwd_id_blob,
+                                   const struct ProvRootKey *root_key,
+                                   struct KeyEnvelope *pwd_id_out)
+{
+       if (root_key == NULL) {
+               return NT_STATUS_INVALID_PARAMETER;
+       }
+
+       if (pwd_id_blob != NULL) {
+               return gkdi_pull_KeyEnvelope(mem_ctx, pwd_id_blob, pwd_id_out);
+       }
+
+       {
+               const char *domain_name = NULL;
+               const char *forest_name = NULL;
+
+               domain_name = samdb_default_domain_name(ldb, mem_ctx);
+               if (domain_name == NULL) {
+                       return NT_STATUS_NO_MEMORY;
+               }
+
+               forest_name = samdb_forest_name(ldb, mem_ctx);
+               if (forest_name == NULL) {
+                       /* We leak ‘domain_name’, but that can’t be helped. */
+                       return NT_STATUS_NO_MEMORY;
+               }
+
+               *pwd_id_out = (struct KeyEnvelope){
+                       .version = root_key->version,
+                       .flags = ENVELOPE_FLAG_KEY_MAY_ENCRYPT_NEW_DATA,
+                       .domain_name = domain_name,
+                       .forest_name = forest_name,
+               };
+       }
+
+       return NT_STATUS_OK;
+}
+
+void gmsa_update_managed_pwd_id(struct KeyEnvelope *pwd_id,
+                               const struct gmsa_update_pwd_part *new_pwd)
+{
+       pwd_id->l0_index = new_pwd->gkid.l0_idx;
+       pwd_id->l1_index = new_pwd->gkid.l1_idx;
+       pwd_id->l2_index = new_pwd->gkid.l2_idx;
+       pwd_id->root_key_id = new_pwd->root_key->id;
+}
+
+NTSTATUS gmsa_pack_managed_pwd_id(TALLOC_CTX *mem_ctx,
+                                 const struct KeyEnvelope *pwd_id,
+                                 DATA_BLOB *pwd_id_out)
+{
+       NTSTATUS status = NT_STATUS_OK;
+       enum ndr_err_code err;
+
+       err = ndr_push_struct_blob(pwd_id_out,
+                                  mem_ctx,
+                                  pwd_id,
+                                  (ndr_push_flags_fn_t)ndr_push_KeyEnvelope);
+       status = ndr_map_error2ntstatus(err);
+       if (!NT_STATUS_IS_OK(status)) {
+               DBG_WARNING("KeyEnvelope push failed: %s\n", nt_errstr(status));
+       }
+
+       return status;
+}
+
+static int gmsa_specific_password(TALLOC_CTX *mem_ctx,
+                                 struct ldb_context *ldb,
+                                 const struct KeyEnvelopeId pwd_id,
+                                 struct gmsa_update_pwd_part *new_pwd_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       NTSTATUS status = NT_STATUS_OK;
+       int ret = LDB_SUCCESS;
+
+       tmp_ctx = talloc_new(mem_ctx);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       {
+               const struct ldb_message *root_key_msg = NULL;
+
+               ret = gkdi_root_key_from_id(tmp_ctx,
+                                           ldb,
+                                           &pwd_id.root_key_id,
+                                           &root_key_msg);
+               if (ret) {
+                       goto out;
+               }
+
+               status = gkdi_root_key_from_msg(mem_ctx,
+                                               pwd_id.root_key_id,
+                                               root_key_msg,
+                                               &new_pwd_out->root_key);
+               if (!NT_STATUS_IS_OK(status)) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+       }
+
+       new_pwd_out->gkid = pwd_id.gkid;
+
+out:
+       talloc_free(tmp_ctx);
+       return ret;
+}
+
+static int gmsa_nonspecific_password(TALLOC_CTX *mem_ctx,
+                                    struct ldb_context *ldb,
+                                    const NTTIME key_start_time,
+                                    const NTTIME current_time,
+                                    struct gmsa_update_pwd_part *new_pwd_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       int ret = LDB_SUCCESS;
+
+       tmp_ctx = talloc_new(mem_ctx);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       {
+               const struct ldb_message *root_key_msg = NULL;
+               struct GUID root_key_id;
+               NTSTATUS status = NT_STATUS_OK;
+
+               ret = gkdi_most_recently_created_root_key(tmp_ctx,
+                                                         ldb,
+                                                         current_time,
+                                                         key_start_time,
+                                                         &root_key_id,
+                                                         &root_key_msg);
+               if (ret) {
+                       goto out;
+               }
+
+               status = gkdi_root_key_from_msg(mem_ctx,
+                                               root_key_id,
+                                               root_key_msg,
+                                               &new_pwd_out->root_key);
+               if (!NT_STATUS_IS_OK(status)) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+       }
+
+       new_pwd_out->gkid = gkdi_get_interval_id(key_start_time);
+
+out:
+       talloc_free(tmp_ctx);
+       return ret;
+}
+
+static int gmsa_specifc_root_key(TALLOC_CTX *mem_ctx,
+                                const struct KeyEnvelopeId pwd_id,
+                                struct RootKey *root_key_out)
+{
+       if (root_key_out == NULL) {
+               return LDB_ERR_OPERATIONS_ERROR;
+       }
+
+       *root_key_out = (struct RootKey){.mem_ctx = mem_ctx,
+                                        .type = ROOT_KEY_SPECIFIC,
+                                        .u.specific = pwd_id};
+       return LDB_SUCCESS;
+}
+
+static int gmsa_nonspecifc_root_key(TALLOC_CTX *mem_ctx,
+                                   const NTTIME key_start_time,
+                                   struct RootKey *root_key_out)
+{
+       if (root_key_out == NULL) {
+               return LDB_ERR_OPERATIONS_ERROR;
+       }
+
+       *root_key_out = (struct RootKey){
+               .mem_ctx = mem_ctx,
+               .type = ROOT_KEY_NONSPECIFIC,
+               .u.nonspecific.key_start_time = key_start_time};
+       return LDB_SUCCESS;
+}
+
+static int gmsa_obtained_root_key_steal(
+       TALLOC_CTX *mem_ctx,
+       const struct gmsa_update_pwd_part key,
+       struct gmsa_null_terminated_password *password,
+       struct RootKey *root_key_out)
+{
+       if (root_key_out == NULL) {
+               return LDB_ERR_OPERATIONS_ERROR;
+       }
+
+       /* Steal the data on to the appropriate memory context. */
+       talloc_steal(mem_ctx, key.root_key);
+       talloc_steal(mem_ctx, password);
+
+       *root_key_out = (struct RootKey){.mem_ctx = mem_ctx,
+                                        .type = ROOT_KEY_OBTAINED,
+                                        .u.obtained = {.key = key,
+                                                       .password = password}};
+       return LDB_SUCCESS;
+}
+
+static int gmsa_fetch_root_key(struct ldb_context *ldb,
+                              const NTTIME current_time,
+                              struct RootKey *root_key,
+                              const struct dom_sid *const account_sid)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       NTSTATUS status = NT_STATUS_OK;
+       int ret = LDB_SUCCESS;
+
+       if (root_key == NULL) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+       switch (root_key->type) {
+       case ROOT_KEY_SPECIFIC:
+       case ROOT_KEY_NONSPECIFIC: {
+               struct gmsa_null_terminated_password *password = NULL;
+               struct gmsa_update_pwd_part key;
+
+               tmp_ctx = talloc_new(NULL);
+               if (tmp_ctx == NULL) {
+                       ret = ldb_oom(ldb);
+                       goto out;
+               }
+
+               if (root_key->type == ROOT_KEY_SPECIFIC) {
+                       /* Search for a specific root key. */
+                       ret = gmsa_specific_password(tmp_ctx,
+                                                    ldb,
+                                                    root_key->u.specific,
+                                                    &key);
+                       if (ret) {
+                               /*
+                                * We couldn’t find a specific key — treat this
+                                * as an error.
+                                */
+                               goto out;
+                       }
+               } else {
+                       /*
+                        * Search for the most recent root key meeting the start
+                        * time requirement.
+                        */
+                       ret = gmsa_nonspecific_password(
+                               tmp_ctx,
+                               ldb,
+                               root_key->u.nonspecific.key_start_time,
+                               current_time,
+                               &key);
+                       /* Handle errors below. */
+               }
+               if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+                       /*
+                        * We couldn’t find a key meeting the requirements —
+                        * that’s OK, presumably. It’s not critical if we can’t
+                        * find a key for deriving a previous gMSA password, for
+                        * example.
+                        */
+                       ret = LDB_SUCCESS;
+                       *root_key = empty_root_key;
+               } else if (ret) {
+                       goto out;
+               } else {
+                       /* Derive the password. */
+                       status = gmsa_talloc_password_based_on_key_id(
+                               tmp_ctx,
+                               key.gkid,
+                               current_time,
+                               key.root_key,
+                               account_sid,
+                               &password);
+                       if (!NT_STATUS_IS_OK(status)) {
+                               ret = ldb_operr(ldb);
+                               goto out;
+                       }
+
+                       /*
+                        * Initialize the obtained structure, and give it the
+                        * appropriate memory context.
+                        */
+                       ret = gmsa_obtained_root_key_steal(root_key->mem_ctx,
+                                                          key,
+                                                          password,
+                                                          root_key);
+                       if (ret) {
+                               goto out;
+                       }
+               }
+       } break;
+       case ROOT_KEY_NONE:
+               /* No key is available. */
+               break;
+       case ROOT_KEY_OBTAINED:
+               /* The key has already been obtained. */
+               break;
+       default:
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+out:
+       TALLOC_FREE(tmp_ctx);
+       return ret;
+}
+
+/*
+ * Get the password and update information associated with a root key. The
+ * caller *does not* own these structures; the root key object retains
+ * ownership.
+ */
+static int gmsa_get_root_key(
+       struct ldb_context *ldb,
+       const NTTIME current_time,
+       const struct dom_sid *const account_sid,
+       struct RootKey *root_key,
+       struct gmsa_null_terminated_password **password_out,
+       struct gmsa_update_pwd_part *update_out)
+{
+       int ret = LDB_SUCCESS;
+
+       if (password_out == NULL) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+       *password_out = NULL;
+
+       if (update_out != NULL) {
+               *update_out = (struct gmsa_update_pwd_part){};
+       }
+
+       /* Fetch the root key from the database and obtain the password. */
+       ret = gmsa_fetch_root_key(ldb, current_time, root_key, account_sid);
+       if (ret) {
+               goto out;
+       }
+
+       switch (root_key->type) {
+       case ROOT_KEY_NONE:
+               /* No key is available. */
+               break;
+       case ROOT_KEY_OBTAINED:
+               *password_out = root_key->u.obtained.password;
+               if (update_out != NULL) {
+                       *update_out = root_key->u.obtained.key;
+               }
+               break;
+       default:
+               /* Unexpected. */
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+out:
+       return ret;
+}
+
+static int gmsa_system_update_password_id_req(
+       struct ldb_context *ldb,
+       TALLOC_CTX *mem_ctx,
+       const struct ldb_message *msg,
+       const struct gmsa_update_pwd *new_pwd,
+       struct ldb_request **req_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       const struct ldb_val *pwd_id_blob = ldb_msg_find_ldb_val(
+               msg, "msDS-ManagedPasswordId");
+       struct KeyEnvelope pwd_id;
+       struct ldb_message *mod_msg = NULL;
+       NTSTATUS status = NT_STATUS_OK;
+       int ret = LDB_SUCCESS;
+
+       tmp_ctx = talloc_new(mem_ctx);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       /* Create a new ldb message. */
+       mod_msg = ldb_msg_new(tmp_ctx);
+       if (mod_msg == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+       {
+               struct ldb_dn *dn = ldb_dn_copy(mod_msg, msg->dn);
+               if (dn == NULL) {
+                       ret = ldb_oom(ldb);
+                       goto out;
+               }
+               mod_msg->dn = dn;
+       }
+
+       /* Get the Managed Password ID. */
+       status = gmsa_managed_pwd_id(
+               ldb, tmp_ctx, pwd_id_blob, new_pwd->new_id.root_key, &pwd_id);
+       if (!NT_STATUS_IS_OK(status)) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+       /* Update the password ID to contain the new GKID and root key ID. */
+       gmsa_update_managed_pwd_id(&pwd_id, &new_pwd->new_id);
+
+       {
+               DATA_BLOB new_pwd_id_blob = {};
+
+               /* Pack the current password ID. */
+               status = gmsa_pack_managed_pwd_id(tmp_ctx,
+                                                 &pwd_id,
+                                                 &new_pwd_id_blob);
+               if (!NT_STATUS_IS_OK(status)) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+
+               /* Update the msDS-ManagedPasswordId attribute. */
+               ret = ldb_msg_append_steal_value(mod_msg,
+                                                "msDS-ManagedPasswordId",
+                                                &new_pwd_id_blob,
+                                                LDB_FLAG_MOD_REPLACE);
+               if (ret) {
+                       goto out;
+               }
+       }
+
+       {
+               DATA_BLOB *prev_pwd_id_blob = NULL;
+               DATA_BLOB _prev_pwd_id_blob;
+               DATA_BLOB prev_pwd_id = {};
+
+               if (new_pwd->prev_id.root_key != NULL) {
+                       /*
+                        * Update the password ID to contain the previous GKID
+                        * and root key ID.
+                        */
+                       gmsa_update_managed_pwd_id(&pwd_id, &new_pwd->prev_id);
+
+                       /* Pack the previous password ID. */
+                       status = gmsa_pack_managed_pwd_id(tmp_ctx,
+                                                         &pwd_id,
+                                                         &prev_pwd_id);
+                       if (!NT_STATUS_IS_OK(status)) {
+                               ret = ldb_operr(ldb);
+                               goto out;
+                       }
+
+                       prev_pwd_id_blob = &prev_pwd_id;
+               } else if (pwd_id_blob != NULL) {
+                       /* Copy the current password ID to the previous ID. */
+                       _prev_pwd_id_blob = ldb_val_dup(tmp_ctx, pwd_id_blob);
+                       if (_prev_pwd_id_blob.length != pwd_id_blob->length) {
+                               ret = ldb_oom(ldb);
+                               goto out;
+                       }
+
+                       prev_pwd_id_blob = &_prev_pwd_id_blob;
+               }
+
+               if (prev_pwd_id_blob != NULL) {
+                       /*
+                        * Update the msDS-ManagedPasswordPreviousId attribute.
+                        */
+                       ret = ldb_msg_append_steal_value(
+                               mod_msg,
+                               "msDS-ManagedPasswordPreviousId",
+                               prev_pwd_id_blob,
+                               LDB_FLAG_MOD_REPLACE);
+                       if (ret) {
+                               goto out;
+                       }
+               }
+       }
+
+       {
+               struct ldb_request *req = NULL;
+
+               /* Build the ldb request to return. */
+               ret = ldb_build_mod_req(&req,
+                                       ldb,
+                                       tmp_ctx,
+                                       mod_msg,
+                                       NULL,
+                                       NULL,
+                                       ldb_op_default_callback,
+                                       NULL);
+               if (ret) {
+                       goto out;
+               }
+
+               /* Tie the lifetime of the message to that of the request. */
+               talloc_steal(req, mod_msg);
+
+               /* Make sure the password ID update happens as System. */
+               ret = dsdb_request_add_controls(req, DSDB_FLAG_AS_SYSTEM);
+               if (ret) {
+                       goto out;
+               }
+
+               *req_out = talloc_steal(mem_ctx, req);
+       }
+
+out:
+       talloc_free(tmp_ctx);
+       return ret;
+}
+
+int gmsa_generate_blobs(struct ldb_context *ldb,
+                       TALLOC_CTX *mem_ctx,
+                       const NTTIME current_time,
+                       const struct dom_sid *const account_sid,
+                       DATA_BLOB *pwd_id_blob_out,
+                       struct gmsa_null_terminated_password **password_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       struct KeyEnvelope pwd_id;
+       const struct ProvRootKey *root_key = NULL;
+       NTSTATUS status = NT_STATUS_OK;
+       int ret = LDB_SUCCESS;
+
+       tmp_ctx = talloc_new(mem_ctx);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       {
+               const struct ldb_message *root_key_msg = NULL;
+               struct GUID root_key_id;
+               const NTTIME one_interval = gkdi_key_cycle_duration +
+                                           gkdi_max_clock_skew;
+               const NTTIME one_interval_ago = current_time -
+                                               MIN(one_interval, current_time);
+
+               ret = gkdi_most_recently_created_root_key(tmp_ctx,
+                                                         ldb,
+                                                         current_time,
+                                                         one_interval_ago,
+                                                         &root_key_id,
+                                                         &root_key_msg);
+               if (ret) {
+                       goto out;
+               }
+
+               status = gkdi_root_key_from_msg(tmp_ctx,
+                                               root_key_id,
+                                               root_key_msg,
+                                               &root_key);
+               if (!NT_STATUS_IS_OK(status)) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+       }
+
+       /* Get the Managed Password ID. */
+       status = gmsa_managed_pwd_id(ldb, tmp_ctx, NULL, root_key, &pwd_id);
+       if (!NT_STATUS_IS_OK(status)) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+       {
+               const struct Gkid current_gkid = gkdi_get_interval_id(
+                       current_time);
+
+               /* Derive the password. */
+               status = gmsa_talloc_password_based_on_key_id(tmp_ctx,
+                                                             current_gkid,
+                                                             current_time,
+                                                             root_key,
+                                                             account_sid,
+                                                             password_out);
+               if (!NT_STATUS_IS_OK(status)) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+
+               {
+                       const struct gmsa_update_pwd_part new_id = {
+                               .root_key = root_key,
+                               .gkid = current_gkid,
+                       };
+
+                       /*
+                        * Update the password ID to contain the new GKID and
+                        * root key ID.
+                        */
+                       gmsa_update_managed_pwd_id(&pwd_id, &new_id);
+               }
+       }
+
+       /* Pack the current password ID. */
+       status = gmsa_pack_managed_pwd_id(mem_ctx, &pwd_id, pwd_id_blob_out);
+       if (!NT_STATUS_IS_OK(status)) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+       /* Transfer ownership of the password to the caller’s memory context. */
+       talloc_steal(mem_ctx, *password_out);
+
+out:
+       talloc_free(tmp_ctx);
+       return ret;
+}
+
+static int gmsa_create_update(TALLOC_CTX *mem_ctx,
+                             struct ldb_context *ldb,
+                             const struct ldb_message *msg,
+                             const NTTIME current_time,
+                             const struct dom_sid *account_sid,
+                             const bool current_key_becomes_previous,
+                             struct RootKey *current_key,
+                             struct RootKey *previous_key,
+                             struct gmsa_update **update_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       struct ldb_request *old_pw_req = NULL;
+       struct ldb_request *new_pw_req = NULL;
+       struct ldb_request *pwd_id_req = NULL;
+       struct gmsa_update_pwd new_pwd = {};
+       struct gmsa_update *update = NULL;
+       NTSTATUS status = NT_STATUS_OK;
+       int ret = LDB_SUCCESS;
+
+       if (update_out == NULL) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+       *update_out = NULL;
+
+       if (current_key == NULL) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+       tmp_ctx = talloc_new(mem_ctx);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       {
+               /*
+                * The password_hash module expects these passwords to be
+                * null‐terminated.
+                */
+               struct gmsa_null_terminated_password *new_password = NULL;
+
+               ret = gmsa_get_root_key(ldb,
+                                       current_time,
+                                       account_sid,
+                                       current_key,
+                                       &new_password,
+                                       &new_pwd.new_id);
+               if (ret) {
+                       goto out;
+               }
+
+               if (new_password == NULL) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+
+               status = gmsa_system_password_update_request(
+                       ldb, tmp_ctx, msg->dn, new_password->buf, &new_pw_req);
+               if (!NT_STATUS_IS_OK(status)) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+       }
+
+       /* Does the previous password need to be updated? */
+       if (current_key_becomes_previous) {
+               /*
+                * When we perform the password set, the now‐current password
+                * will become the previous password automatically. We don’t
+                * have to manage that ourselves.
+                */
+       } else {
+               struct gmsa_null_terminated_password *old_password = NULL;
+
+               /* The current key cannot be reused as the previous key. */
+               ret = gmsa_get_root_key(ldb,
+                                       current_time,
+                                       account_sid,
+                                       previous_key,
+                                       &old_password,
+                                       &new_pwd.prev_id);
+               if (ret) {
+                       goto out;
+               }
+
+               if (old_password != NULL) {
+                       status = gmsa_system_password_update_request(
+                               ldb,
+                               tmp_ctx,
+                               msg->dn,
+                               old_password->buf,
+                               &old_pw_req);
+                       if (!NT_STATUS_IS_OK(status)) {
+                               ret = ldb_operr(ldb);
+                               goto out;
+                       }
+               }
+       }
+
+       /* Ready the update of the msDS-ManagedPasswordId attribute. */
+       ret = gmsa_system_update_password_id_req(
+               ldb, tmp_ctx, msg, &new_pwd, &pwd_id_req);
+       if (ret) {
+               goto out;
+       }
+
+       update = talloc(tmp_ctx, struct gmsa_update);
+       if (update == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       *update = (struct gmsa_update){
+               .old_pw_req = talloc_steal(update, old_pw_req),
+               .new_pw_req = talloc_steal(update, new_pw_req),
+               .pwd_id_req = talloc_steal(update, pwd_id_req)};
+
+       *update_out = talloc_steal(mem_ctx, update);
+
+out:
+       TALLOC_FREE(tmp_ctx);
+       return ret;
+}
+
+NTSTATUS gmsa_pack_managed_pwd(TALLOC_CTX *mem_ctx,
+                              const uint8_t *new_password,
+                              const uint8_t *old_password,
+                              uint64_t query_interval,
+                              uint64_t unchanged_interval,
+                              DATA_BLOB *managed_pwd_out)
+{
+       const struct MANAGEDPASSWORD_BLOB managed_pwd = {
+               .passwords = {.current = new_password,
+                             .previous = old_password,
+                             .query_interval = &query_interval,
+                             .unchanged_interval = &unchanged_interval}};
+       NTSTATUS status = NT_STATUS_OK;
+       enum ndr_err_code err;
+
+       err = ndr_push_struct_blob(managed_pwd_out,
+                                  mem_ctx,
+                                  &managed_pwd,
+                                  (ndr_push_flags_fn_t)
+                                          ndr_push_MANAGEDPASSWORD_BLOB);
+       status = ndr_map_error2ntstatus(err);
+       if (!NT_STATUS_IS_OK(status)) {
+               DBG_WARNING("MANAGEDPASSWORD_BLOB push failed: %s\n",
+                           nt_errstr(status));
+       }
+
+       return status;
+}
+
+bool dsdb_account_is_gmsa(struct ldb_context *ldb,
+                         const struct ldb_message *msg)
+{
+       /*
+        * Check if the account has objectClass
+        * ‘msDS-GroupManagedServiceAccount’.
+        */
+       return samdb_find_attribute(ldb,
+                                   msg,
+                                   "objectclass",
+                                   "msDS-GroupManagedServiceAccount") != NULL;
+}
+
+static struct new_key {
+       NTTIME start_time;
+       bool immediately_follows_previous;
+} calculate_new_key(const NTTIME current_time,
+                   const NTTIME current_key_expiration_time,
+                   const NTTIME rollover_interval)
+{
+       NTTIME new_key_start_time = current_key_expiration_time;
+       bool immediately_follows_previous = false;
+
+       if (new_key_start_time < current_time && rollover_interval) {
+               /*
+                * Advance the key start time by the rollover interval until it
+                * would be greater than the current time.
+                */
+               const NTTIME time_to_advance_by = current_time + 1 -
+                                                 new_key_start_time;
+               const uint64_t stale_count = time_to_advance_by /
+                                            rollover_interval;
+               new_key_start_time += stale_count * rollover_interval;
+
+               SMB_ASSERT(new_key_start_time <= current_time);
+
+               immediately_follows_previous = stale_count == 0;
+       } else {
+               /*
+                * It is possible that new_key_start_time ≥ current_time;
+                * specifically, if there is no password ID, and the creation
+                * time of the gMSA is in the future (perhaps due to replication
+                * weirdness).
+                */
+       }
+
+       return (struct new_key){
+               .start_time = new_key_start_time,
+               .immediately_follows_previous = immediately_follows_previous};
+}
+
+static bool gmsa_creation_time(const struct ldb_message *msg,
+                              const NTTIME current_time,
+                              NTTIME *creation_time_out)
+{
+       const struct ldb_val *when_created = NULL;
+       time_t creation_unix_time;
+       int ret;
+
+       when_created = ldb_msg_find_ldb_val(msg, "whenCreated");
+       ret = ldb_val_to_time(when_created, &creation_unix_time);
+       if (ret) {
+               /* Fail if we can’t read the attribute or it isn’t present. */
+               return false;
+       }
+
+       unix_to_nt_time(creation_time_out, creation_unix_time);
+       return true;
+}
+
+static const struct KeyEnvelopeId *gmsa_get_managed_pwd_id_attr_name(
+       const struct ldb_message *msg,
+       const char *attr_name,
+       struct KeyEnvelopeId *key_env_out)
+{
+       const struct ldb_val *pwd_id_blob = ldb_msg_find_ldb_val(msg,
+                                                                attr_name);
+       if (pwd_id_blob == NULL) {
+               return NULL;
+       }
+
+       return gkdi_pull_KeyEnvelopeId(*pwd_id_blob, key_env_out);
+}
+
+const struct KeyEnvelopeId *gmsa_get_managed_pwd_id(
+       const struct ldb_message *msg,
+       struct KeyEnvelopeId *key_env_out)
+{
+       return gmsa_get_managed_pwd_id_attr_name(msg,
+                                                "msDS-ManagedPasswordId",
+                                                key_env_out);
+}
+
+static const struct KeyEnvelopeId *gmsa_get_managed_pwd_prev_id(
+       const struct ldb_message *msg,
+       struct KeyEnvelopeId *key_env_out)
+{
+       return gmsa_get_managed_pwd_id_attr_name(
+               msg, "msDS-ManagedPasswordPreviousId", key_env_out);
+}
+
+static bool samdb_result_gkdi_rollover_interval(const struct ldb_message *msg,
+                                               NTTIME *rollover_interval_out)
+{
+       int64_t managed_password_interval;
+
+       managed_password_interval = ldb_msg_find_attr_as_int64(
+               msg, "msDS-ManagedPasswordInterval", 30);
+       return gkdi_rollover_interval(managed_password_interval,
+                                     rollover_interval_out);
+}
+
+int gmsa_recalculate_managed_pwd(TALLOC_CTX *mem_ctx,
+                                struct ldb_context *ldb,
+                                const struct ldb_message *msg,
+                                const NTTIME current_time,
+                                struct gmsa_update **update_out,
+                                struct gmsa_return_pwd *return_out)
+{
+       TALLOC_CTX *tmp_ctx = NULL;
+       int ret = LDB_SUCCESS;
+       NTTIME rollover_interval;
+       NTTIME current_key_expiration_time;
+       NTTIME key_expiration_time;
+       struct dom_sid account_sid;
+       struct KeyEnvelopeId pwd_id_buf;
+       const struct KeyEnvelopeId *pwd_id = NULL;
+       struct RootKey previous_key = empty_root_key;
+       struct RootKey current_key = empty_root_key;
+       struct gmsa_update *update = NULL;
+       struct gmsa_null_terminated_password *previous_password = NULL;
+       struct gmsa_null_terminated_password *current_password = NULL;
+       NTTIME query_interval = 0;
+       NTTIME unchanged_interval = 0;
+       NTTIME creation_time = 0;
+       NTTIME account_age = 0;
+       NTTIME key_start_time = 0;
+       bool have_key_start_time = false;
+       bool ok = true;
+       bool current_key_is_valid = false;
+
+       if (update_out == NULL) {
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+       *update_out = NULL;
+
+       {
+               /* Is the account a Group Managed Service Account? */
+               const bool is_gmsa = dsdb_account_is_gmsa(ldb, msg);
+               if (!is_gmsa) {
+                       /* It’s not a GMSA — we’re done here. */
+                       *update_out = NULL;
+                       if (return_out != NULL) {
+                               *return_out = (struct gmsa_return_pwd){};
+                       }
+                       ret = LDB_SUCCESS;
+                       goto out;
+               }
+       }
+
+       /* Calculate the rollover interval. */
+       ok = samdb_result_gkdi_rollover_interval(msg, &rollover_interval);
+       if (!ok || rollover_interval == 0) {
+               /* We can’t do anything if the rollover interval is zero. */
+               ret = ldb_operr(ldb);
+               goto out;
+       }
+
+       ok = gmsa_creation_time(msg, current_time, &creation_time);
+       if (!ok) {
+               return ldb_error(ldb,
+                                LDB_ERR_OPERATIONS_ERROR,
+                                "unable to determine creation time of Group "
+                                "Managed Service Account");
+       }
+       account_age = current_time - MIN(creation_time, current_time);
+
+       /* Calculate the expiration time of the current key. */
+       pwd_id = gmsa_get_managed_pwd_id(msg, &pwd_id_buf);
+       if (pwd_id != NULL &&
+           gkdi_get_key_start_time(pwd_id->gkid, &key_start_time))
+       {
+               have_key_start_time = true;
+
+               /* Check for overflow. */
+               if (key_start_time > UINT64_MAX - rollover_interval) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+               current_key_expiration_time = key_start_time +
+                                             rollover_interval;
+       } else {
+               /*
+                * [MS-ADTS] does not say to use gkdi_get_interval_start_time(),
+                * but surely it makes no sense to have keys starting or ending
+                * at random times.
+                */
+               current_key_expiration_time = gkdi_get_interval_start_time(
+                       creation_time);
+       }
+
+       /* Fetch the account’s SID, necessary for deriving passwords. */
+       ret = samdb_result_dom_sid_buf(msg, "objectSid", &account_sid);
+       if (ret) {
+               goto out;
+       }
+
+       tmp_ctx = talloc_new(mem_ctx);
+       if (tmp_ctx == NULL) {
+               ret = ldb_oom(ldb);
+               goto out;
+       }
+
+       /*
+        * In determining whether the account’s passwords should be updated, we
+        * do not validate that the unicodePwd attribute is up‐to‐date, or even
+        * that it exists. We rely entirely on the fact that the managed
+        * password ID should be updated *only* as part of a successful gMSA
+        * password update. In any case, unicodePwd is optional in Samba — save
+        * for machine accounts (which gMSAs are :)) — and we can’t always rely
+        * on its presence.
+        *
+        * All this means that an admin (or a DC that doesn’t support gMSAs)
+        * could reset a gMSA’s password outside of the normal procedure, and
+        * the account would then have the wrong password until the key was due
+        * to roll over again. There’s nothing much we can do about this if we
+        * don’t want to re‐derive and verify the password every time we look up
+        * the keys.
+        */
+
+       current_key_is_valid = pwd_id != NULL &&
+                              current_time < current_key_expiration_time;
+       if (current_key_is_valid) {
+               key_expiration_time = current_key_expiration_time;
+
+               if (return_out != NULL) {
+                       struct KeyEnvelopeId prev_pwd_id_buf;
+                       const struct KeyEnvelopeId *prev_pwd_id = NULL;
+
+                       ret = gmsa_specifc_root_key(tmp_ctx,
+                                                   *pwd_id,
+                                                   &current_key);
+                       if (ret) {
+                               goto out;
+                       }
+
+                       if (account_age >= rollover_interval) {
+                               prev_pwd_id = gmsa_get_managed_pwd_prev_id(
+                                       msg, &prev_pwd_id_buf);
+                               if (prev_pwd_id != NULL) {
+                                       ret = gmsa_specifc_root_key(
+                                               tmp_ctx,
+                                               *prev_pwd_id,
+                                               &previous_key);
+                                       if (ret) {
+                                               goto out;
+                                       }
+                               } else if (have_key_start_time &&
+                                          key_start_time >= rollover_interval)
+                               {
+                                       /*
+                                        * The account’s old enough to have a
+                                        * previous password, but it doesn’t
+                                        * have a previous password ID for some
+                                        * reason. This can happen in our tests
+                                        * (python/samba/krb5/gmsa_tests.py)
+                                        * when we’re mucking about with times.
+                                        * Just produce what would have been the
+                                        * previous key.
+                                        */
+                                       ret = gmsa_nonspecifc_root_key(
+                                               tmp_ctx,
+                                               key_start_time -
+                                                       rollover_interval,
+                                               &previous_key);
+                                       if (ret) {
+                                               goto out;
+                                       }
+                               }
+                       } else {
+                               /*
+                                * The account is not old enough to have a
+                                * previous password. The old password will not
+                                * be returned.
+                                */
+                       }
+               }
+       } else {
+               /* Calculate the start time of the new key. */
+               const struct new_key new_key = calculate_new_key(
+                       current_time,
+                       current_key_expiration_time,
+                       rollover_interval);
+               const bool current_key_becomes_previous =
+                       pwd_id != NULL && new_key.immediately_follows_previous;
+
+               /* Check for overflow. */
+               if (new_key.start_time > UINT64_MAX - rollover_interval) {
+                       ret = ldb_operr(ldb);
+                       goto out;
+               }
+               key_expiration_time = new_key.start_time + rollover_interval;
+
+               ret = gmsa_nonspecifc_root_key(tmp_ctx,
+                                              new_key.start_time,
+                                              &current_key);
+               if (ret) {
+                       goto out;
+               }
+
+               if (account_age >= rollover_interval) {
+                       /* Check for underflow. */
+                       if (new_key.start_time < rollover_interval) {
+                               ret = ldb_operr(ldb);
+                               goto out;
+                       }
+                       ret = gmsa_nonspecifc_root_key(
+                               tmp_ctx,
+                               new_key.start_time - rollover_interval,
+                               &previous_key);
+                       if (ret) {
+                               goto out;
+                       }
+               } else {
+                       /*
+                        * The account is not old enough to have a previous
+                        * password. The old password will not be returned.
+                        */
+               }
+
+               /*
+                * The current GMSA key, according to the Managed Password ID,
+                * is no longer valid. We should update the account’s Managed
+                * Password ID and keys in anticipation of their being needed in
+                * the near future.
+                */
+
+               ret = gmsa_create_update(tmp_ctx,
+                                        ldb,
+                                        msg,
+                                        current_time,
+                                        &account_sid,
+                                        current_key_becomes_previous,
+                                        &current_key,
+                                        &previous_key,
+                                        &update);
+               if (ret) {
+                       goto out;
+               }
+       }
+
+       if (return_out != NULL) {
+               bool return_future_key;
+
+               unchanged_interval = query_interval = key_expiration_time -
+                                                     MIN(current_time,
+                                                         key_expiration_time);
+
+               /* Derive the current and previous passwords. */
+               return_future_key = query_interval <= gkdi_max_clock_skew;
+               if (return_future_key) {
+                       struct RootKey future_key = empty_root_key;
+
+                       /*
+                        * The current key hasn’t expired yet, but it
+                        * soon will. Return a new key that will be valid in the
+                        * next epoch.
+                        */
+
+                       ret = gmsa_nonspecifc_root_key(tmp_ctx,
+                                                      key_expiration_time,
+                                                      &future_key);
+                       if (ret) {
+                               goto out;
+                       }
+
+                       ret = gmsa_get_root_key(ldb,
+                                               current_time,
+                                               &account_sid,
+                                               &future_key,
+                                               &current_password,
+                                               NULL);
+                       if (ret) {
+                               goto out;
+                       }
+
+                       ret = gmsa_get_root_key(ldb,
+                                               current_time,
+                                               &account_sid,
+                                               &current_key,
+                                               &previous_password,
+                                               NULL);
+                       if (ret) {
+                               goto out;
+                       }
+
+                       /* Check for overflow. */
+                       if (unchanged_interval > UINT64_MAX - rollover_interval)
+                       {
+                               ret = ldb_operr(ldb);
+                               goto out;
+                       }
+                       unchanged_interval += rollover_interval;
+               } else {
+                       /*
+                        * Note that a gMSA will become unusable (at least until
+                        * the next rollover) if its associated root key is ever
+                        * deleted.
+                        */
+
+                       ret = gmsa_get_root_key(ldb,
+                                               current_time,
+                                               &account_sid,
+                                               &current_key,
+                                               &current_password,
+                                               NULL);
+                       if (ret) {
+                               goto out;
+                       }
+
+                       ret = gmsa_get_root_key(ldb,
+                                               current_time,
+                                               &account_sid,
+                                               &previous_key,
+                                               &previous_password,
+                                               NULL);
+                       if (ret) {
+                               goto out;
+                       }
+               }
+
+               unchanged_interval -= MIN(gkdi_max_clock_skew,
+                                         unchanged_interval);
+       }
+
+       *update_out = talloc_steal(mem_ctx, update);
+       if (return_out != NULL) {
+               *return_out = (struct gmsa_return_pwd){
+                       .prev_pwd = talloc_steal(mem_ctx, previous_password),
+                       .new_pwd = talloc_steal(mem_ctx, current_password),
+                       .query_interval = query_interval,
+                       .unchanged_interval = unchanged_interval,
+               };
+       }
+
+out:
+       TALLOC_FREE(tmp_ctx);
+       return ret;
+}
+
+bool dsdb_gmsa_current_time(struct ldb_context *ldb, NTTIME *current_time_out)
+{
+       const unsigned long long *gmsa_time = talloc_get_type(
+               ldb_get_opaque(ldb, DSDB_GMSA_TIME_OPAQUE), unsigned long long);
+
+       if (gmsa_time != NULL) {
+               *current_time_out = *gmsa_time;
+               return true;
+       }
+
+       return gmsa_current_time(current_time_out);
+}
diff --git a/source4/dsdb/gmsa/util.h b/source4/dsdb/gmsa/util.h
new file mode 100644 (file)
index 0000000..7d5430e
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+   Unix SMB/CIFS implementation.
+   msDS-ManagedPassword attribute for Group Managed Service Accounts
+
+   Copyright (C) Catalyst.Net Ltd 2024
+
+   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 <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef DSDB_GMSA_UTIL_H
+#define DSDB_GMSA_UTIL_H
+
+#include "ldb.h"
+#include "ldb_module.h"
+#include <talloc.h>
+
+#include "lib/crypto/gkdi.h"
+#include "lib/crypto/gmsa.h"
+#include "lib/util/data_blob.h"
+#include "lib/util/time.h"
+
+struct gmsa_update {
+       /* An optional request to set the previous password. */
+       struct ldb_request *old_pw_req;
+       /* A request to set the current password. */
+       struct ldb_request *new_pw_req;
+       /* An request to set the managed password ID. */
+       struct ldb_request *pwd_id_req;
+};
+
+struct gmsa_update_pwd_part {
+       const struct ProvRootKey *root_key;
+       struct Gkid gkid;
+};
+
+struct gmsa_update_pwd {
+       struct gmsa_update_pwd_part prev_id;
+       struct gmsa_update_pwd_part new_id;
+};
+
+struct dom_sid;
+int gmsa_allowed_to_view_managed_password(TALLOC_CTX *mem_ctx,
+                                         struct ldb_context *ldb,
+                                         const struct ldb_message *msg,
+                                         const struct dom_sid *account_sid,
+                                         bool *allowed_out);
+
+struct KeyEnvelope;
+void gmsa_update_managed_pwd_id(struct KeyEnvelope *pwd_id,
+                               const struct gmsa_update_pwd_part *new_pwd);
+
+NTSTATUS gmsa_pack_managed_pwd_id(TALLOC_CTX *mem_ctx,
+                                 const struct KeyEnvelope *pwd_id,
+                                 DATA_BLOB *pwd_id_out);
+
+int gmsa_generate_blobs(struct ldb_context *ldb,
+                       TALLOC_CTX *mem_ctx,
+                       const NTTIME current_time,
+                       const struct dom_sid *const account_sid,
+                       DATA_BLOB *pwd_id_blob_out,
+                       struct gmsa_null_terminated_password **password_out);
+
+NTSTATUS gmsa_pack_managed_pwd(TALLOC_CTX *mem_ctx,
+                              const uint8_t *new_password,
+                              const uint8_t *old_password,
+                              uint64_t query_interval,
+                              uint64_t unchanged_interval,
+                              DATA_BLOB *managed_pwd_out);
+
+bool dsdb_account_is_gmsa(struct ldb_context *ldb,
+                         const struct ldb_message *msg);
+
+const struct KeyEnvelopeId *gmsa_get_managed_pwd_id(
+       const struct ldb_message *msg,
+       struct KeyEnvelopeId *key_env_out);
+
+struct gmsa_return_pwd {
+       struct gmsa_null_terminated_password *prev_pwd;
+       struct gmsa_null_terminated_password *new_pwd;
+       NTTIME query_interval;
+       NTTIME unchanged_interval;
+};
+
+int gmsa_recalculate_managed_pwd(TALLOC_CTX *mem_ctx,
+                                struct ldb_context *ldb,
+                                const struct ldb_message *msg,
+                                const NTTIME current_time,
+                                struct gmsa_update **update_out,
+                                struct gmsa_return_pwd *return_out);
+
+#define DSDB_GMSA_TIME_OPAQUE ("dsdb_gmsa_time_opaque")
+
+bool dsdb_gmsa_current_time(struct ldb_context *ldb, NTTIME *current_time_out);
+
+#endif /* DSDB_GMSA_UTIL_H */
index b263c72cc306b4ec86b8f9a34fc536491a30aa4b..196cf66dfabd25dab56ebd4da68176e6246ccd97 100644 (file)
@@ -13,7 +13,7 @@ bld.SAMBA_LIBRARY('samdb',
        )
 
 bld.SAMBA_LIBRARY('samdb-common',
-       source='common/util.c common/util_trusts.c common/util_groups.c common/util_samr.c common/dsdb_dn.c common/dsdb_access.c common/util_links.c common/rodc_helper.c gmsa/gkdi.c',
+       source='common/util.c common/util_trusts.c common/util_groups.c common/util_samr.c common/dsdb_dn.c common/dsdb_access.c common/util_links.c common/rodc_helper.c gmsa/gkdi.c gmsa/util.c',
        autoproto='common/proto.h',
        private_library=True,
        deps='ldb NDR_DRSBLOBS util_ldb LIBCLI_AUTH samba-hostconfig samba_socket cli-ldap-common flag_mapping UTIL_RUNCMD SAMBA_VERSION samba-security gkdi gmsa'