1
0
mirror of https://github.com/samba-team/samba.git synced 2024-12-22 13:34:15 +03:00
samba-mirror/source4/dsdb/gmsa/util.c
Volker Lendecke 56ec6fe5ed lib: Add a few explicit includes
Required to trim down includes from gen_ndr/*security*

Signed-off-by: Volker Lendecke <vl@samba.org>
Reviewed-by: Jennifer Sutton <jsutton@samba.org>
2024-08-27 07:19:32 +00:00

1798 lines
42 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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 "libcli/security/session.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 cant 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 couldnt 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 couldnt find a key meeting the requirements —
* thats OK, presumably. Its not critical if we cant
* 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,
const bool current_key_becomes_previous,
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 (current_key_becomes_previous && 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 callers 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;
const DATA_BLOB *found_pwd_id = NULL;
struct ldb_request *old_pw_req = NULL;
struct ldb_request *new_pw_req = NULL;
struct ldb_request *pwd_id_req = NULL;
struct ldb_dn *account_dn = 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
* nullterminated.
*/
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 nowcurrent password
* will become the previous password automatically. We dont
* 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,
current_key_becomes_previous,
&pwd_id_req);
if (ret) {
goto out;
}
{
/*
* Remember the original managed password ID so that we can
* confirm it hasnt changed when we perform the update.
*/
const struct ldb_val *pwd_id_blob = ldb_msg_find_ldb_val(
msg, "msDS-ManagedPasswordId");
if (pwd_id_blob != NULL) {
DATA_BLOB found_pwd_id_data = {};
DATA_BLOB *found_pwd_id_blob = NULL;
found_pwd_id_blob = talloc(tmp_ctx, DATA_BLOB);
if (found_pwd_id_blob == NULL) {
ret = ldb_oom(ldb);
goto out;
}
found_pwd_id_data = data_blob_dup_talloc(
found_pwd_id_blob, *pwd_id_blob);
if (found_pwd_id_data.length != pwd_id_blob->length) {
ret = ldb_oom(ldb);
goto out;
}
*found_pwd_id_blob = found_pwd_id_data;
found_pwd_id = found_pwd_id_blob;
}
}
account_dn = ldb_dn_copy(tmp_ctx, msg->dn);
if (account_dn == NULL) {
ret = ldb_oom(ldb);
goto out;
}
update = talloc(tmp_ctx, struct gmsa_update);
if (update == NULL) {
ret = ldb_oom(ldb);
goto out;
}
*update = (struct gmsa_update){
.dn = talloc_steal(update, account_dn),
.found_pwd_id = talloc_steal(update, found_pwd_id),
.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 cant read the attribute or it isnt 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);
}
bool samdb_gmsa_key_is_recent(const struct ldb_message *msg,
const NTTIME current_time)
{
const struct KeyEnvelopeId *pwd_id = NULL;
struct KeyEnvelopeId pwd_id_buf;
NTTIME key_start_time;
bool ok;
pwd_id = gmsa_get_managed_pwd_id(msg, &pwd_id_buf);
if (pwd_id == NULL) {
return false;
}
ok = gkdi_get_key_start_time(pwd_id->gkid, &key_start_time);
if (!ok) {
return false;
}
if (current_time < key_start_time) {
return false;
}
return current_time - key_start_time < gkdi_max_clock_skew;
}
/*
* Recalculate the managed password of an account. The account referred to by
* msg should be a Group Managed Service Account.
*
* Updated passwords are returned in update_out.
*
* Pass in a nonNULL pointer for return_out if you want the passwords as
* reflected by the msDS-ManagedPassword operational attribute.
*/
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;
/* Calculate the rollover interval. */
ok = samdb_result_gkdi_rollover_interval(msg, &rollover_interval);
if (!ok || rollover_interval == 0) {
/* We cant 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 accounts 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 accounts passwords should be updated, we
* do not validate that the unicodePwd attribute is uptodate, 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 cant always rely
* on its presence.
*
* All this means that an admin (or a DC that doesnt support gMSAs)
* could reset a gMSAs password outside of the normal procedure, and
* the account would then have the wrong password until the key was due
* to roll over again. Theres nothing much we can do about this if we
* dont want to rederive and verify the password every time we look up
* the keys.
*/
/*
* Administrators should be careful not to set a DCs clock too far in
* the future, or a gMSAs keys may be stuck at that future time and
* stop updating until said time rolls around for real.
*/
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 accounts old enough to have a
* previous password, but it doesnt
* have a previous password ID for some
* reason. This can happen in our tests
* (python/samba/krb5/gmsa_tests.py)
* when were 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 accounts 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 hasnt 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;
}
static void gmsa_update_debug(const struct gmsa_update *gmsa_update)
{
struct ldb_dn *dn = NULL;
const char *account_dn = "<unknown>";
if (!CHECK_DEBUGLVL(DBGLVL_NOTICE)) {
return;
}
dn = gmsa_update->dn;
if (dn != NULL) {
const char *dn_str = NULL;
dn_str = ldb_dn_get_linearized(dn);
if (dn_str != NULL) {
account_dn = dn_str;
}
}
DBG_NOTICE("Updating keys for Group Managed Service Account %s\n",
account_dn);
}
static int gmsa_perform_request(struct ldb_context *ldb,
struct ldb_request *req)
{
int ret = LDB_SUCCESS;
if (req == NULL) {
return LDB_SUCCESS;
}
ret = ldb_request(ldb, req);
if (ret) {
return ret;
}
return ldb_wait(req->handle, LDB_WAIT_ALL);
}
static bool dsdb_data_blobs_equal(const DATA_BLOB *d1, const DATA_BLOB *d2)
{
if (d1 == NULL && d2 == NULL) {
return true;
}
if (d1 == NULL || d2 == NULL) {
return false;
}
{
const int cmp = data_blob_cmp(d1, d2);
return cmp == 0;
}
}
int dsdb_update_gmsa_entry_keys(TALLOC_CTX *mem_ctx,
struct ldb_context *ldb,
const struct gmsa_update *gmsa_update)
{
TALLOC_CTX *tmp_ctx = NULL;
int ret = LDB_SUCCESS;
bool in_transaction = false;
if (gmsa_update == NULL) {
ret = ldb_operr(ldb);
goto out;
}
tmp_ctx = talloc_new(mem_ctx);
if (tmp_ctx == NULL) {
ret = ldb_oom(ldb);
goto out;
}
gmsa_update_debug(gmsa_update);
/* The following must take place in a single transaction. */
ret = ldb_transaction_start(ldb);
if (ret) {
goto out;
}
in_transaction = true;
{
/*
* Before performing the update, ensure that the managed
* password ID in the database has the value we expect.
*/
struct ldb_result *res = NULL;
const struct ldb_val *pwd_id_blob = NULL;
static const char *const managed_pwd_id_attr[] = {
"msDS-ManagedPasswordId",
NULL,
};
if (gmsa_update->dn == NULL) {
ret = ldb_operr(ldb);
goto out;
}
ret = dsdb_search_dn(ldb,
tmp_ctx,
&res,
gmsa_update->dn,
managed_pwd_id_attr,
0);
if (ret) {
goto out;
}
if (res->count != 1) {
ret = ldb_error(
ldb,
LDB_ERR_NO_SUCH_OBJECT,
"failed to find Group Managed Service Account "
"to verify managed password ID");
goto out;
}
pwd_id_blob = ldb_msg_find_ldb_val(res->msgs[0],
"msDS-ManagedPasswordId");
if (!dsdb_data_blobs_equal(pwd_id_blob,
gmsa_update->found_pwd_id))
{
/*
* The accounts managed password ID doesnt match what
* we thought it was — cancel the update. If the caller
* needs the latest values, it will retry the search,
* performing the update again if necessary.
*/
ret = LDB_SUCCESS;
goto out;
}
}
/*
* First update the previous password (if the request is not NULL,
* indicating that the previous password already matches the password of
* the account).
*/
ret = gmsa_perform_request(ldb, gmsa_update->old_pw_req);
if (ret) {
goto out;
}
/* Then update the current password. */
ret = gmsa_perform_request(ldb, gmsa_update->new_pw_req);
if (ret) {
goto out;
}
/* Finally, update the msDS-ManagedPasswordId attribute. */
ret = gmsa_perform_request(ldb, gmsa_update->pwd_id_req);
if (ret) {
goto out;
}
/* Commit the transaction. */
ret = ldb_transaction_commit(ldb);
in_transaction = false;
if (ret) {
goto out;
}
out:
if (in_transaction) {
int ret2 = ldb_transaction_cancel(ldb);
if (ret2) {
ret = ret2;
}
}
talloc_free(tmp_ctx);
return ret;
}
int dsdb_update_gmsa_keys(TALLOC_CTX *mem_ctx,
struct ldb_context *ldb,
const struct ldb_result *res,
bool *retry_out)
{
TALLOC_CTX *tmp_ctx = NULL;
int ret = LDB_SUCCESS;
bool retry = false;
unsigned i;
NTTIME current_time;
bool am_rodc = true;
/*
* This is non-zero if we are local to the sam.ldb, this is an
* opaque set by the samba_dsdb module
*/
void *samba_dsdb_opaque = ldb_get_opaque(
ldb, DSDB_OPAQUE_PARTITION_MODULE_MSG_OPAQUE_NAME);
if (samba_dsdb_opaque == NULL) {
/*
* We are not connected locally, so no point trying to
* set passwords
*/
*retry_out = false;
return LDB_SUCCESS;
}
{
/* Calculate the current time, as reckoned for gMSAs. */
bool ok = dsdb_gmsa_current_time(ldb, &current_time);
if (!ok) {
ret = ldb_operr(ldb);
goto out;
}
}
tmp_ctx = talloc_new(mem_ctx);
if (tmp_ctx == NULL) {
ret = ldb_oom(ldb);
goto out;
}
/* Are we operating as an RODC? */
ret = samdb_rodc(ldb, &am_rodc);
if (ret != LDB_SUCCESS) {
DBG_WARNING("unable to tell if we are an RODC\n");
goto out;
}
/* Loop through each entry in the results. */
for (i = 0; i < res->count; ++i) {
struct ldb_message *msg = res->msgs[i];
struct gmsa_update *gmsa_update = NULL;
const bool is_gmsa = dsdb_account_is_gmsa(ldb, msg);
/* Is the account a Group Managed Service Account? */
if (!is_gmsa) {
/*
* Its not a gMSA, and theres nothing more to do for
* this result.
*/
continue;
}
if (am_rodc) {
static const char *const secret_attributes[] = {
DSDB_SECRET_ATTRIBUTES};
size_t j;
/*
* If were an RODC, we wont be able to update the
* database entry with the new gMSA keys. The simplest
* thing to do is redact all the password attributes in
* the message. If our caller is the KDC, it will
* recognize the missing keys and dispatch a referral to
* a writable DC. */
for (j = 0; j < ARRAY_SIZE(secret_attributes); ++j) {
ldb_msg_remove_attr(msg, secret_attributes[j]);
}
/* Proceed to the next search result. */
continue;
}
/* Update any old gMSA state. */
ret = gmsa_recalculate_managed_pwd(
tmp_ctx, ldb, msg, current_time, &gmsa_update, NULL);
if (ret) {
goto out;
}
if (gmsa_update == NULL) {
/*
* The usual case; the keys are uptodate, and theres
* nothing more to do for this result.
*/
continue;
}
ret = dsdb_update_gmsa_entry_keys(tmp_ctx,
ldb,
gmsa_update);
if (ret) {
goto out;
}
/*
* Since the database entry has been updated, the caller will
* need to perform the search again.
*/
retry = true;
}
*retry_out = retry;
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);
}
/* Set the current time. Caller to supply valid unsigned long long talloc pointer and manage lifetime */
bool dsdb_gmsa_set_current_time(struct ldb_context *ldb, unsigned long long *current_time_talloc)
{
int ret = ldb_set_opaque(ldb, DSDB_GMSA_TIME_OPAQUE, current_time_talloc);
if (ret != LDB_SUCCESS) {
return false;
}
return true;
}