From: Andrew Bartlett Date: Mon, 20 May 2024 23:14:50 +0000 (+1200) Subject: kdc: Detect (about to) expire UF_SMARTCARD_REQUIRED accounts and rotate passwords X-Git-Tag: tdb-1.4.11~402 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=1e1c80656f7d19d1cfde118bdba75a576da978f7;p=thirdparty%2Fsamba.git kdc: Detect (about to) expire UF_SMARTCARD_REQUIRED accounts and rotate passwords This ensures that before the KDC starts to process the entry we check if it is expired and rotate it. As an account with UF_SMARTCARD_REQUIRED simply can not expire unless msDS-ExpirePasswordsOnSmartCardOnlyAccounts is set and the Domain Functional Level is >= 2016 we do not need to do configuration checks here. Signed-off-by: Andrew Bartlett Signed-off-by: Jo Sutton Pair-programmed-by: Jo Sutton Reviewed-by: Jo Sutton --- diff --git a/selftest/knownfail_heimdal_kdc b/selftest/knownfail_heimdal_kdc index fd23bdd740d..167e7e71ec3 100644 --- a/selftest/knownfail_heimdal_kdc +++ b/selftest/knownfail_heimdal_kdc @@ -72,11 +72,8 @@ # PK-INIT tests # ^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_no_des3.ad_dc -^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_ntlm_from_pac_must_change_now -^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_ntlm_from_pac_smartcard_required_must_change_now\( -^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_now\( -^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_expired -^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_soon\( +^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_ntlm_from_pac_must_change_now\( +^samba.tests.krb5.pkinit_tests.samba.tests.krb5.pkinit_tests.PkInitTests.test_pkinit_smartcard_required_must_change_before_tgt_expiry\( # # Windows 2000 PK-INIT tests # diff --git a/source4/kdc/db-glue.c b/source4/kdc/db-glue.c index e1a62552afe..fb835588dcf 100644 --- a/source4/kdc/db-glue.c +++ b/source4/kdc/db-glue.c @@ -28,6 +28,7 @@ #include "auth/auth_sam.h" #include "dsdb/gmsa/util.h" #include "dsdb/samdb/samdb.h" +#include "dsdb/common/proto.h" #include "dsdb/common/util.h" #include "librpc/gen_ndr/ndr_drsblobs.h" #include "param/param.h" @@ -1235,8 +1236,9 @@ static krb5_error_code samba_kdc_message2entry(krb5_context context, const struct authn_kerberos_client_policy *authn_client_policy = NULL; const struct authn_server_policy *authn_server_policy = NULL; - int64_t enforced_tgt_lifetime_raw; const bool user2user = (flags & SDB_F_USER2USER_PRINCIPAL); + int64_t lifetime_secs; + int effective_lifetime_secs; *entry = (struct sdb_entry) {}; @@ -1606,22 +1608,25 @@ static krb5_error_code samba_kdc_message2entry(krb5_context context, } } - enforced_tgt_lifetime_raw = authn_policy_enforced_tgt_lifetime_raw(authn_client_policy); - if (enforced_tgt_lifetime_raw != 0) { - int64_t lifetime_secs = enforced_tgt_lifetime_raw; + entry->skdc_entry->enforced_tgt_lifetime_nt_ticks = authn_policy_enforced_tgt_lifetime_raw(authn_client_policy); + lifetime_secs = entry->skdc_entry->enforced_tgt_lifetime_nt_ticks; + effective_lifetime_secs = *entry->max_life; + if (lifetime_secs != 0) { lifetime_secs /= INT64_C(1000) * 1000 * 10; lifetime_secs = MIN(lifetime_secs, INT_MAX); lifetime_secs = MAX(lifetime_secs, INT_MIN); + effective_lifetime_secs = MIN(effective_lifetime_secs, + lifetime_secs); + /* * Set both lifetime and renewal time based only on the * configured maximum lifetime — not on the configured renewal * time. Yes, this is what Windows does. */ - lifetime_secs = MIN(*entry->max_life, lifetime_secs); - *entry->max_life = lifetime_secs; - *entry->max_renew = lifetime_secs; + *entry->max_life = effective_lifetime_secs; + *entry->max_renew = effective_lifetime_secs; } if (ent_type == SAMBA_KDC_ENT_TYPE_CLIENT && (flags & SDB_F_FOR_AS_REQ)) { @@ -1662,17 +1667,29 @@ static krb5_error_code samba_kdc_message2entry(krb5_context context, entry->flags.forwardable = 0; entry->flags.proxiable = 0; - if (enforced_tgt_lifetime_raw == 0) { + if (lifetime_secs == 0) { /* * If a TGT lifetime hasn’t been set, Protected * Users enforces a four hour TGT lifetime. */ - *entry->max_life = MIN(*entry->max_life, 4 * 60 * 60); - *entry->max_renew = MIN(*entry->max_renew, 4 * 60 * 60); + + effective_lifetime_secs = 4 * 60 * 60; + + *entry->max_life = MIN(*entry->max_life, effective_lifetime_secs); + *entry->max_renew = MIN(*entry->max_renew, effective_lifetime_secs); } } } + if (effective_lifetime_secs != lifetime_secs) { + /* + * Since ‘effective_lifetime_secs’ has changed, update + * ‘enforced_tgt_lifetime_nt_ticks’ to match. + */ + entry->skdc_entry->enforced_tgt_lifetime_nt_ticks = + effective_lifetime_secs * (INT64_C(1000) * 1000 * 10); + } + if (rid == DOMAIN_RID_KRBTGT || is_rodc) { bool enable_fast; @@ -2726,6 +2743,69 @@ static krb5_error_code samba_kdc_lookup_client(krb5_context context, return 0; } +/* This is for the reset UF_SMARTCARD_REQUIRED password, but only in the expired case */ +static void smartcard_random_pw_update(TALLOC_CTX *mem_ctx, + struct ldb_context *ldb, + struct ldb_dn *dn) +{ + int ret; + NTSTATUS status = NT_STATUS_OK; + /* + * The password_hash module expects these passwords to be + * null‐terminated, so we zero-initialise with {} + */ + uint8_t new_password[128] = {}; + DATA_BLOB password_blob = {.data = new_password, + .length = sizeof(new_password)}; + + /* + * This will be re-randomised in password_hash, but want this + * to be random in a failure case + */ + generate_random_buffer(new_password, sizeof(new_password)-2); + + ret = ldb_transaction_start(ldb); + if (ret != LDB_SUCCESS) { + DBG_ERR("Transaction start for automated " + "password rotation " + "of soon-to-expire " + "underlying password on account %s with " + "UF_SMARTCARD_REQUIRED failed: %s\n", + ldb_dn_get_linearized(dn), + ldb_errstring(ldb)); + return; + } + + status = samdb_set_password(ldb, + mem_ctx, + dn, + &password_blob, + NULL, + DSDB_PASSWORD_KDC_RESET_SMARTCARD_ACCOUNT_PASSWORD, + NULL, NULL); + if (!NT_STATUS_IS_OK(status)) { + ldb_transaction_cancel(ldb); + DBG_ERR("Automated password rotation " + "of soon-to-expire " + "underlying password on account %s with " + "UF_SMARTCARD_REQUIRED failed: %s\n", + ldb_dn_get_linearized(dn), + nt_errstr(status)); + return; + } + + ret = ldb_transaction_commit(ldb); + if (ret != LDB_SUCCESS) { + DBG_ERR("Transaction commit for automated " + "password rotation " + "of soon-to-expire " + "underlying password on account %s with " + "UF_SMARTCARD_REQUIRED failed: %s\n", + ldb_dn_get_linearized(dn), + ldb_errstring(ldb)); + } +} + static krb5_error_code samba_kdc_fetch_client(krb5_context context, struct samba_kdc_db_context *kdc_db_ctx, TALLOC_CTX *mem_ctx, @@ -2737,19 +2817,114 @@ static krb5_error_code samba_kdc_fetch_client(krb5_context context, struct ldb_dn *realm_dn; krb5_error_code ret; struct ldb_message *msg = NULL; + int tries = 0; - ret = samba_kdc_lookup_client(context, kdc_db_ctx, - mem_ctx, principal, user_attrs, DSDB_SEARCH_UPDATE_MANAGED_PASSWORDS, - &realm_dn, &msg); - if (ret != 0) { - return ret; + /* + * We will try up to 3 times to rotate the expired or soon to + * expire password of a UF_SMARTCARD_REQUIRED account, + * re-starting the search if we attempted a password change + * (allowing the new secrets and expiry to be used). + * + * A failure to change the password is not fatal, as password + * changes are attempted before the ultimate expiry. This way + * the server will still process an AS-REQ with PKINIT until + * it (later, in the KDC code) finds the password has actually + * expired. + */ + while (tries++ <= 2) { + uint32_t attr_flags_computed; + + /* + * When we look up the client, we also pre-rotate any expired + * passwords in the UF_SMARTCARD_REQUIRED case + */ + ret = samba_kdc_lookup_client(context, kdc_db_ctx, + mem_ctx, principal, user_attrs, DSDB_SEARCH_UPDATE_MANAGED_PASSWORDS, + &realm_dn, &msg); + if (ret != 0) { + return ret; + } + + ret = samba_kdc_message2entry(context, kdc_db_ctx, mem_ctx, + principal, SAMBA_KDC_ENT_TYPE_CLIENT, + flags, kvno, + realm_dn, msg, entry); + if (ret != 0) { + return ret; + } + + if (!(flags & SDB_F_FOR_AS_REQ)) { + break; + } + + /* This is the check on UF_SMARTCARD_REQUIRED */ + if (!(entry->flags.require_hwauth)) { + break; + } + + /* + * This check is also the configuration gate: the + * operational module will set a + * msDS-UserPasswordExpiryTimeComputed that in turn is + * represented here as NULL unless the + * expiry/auto-rotation of UF_SMARTCARD_REQUIRED + * accounts is enabled + */ + if (entry->pw_end == NULL) { + break; + } + + attr_flags_computed + = ldb_msg_find_attr_as_uint(msg, + "msDS-User-Account-Control-Computed", + UF_PASSWORD_EXPIRED /* A safe if chaotic default */); + if (attr_flags_computed & UF_PASSWORD_EXPIRED) { + /* Already expired, keep processing */ + } else { + /* + * Will expire soon, but not already expired. + * + * However we must first + * check if this is before the TGT is due to + * expire. + */ + NTTIME must_change_time + = samdb_result_nttime(msg, + "msDS-UserPasswordExpiryTimeComputed", + 0); + if (must_change_time + > entry->skdc_entry->enforced_tgt_lifetime_nt_ticks + entry->skdc_entry->current_nttime) { + /* Password will not expire before TGT will */ + break; + } + /* Keep processing */ + } + + if (kdc_db_ctx->rodc) { + /* + * Nothing we can do locally on an RODC. So + * we trigger pushing the user back to the + * full DC to ensure the PW is rotated. + */ + ret = SDB_ERR_NOT_FOUND_HERE; + break; + } + + /* + * Reset PW to random value. All we can do is loop + * and hope we succeed again on failure, if we succeed + * then we will pass the tests above and break out of the loop + * + * We don't want to fail on error here as we might + * still be able to provide service to the client if + * the password is not yet actually expired. They may get + * better luck at another KDC or at a later AS-REQ. + */ + smartcard_random_pw_update(mem_ctx, kdc_db_ctx->samdb, entry->skdc_entry->msg->dn); } - ret = samba_kdc_message2entry(context, kdc_db_ctx, mem_ctx, - principal, SAMBA_KDC_ENT_TYPE_CLIENT, - flags, kvno, - realm_dn, msg, entry); return ret; + } static krb5_error_code samba_kdc_fetch_krbtgt(krb5_context context, diff --git a/source4/kdc/samba_kdc.h b/source4/kdc/samba_kdc.h index 3c5116092b9..c9d41a07d12 100644 --- a/source4/kdc/samba_kdc.h +++ b/source4/kdc/samba_kdc.h @@ -88,6 +88,7 @@ struct samba_kdc_entry { bool claims_from_db_are_initialized : 1; bool group_managed_service_account : 1; NTTIME current_nttime; + int64_t enforced_tgt_lifetime_nt_ticks; }; extern struct hdb_method hdb_samba4_interface; diff --git a/source4/kdc/wscript_build b/source4/kdc/wscript_build index cf5422f48a7..9b73adfbe0f 100644 --- a/source4/kdc/wscript_build +++ b/source4/kdc/wscript_build @@ -135,7 +135,7 @@ bld.SAMBA_LIBRARY('pac', bld.SAMBA_LIBRARY('db-glue', source='db-glue.c', - deps='ldb auth4_sam common_auth samba-credentials sdb samba-hostconfig com_err RPC_NDR_IRPC MESSAGING PAC_GLUE authn_policy_util', + deps='ldb auth4_sam common_auth samba-credentials sdb samba-hostconfig com_err RPC_NDR_IRPC MESSAGING PAC_GLUE authn_policy_util samdb-common', private_library=True, )