diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 05:31:45 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 05:31:45 +0000 |
commit | 74aa0bc6779af38018a03fd2cf4419fe85917904 (patch) | |
tree | 9cb0681aac9a94a49c153d5823e7a55d1513d91f /src/providers/krb5 | |
parent | Initial commit. (diff) | |
download | sssd-74aa0bc6779af38018a03fd2cf4419fe85917904.tar.xz sssd-74aa0bc6779af38018a03fd2cf4419fe85917904.zip |
Adding upstream version 2.9.4.upstream/2.9.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/providers/krb5')
-rw-r--r-- | src/providers/krb5/krb5_access.c | 220 | ||||
-rw-r--r-- | src/providers/krb5/krb5_auth.c | 1356 | ||||
-rw-r--r-- | src/providers/krb5/krb5_auth.h | 158 | ||||
-rw-r--r-- | src/providers/krb5/krb5_ccache.c | 796 | ||||
-rw-r--r-- | src/providers/krb5/krb5_ccache.h | 73 | ||||
-rw-r--r-- | src/providers/krb5/krb5_child.c | 4247 | ||||
-rw-r--r-- | src/providers/krb5/krb5_child_handler.c | 1022 | ||||
-rw-r--r-- | src/providers/krb5/krb5_common.c | 1261 | ||||
-rw-r--r-- | src/providers/krb5/krb5_common.h | 249 | ||||
-rw-r--r-- | src/providers/krb5/krb5_delayed_online_authentication.c | 386 | ||||
-rw-r--r-- | src/providers/krb5/krb5_init.c | 229 | ||||
-rw-r--r-- | src/providers/krb5/krb5_init_shared.c | 105 | ||||
-rw-r--r-- | src/providers/krb5/krb5_init_shared.h | 29 | ||||
-rw-r--r-- | src/providers/krb5/krb5_keytab.c | 231 | ||||
-rw-r--r-- | src/providers/krb5/krb5_opts.c | 50 | ||||
-rw-r--r-- | src/providers/krb5/krb5_opts.h | 30 | ||||
-rw-r--r-- | src/providers/krb5/krb5_renew_tgt.c | 631 | ||||
-rw-r--r-- | src/providers/krb5/krb5_utils.c | 605 | ||||
-rw-r--r-- | src/providers/krb5/krb5_utils.h | 59 | ||||
-rw-r--r-- | src/providers/krb5/krb5_wait_queue.c | 373 |
20 files changed, 12110 insertions, 0 deletions
diff --git a/src/providers/krb5/krb5_access.c b/src/providers/krb5/krb5_access.c new file mode 100644 index 0000000..2ae5abe --- /dev/null +++ b/src/providers/krb5/krb5_access.c @@ -0,0 +1,220 @@ +/* + SSSD + + Kerberos 5 Backend Module - access control + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2010 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include "util/util.h" +#include "providers/krb5/krb5_auth.h" +#include "providers/krb5/krb5_common.h" +#include "providers/krb5/krb5_utils.h" + +struct krb5_access_state { + struct tevent_context *ev; + struct be_ctx *be_ctx; + + struct pam_data *pd; + struct krb5_ctx *krb5_ctx; + struct krb5child_req *kr; + + bool access_allowed; +}; + +static void krb5_access_done(struct tevent_req *subreq); +struct tevent_req *krb5_access_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct be_ctx *be_ctx, + struct pam_data *pd, + struct krb5_ctx *krb5_ctx) +{ + struct krb5_access_state *state; + struct tevent_req *req; + struct tevent_req *subreq; + int ret; + const char **attrs; + struct ldb_result *res; + struct sss_domain_info *dom; + + req = tevent_req_create(mem_ctx, &state, struct krb5_access_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create failed.\n"); + return NULL; + } + + state->ev = ev; + state->be_ctx = be_ctx; + state->pd = pd; + state->krb5_ctx = krb5_ctx; + state->access_allowed = false; + + ret = get_domain_or_subdomain(be_ctx, pd->domain, &dom); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "get_domain_or_subdomain failed.\n"); + goto done; + } + + ret = krb5_setup(state, pd, dom, krb5_ctx, &state->kr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_setup failed.\n"); + goto done; + } + + if (pd->cmd != SSS_PAM_ACCT_MGMT) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Unexpected pam task %d.\n", pd->cmd); + ret = EINVAL; + goto done; + } + + attrs = talloc_array(state, const char *, 5); + if (attrs == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_array failed.\n"); + ret = ENOMEM; + goto done; + } + + attrs[0] = SYSDB_UPN; + attrs[1] = SYSDB_UIDNUM; + attrs[2] = SYSDB_GIDNUM; + attrs[3] = SYSDB_CANONICAL_UPN; + attrs[4] = NULL; + + ret = sysdb_get_user_attr(state, be_ctx->domain, state->pd->user, attrs, + &res); + if (ret) { + DEBUG(SSSDBG_FUNC_DATA, + "sysdb search for upn of user [%s] failed.\n", pd->user); + goto done; + } + + switch (res->count) { + case 0: + DEBUG(SSSDBG_FUNC_DATA, + "No attributes for user [%s] found.\n", pd->user); + ret = ENOENT; + goto done; + break; + case 1: + ret = find_or_guess_upn(state, res->msgs[0], krb5_ctx, be_ctx->domain, + state->kr->user, pd->domain, &state->kr->upn); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "find_or_guess_upn failed.\n"); + goto done; + } + + state->kr->uid = ldb_msg_find_attr_as_uint64(res->msgs[0], SYSDB_UIDNUM, + 0); + if (state->kr->uid == 0) { + DEBUG(SSSDBG_CONF_SETTINGS, + "UID for user [%s] not known.\n", pd->user); + ret = ENOENT; + goto done; + } + + state->kr->gid = ldb_msg_find_attr_as_uint64(res->msgs[0], SYSDB_GIDNUM, + 0); + if (state->kr->gid == 0) { + DEBUG(SSSDBG_CONF_SETTINGS, + "GID for user [%s] not known.\n", pd->user); + ret = ENOENT; + goto done; + } + + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "User search for [%s] returned > 1 results!\n", pd->user); + ret = EINVAL; + goto done; + break; + } + + subreq = handle_child_send(state, state->ev, state->kr); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "handle_child_send failed.\n"); + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, krb5_access_done, req); + return req; + +done: + if (ret == EOK) { + tevent_req_done(req); + } else { + tevent_req_error(req, ret); + } + tevent_req_post(req, state->ev); + return req; +} + +static void krb5_access_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, struct tevent_req); + struct krb5_access_state *state = tevent_req_data(req, + struct krb5_access_state); + int ret; + uint8_t *buf = NULL; + ssize_t len = -1; + int32_t msg_status; + + ret = handle_child_recv(subreq, state, &buf, &len); + talloc_free(subreq); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "child failed [%d][%s].\n", ret, strerror(ret)); + goto fail; + } + + if ((size_t) len != sizeof(int32_t)) { + DEBUG(SSSDBG_CRIT_FAILURE, "message has the wrong size.\n"); + ret = EINVAL; + goto fail; + } + + SAFEALIGN_COPY_INT32(&msg_status, buf, NULL); + + if (msg_status == EOK) { + state->access_allowed = true; + } else { + state->access_allowed = false; + } + + tevent_req_done(req); + return; + +fail: + tevent_req_error(req, ret); + return; +} + +int krb5_access_recv(struct tevent_req *req, bool *access_allowed) +{ + struct krb5_access_state *state = tevent_req_data(req, + struct krb5_access_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + *access_allowed = state->access_allowed; + + return EOK; +} diff --git a/src/providers/krb5/krb5_auth.c b/src/providers/krb5/krb5_auth.c new file mode 100644 index 0000000..be34880 --- /dev/null +++ b/src/providers/krb5/krb5_auth.c @@ -0,0 +1,1356 @@ +/* + SSSD + + Kerberos 5 Backend Module + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2009-2010 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <errno.h> +#include <sys/time.h> + +#include <sys/types.h> +#include <sys/wait.h> +#include <pwd.h> +#include <sys/stat.h> + +#include <security/pam_modules.h> + +#include "util/util.h" +#include "util/crypto/sss_crypto.h" +#include "util/find_uid.h" +#include "util/auth_utils.h" +#include "db/sysdb.h" +#include "util/sss_utf8.h" +#include "util/child_common.h" +#include "providers/krb5/krb5_auth.h" +#include "providers/krb5/krb5_utils.h" +#include "providers/krb5/krb5_ccache.h" + +#define NON_POSIX_CCNAME_FMT "MEMORY:sssd_nonposix_dummy_%u" + +static int krb5_mod_ccname(TALLOC_CTX *mem_ctx, + struct sysdb_ctx *sysdb, + struct sss_domain_info *domain, + const char *name, + const char *ccname, + int mod_op) +{ + TALLOC_CTX *tmpctx; + struct sysdb_attrs *attrs; + int ret; + errno_t sret; + bool in_transaction = false; + + if (name == NULL || ccname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing user or ccache name.\n"); + return EINVAL; + } + + if (mod_op != SYSDB_MOD_REP && mod_op != SYSDB_MOD_DEL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unsupported operation [%d].\n", mod_op); + return EINVAL; + } + + DEBUG(SSSDBG_TRACE_ALL, "%s ccname [%s] for user [%s].\n", + mod_op == SYSDB_MOD_REP ? "Save" : "Delete", ccname, name); + + tmpctx = talloc_new(mem_ctx); + if (!tmpctx) { + return ENOMEM; + } + + attrs = sysdb_new_attrs(tmpctx); + if (!attrs) { + ret = ENOMEM; + goto done; + } + + ret = sysdb_attrs_add_string(attrs, SYSDB_CCACHE_FILE, ccname); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "sysdb_attrs_add_string failed.\n"); + goto done; + } + + ret = sysdb_transaction_start(sysdb); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Error %d starting transaction (%s)\n", ret, strerror(ret)); + goto done; + } + in_transaction = true; + + ret = sysdb_set_user_attr(domain, name, attrs, mod_op); + if (ret != EOK) { + DEBUG(SSSDBG_TRACE_FUNC, "Error: %d (%s)\n", ret, strerror(ret)); + goto done; + } + + ret = sysdb_transaction_commit(sysdb); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to commit transaction!\n"); + goto done; + } + in_transaction = false; + +done: + if (in_transaction) { + sret = sysdb_transaction_cancel(sysdb); + if (sret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to cancel transaction\n"); + } + } + talloc_zfree(tmpctx); + return ret; +} + +static int krb5_save_ccname(TALLOC_CTX *mem_ctx, + struct sysdb_ctx *sysdb, + struct sss_domain_info *domain, + const char *name, + const char *ccname) +{ + return krb5_mod_ccname(mem_ctx, sysdb, domain, name, ccname, + SYSDB_MOD_REP); +} + +static int krb5_delete_ccname(TALLOC_CTX *mem_ctx, + struct sysdb_ctx *sysdb, + struct sss_domain_info *domain, + const char *name, + const char *ccname) +{ + return krb5_mod_ccname(mem_ctx, sysdb, domain, name, ccname, + SYSDB_MOD_DEL); +} + +static int krb5_cleanup(void *ptr) +{ + struct krb5child_req *kr = talloc_get_type(ptr, struct krb5child_req); + + if (kr == NULL) return EOK; + + memset(kr, 0, sizeof(struct krb5child_req)); + + return EOK; +} + +static errno_t +get_krb_primary(struct map_id_name_to_krb_primary *name_to_primary, + char *id_prov_name, bool cs, const char **_krb_primary) +{ + errno_t ret; + int i = 0; + + while(name_to_primary != NULL && + name_to_primary[i].id_name != NULL && + name_to_primary[i].krb_primary != NULL) { + + if (sss_string_equal(cs, name_to_primary[i].id_name, id_prov_name)) { + *_krb_primary = name_to_primary[i].krb_primary; + ret = EOK; + goto done; + } + i++; + } + + /* Handle also the case of name_to_primary being NULL */ + ret = ENOENT; + +done: + return ret; +} + +errno_t krb5_setup(TALLOC_CTX *mem_ctx, + struct pam_data *pd, + struct sss_domain_info *dom, + struct krb5_ctx *krb5_ctx, + struct krb5child_req **_krb5_req) +{ + struct krb5child_req *kr; + const char *mapped_name; + TALLOC_CTX *tmp_ctx; + errno_t ret; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + return ENOMEM; + } + + kr = talloc_zero(tmp_ctx, struct krb5child_req); + if (kr == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc failed.\n"); + ret = ENOMEM; + goto done; + } + kr->is_offline = false; + talloc_set_destructor((TALLOC_CTX *) kr, krb5_cleanup); + + kr->pd = pd; + kr->dom = dom; + kr->krb5_ctx = krb5_ctx; + + ret = get_krb_primary(krb5_ctx->name_to_primary, + pd->user, dom->case_sensitive, &mapped_name); + if (ret == EOK) { + DEBUG(SSSDBG_TRACE_FUNC, "Setting mapped name to: %s\n", mapped_name); + kr->user = mapped_name; + + kr->kuserok_user = sss_output_name(kr, kr->user, + dom->case_sensitive, 0); + if (kr->kuserok_user == NULL) { + ret = ENOMEM; + goto done; + } + } else if (ret == ENOENT) { + DEBUG(SSSDBG_TRACE_ALL, "No mapping for: %s\n", pd->user); + kr->user = pd->user; + + kr->kuserok_user = sss_output_name(kr, kr->user, + dom->case_sensitive, 0); + if (kr->kuserok_user == NULL) { + ret = ENOMEM; + goto done; + } + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "get_krb_primary failed - %s:[%d]\n", + sss_strerror(ret), ret); + goto done; + } + + ret = EOK; + +done: + if (ret == EOK) { + *_krb5_req = talloc_steal(mem_ctx, kr); + } + talloc_free(tmp_ctx); + return ret; +} + + +static void krb5_auth_cache_creds(struct krb5_ctx *krb5_ctx, + struct sss_domain_info *domain, + struct confdb_ctx *cdb, + struct pam_data *pd, uid_t uid, + int *pam_status, int *dp_err) +{ + const char *password = NULL; + errno_t ret; + + ret = sss_authtok_get_password(pd->authtok, &password, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_FATAL_FAILURE, + "Failed to get password [%d] %s. Delayed authentication is only " + "available for password authentication (single factor).\n", + ret, strerror(ret)); + *pam_status = PAM_SYSTEM_ERR; + *dp_err = DP_ERR_OK; + return; + } + + ret = sysdb_cache_auth(domain, pd->user, + password, cdb, true, NULL, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Offline authentication failed\n"); + *pam_status = cached_login_pam_status(ret); + *dp_err = DP_ERR_OK; + return; + } + + ret = add_user_to_delayed_online_authentication(krb5_ctx, domain, pd, uid); + if (ret == ENOTSUP) { + /* This error is not fatal */ + DEBUG(SSSDBG_MINOR_FAILURE, "Delayed authentication not supported\n"); + } else if (ret != EOK) { + /* This error is not fatal */ + DEBUG(SSSDBG_CRIT_FAILURE, + "add_user_to_delayed_online_authentication failed.\n"); + } + *pam_status = PAM_AUTHINFO_UNAVAIL; + *dp_err = DP_ERR_OFFLINE; +} + +static errno_t krb5_auth_prepare_ccache_name(struct krb5child_req *kr, + struct ldb_message *user_msg, + struct be_ctx *be_ctx) +{ + const char *ccname_template; + + switch (kr->dom->type) { + case DOM_TYPE_POSIX: + ccname_template = dp_opt_get_cstring(kr->krb5_ctx->opts, KRB5_CCNAME_TMPL); + + kr->ccname = expand_ccname_template(kr, kr, ccname_template, + kr->krb5_ctx->illegal_path_re, true, + be_ctx->domain->case_sensitive); + if (kr->ccname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "expand_ccname_template failed.\n"); + return ENOMEM; + } + + kr->old_ccname = ldb_msg_find_attr_as_string(user_msg, + SYSDB_CCACHE_FILE, NULL); + if (kr->old_ccname == NULL) { + DEBUG(SSSDBG_TRACE_LIBS, + "No ccache file for user [%s] found.\n", kr->pd->user); + } + break; + case DOM_TYPE_APPLICATION: + DEBUG(SSSDBG_TRACE_FUNC, + "Domain type application, will use in-memory ccache\n"); + kr->ccname = talloc_asprintf(kr, + NON_POSIX_CCNAME_FMT, + sss_rand() % UINT_MAX); + if (kr->ccname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + return ENOMEM; + } + + break; + default: + DEBUG(SSSDBG_FATAL_FAILURE, "Unsupported domain type\n"); + return EINVAL; + } + + return EOK; +} + +static void krb5_auth_store_creds(struct sss_domain_info *domain, + struct pam_data *pd) +{ + const char *password = NULL; + const char *fa2; + size_t password_len; + size_t fa2_len = 0; + int ret = EOK; + + switch(pd->cmd) { + case SSS_CMD_RENEW: + /* The authtok is set to the credential cache + * during renewal. We don't want to save this + * as the cached password. + */ + break; + case SSS_PAM_PREAUTH: + /* There are no credentials available during pre-authentication, + * nothing to do. */ + break; + case SSS_PAM_AUTHENTICATE: + case SSS_PAM_CHAUTHTOK_PRELIM: + if (sss_authtok_get_type(pd->authtok) == SSS_AUTHTOK_TYPE_2FA) { + ret = sss_authtok_get_2fa(pd->authtok, &password, &password_len, + &fa2, &fa2_len); + if (ret == EOK && password_len < + domain->cache_credentials_min_ff_length) { + DEBUG(SSSDBG_FATAL_FAILURE, + "First factor is too short to be cache, " + "minimum length is [%u].\n", + domain->cache_credentials_min_ff_length); + ret = EINVAL; + } + } else if (sss_authtok_get_type(pd->authtok) == + SSS_AUTHTOK_TYPE_PASSWORD) { + ret = sss_authtok_get_password(pd->authtok, &password, NULL); + } else { + DEBUG(SSSDBG_MINOR_FAILURE, "Cannot cache authtok type [%d].\n", + sss_authtok_get_type(pd->authtok)); + ret = EINVAL; + } + break; + case SSS_PAM_CHAUTHTOK: + ret = sss_authtok_get_password(pd->newauthtok, &password, NULL); + break; + default: + DEBUG(SSSDBG_FATAL_FAILURE, + "unsupported PAM command [%d].\n", pd->cmd); + } + + if (ret != EOK) { + DEBUG(SSSDBG_FATAL_FAILURE, + "Failed to get password [%d] %s\n", ret, strerror(ret)); + /* password caching failures are not fatal errors */ + return; + } + + if (password == NULL) { + if (pd->cmd != SSS_CMD_RENEW && pd->cmd != SSS_PAM_PREAUTH) { + DEBUG(SSSDBG_FATAL_FAILURE, + "password not available, offline auth may not work.\n"); + /* password caching failures are not fatal errors */ + } + return; + } + + ret = sysdb_cache_password_ex(domain, pd->user, password, + sss_authtok_get_type(pd->authtok), fa2_len); + if (ret) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to cache password, offline auth may not work." + " (%d)[%s]!?\n", ret, strerror(ret)); + /* password caching failures are not fatal errors */ + } +} + +static bool is_otp_enabled(struct ldb_message *user_msg) +{ + struct ldb_message_element *el; + size_t i; + + el = ldb_msg_find_element(user_msg, SYSDB_AUTH_TYPE); + if (el == NULL) { + return false; + } + + for (i = 0; i < el->num_values; i++) { + if (strcmp((const char * )el->values[i].data, "otp") == 0) { + return true; + } + } + + return false; +} + +/* krb5_auth request */ + +struct krb5_auth_state { + struct tevent_context *ev; + struct be_ctx *be_ctx; + struct pam_data *pd; + struct sysdb_ctx *sysdb; + struct sss_domain_info *domain; + struct krb5_ctx *krb5_ctx; + struct krb5child_req *kr; + + bool search_kpasswd; + + int pam_status; + int dp_err; +}; + +static void krb5_auth_resolve_done(struct tevent_req *subreq); +static void krb5_auth_done(struct tevent_req *subreq); + +struct tevent_req *krb5_auth_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct be_ctx *be_ctx, + struct pam_data *pd, + struct krb5_ctx *krb5_ctx) +{ + const char **attrs; + struct krb5_auth_state *state; + struct ldb_result *res; + struct krb5child_req *kr = NULL; + const char *realm; + struct tevent_req *req; + struct tevent_req *subreq; + enum sss_authtok_type authtok_type; + int ret; + bool otp; + + req = tevent_req_create(mem_ctx, &state, struct krb5_auth_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create failed.\n"); + return NULL; + } + + state->ev = ev; + state->be_ctx = be_ctx; + state->pd = pd; + state->krb5_ctx = krb5_ctx; + state->kr = NULL; + state->pam_status = PAM_SYSTEM_ERR; + state->dp_err = DP_ERR_FATAL; + + ret = get_domain_or_subdomain(be_ctx, pd->domain, &state->domain); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "get_domain_or_subdomain failed.\n"); + goto done; + } + + state->sysdb = state->domain->sysdb; + + authtok_type = sss_authtok_get_type(pd->authtok); + + switch (pd->cmd) { + case SSS_PAM_AUTHENTICATE: + case SSS_PAM_CHAUTHTOK: + if (authtok_type != SSS_AUTHTOK_TYPE_PASSWORD + && authtok_type != SSS_AUTHTOK_TYPE_2FA + && authtok_type != SSS_AUTHTOK_TYPE_2FA_SINGLE + && authtok_type != SSS_AUTHTOK_TYPE_SC_PIN + && authtok_type != SSS_AUTHTOK_TYPE_SC_KEYPAD + && authtok_type != SSS_AUTHTOK_TYPE_OAUTH2 + && authtok_type != SSS_AUTHTOK_TYPE_PASSKEY + && authtok_type != SSS_AUTHTOK_TYPE_PASSKEY_KRB + && authtok_type != SSS_AUTHTOK_TYPE_PASSKEY_REPLY) { + /* handle empty password gracefully */ + if (authtok_type == SSS_AUTHTOK_TYPE_EMPTY) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Illegal empty authtok for user [%s]\n", + pd->user); + state->pam_status = PAM_AUTH_ERR; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + } + + DEBUG(SSSDBG_CRIT_FAILURE, + "Wrong authtok type for user [%s]. " \ + "Expected [%d], got [%d]\n", pd->user, + SSS_AUTHTOK_TYPE_PASSWORD, + authtok_type); + state->pam_status = PAM_SYSTEM_ERR; + state->dp_err = DP_ERR_FATAL; + ret = EINVAL; + goto done; + } + break; + case SSS_PAM_CHAUTHTOK_PRELIM: + if (pd->priv == 1 && + authtok_type != SSS_AUTHTOK_TYPE_PASSWORD) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Password reset by root is not supported.\n"); + state->pam_status = PAM_PERM_DENIED; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + } + break; + case SSS_CMD_RENEW: + if (authtok_type != SSS_AUTHTOK_TYPE_CCFILE) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Wrong authtok type for user [%s]. " \ + "Expected [%d], got [%d]\n", pd->user, + SSS_AUTHTOK_TYPE_CCFILE, + authtok_type); + state->pam_status = PAM_SYSTEM_ERR; + state->dp_err = DP_ERR_FATAL; + ret = EINVAL; + goto done; + } + break; + case SSS_PAM_PREAUTH: + break; + default: + DEBUG(SSSDBG_CONF_SETTINGS, "Unexpected pam task %d.\n", pd->cmd); + state->pam_status = PAM_SYSTEM_ERR; + state->dp_err = DP_ERR_FATAL; + ret = EINVAL; + goto done; + } + + if (be_is_offline(be_ctx) && + (pd->cmd == SSS_PAM_CHAUTHTOK || pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM || + pd->cmd == SSS_CMD_RENEW)) { + DEBUG(SSSDBG_TRACE_ALL, + "Password changes and ticket renewal are not possible " + "while offline.\n"); + state->pam_status = PAM_AUTHINFO_UNAVAIL; + state->dp_err = DP_ERR_OFFLINE; + ret = EOK; + goto done; + } + + attrs = talloc_array(state, const char *, 8); + if (attrs == NULL) { + ret = ENOMEM; + goto done; + } + + attrs[0] = SYSDB_UPN; + attrs[1] = SYSDB_HOMEDIR; + attrs[2] = SYSDB_CCACHE_FILE; + attrs[3] = SYSDB_UIDNUM; + attrs[4] = SYSDB_GIDNUM; + attrs[5] = SYSDB_CANONICAL_UPN; + attrs[6] = SYSDB_AUTH_TYPE; + attrs[7] = NULL; + + ret = krb5_setup(state, pd, state->domain, krb5_ctx, + &state->kr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_setup failed.\n"); + goto done; + } + kr = state->kr; + + ret = sysdb_get_user_attr_with_views(state, state->domain, state->pd->user, + attrs, &res); + if (ret) { + DEBUG(SSSDBG_FUNC_DATA, + "sysdb search for upn of user [%s] failed.\n", pd->user); + state->pam_status = PAM_SYSTEM_ERR; + state->dp_err = DP_ERR_OK; + goto done; + } + + realm = dp_opt_get_cstring(krb5_ctx->opts, KRB5_REALM); + if (realm == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing Kerberos realm.\n"); + ret = ENOENT; + goto done; + } + + switch (res->count) { + case 0: + DEBUG(SSSDBG_FUNC_DATA, + "No attributes for user [%s] found.\n", pd->user); + ret = ENOENT; + goto done; + break; + + case 1: + ret = find_or_guess_upn(state, res->msgs[0], krb5_ctx, be_ctx->domain, + kr->user, pd->domain, &kr->upn); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "find_or_guess_upn failed.\n"); + goto done; + } + + ret = compare_principal_realm(kr->upn, realm, + &kr->upn_from_different_realm); + if (ret != 0) { + DEBUG(SSSDBG_OP_FAILURE, "compare_principal_realm failed.\n"); + goto done; + } + + kr->homedir = sss_view_ldb_msg_find_attr_as_string(state->domain, + res->msgs[0], + SYSDB_HOMEDIR, + NULL); + if (kr->homedir == NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, + "Home directory for user [%s] not known.\n", pd->user); + } + + kr->uid = sss_view_ldb_msg_find_attr_as_uint64(state->domain, + res->msgs[0], + SYSDB_UIDNUM, 0); + if (kr->uid == 0 && state->domain->type == DOM_TYPE_POSIX) { + DEBUG(SSSDBG_CONF_SETTINGS, + "UID for user [%s] not known.\n", pd->user); + ret = ENOENT; + goto done; + } + + kr->gid = sss_view_ldb_msg_find_attr_as_uint64(state->domain, + res->msgs[0], + SYSDB_GIDNUM, 0); + if (kr->gid == 0 && state->domain->type == DOM_TYPE_POSIX) { + DEBUG(SSSDBG_CONF_SETTINGS, + "GID for user [%s] not known.\n", pd->user); + ret = ENOENT; + goto done; + } + + ret = krb5_auth_prepare_ccache_name(kr, res->msgs[0], state->be_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot prepare ccache names!\n"); + goto done; + } + break; + + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "User search for (%s) returned > 1 results!\n", pd->user); + ret = EINVAL; + goto done; + break; + } + + otp = is_otp_enabled(res->msgs[0]); + if (pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM && otp == true) { + /* To avoid consuming the OTP */ + DEBUG(SSSDBG_TRACE_FUNC, + "Skipping password checks for OTP-enabled user\n"); + state->pam_status = PAM_SUCCESS; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + } + + kr->srv = NULL; + kr->kpasswd_srv = NULL; + + state->search_kpasswd = false; + subreq = be_resolve_server_send(state, state->ev, state->be_ctx, + state->krb5_ctx->service->name, + state->kr->srv == NULL ? true : false); + if (!subreq) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed resolver request.\n"); + ret = EIO; + goto done; + } + tevent_req_set_callback(subreq, krb5_auth_resolve_done, req); + + return req; + +done: + if (ret == EOK) { + tevent_req_done(req); + } else { + tevent_req_error(req, ret); + } + tevent_req_post(req, state->ev); + return req; +} + +static void krb5_auth_resolve_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, struct tevent_req); + struct krb5_auth_state *state = tevent_req_data(req, struct krb5_auth_state); + struct krb5child_req *kr = state->kr; + int ret; + + if (!state->search_kpasswd) { + ret = be_resolve_server_recv(subreq, kr, &kr->srv); + } else { + ret = be_resolve_server_recv(subreq, kr, &kr->kpasswd_srv); + } + talloc_zfree(subreq); + + if (state->search_kpasswd) { + if ((ret != EOK) && + (kr->pd->cmd == SSS_PAM_CHAUTHTOK || + kr->pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM)) { + /* all kpasswd servers have been tried and none was found good, + * but the kdc seems ok. Password changes are not possible but + * authentication is. We return an PAM error here, but do not + * mark the backend offline. */ + state->pam_status = PAM_AUTHTOK_LOCK_BUSY; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + } + } else { + if (ret != EOK) { + /* all servers have been tried and none + * was found good, setting offline, + * but we still have to call the child to setup + * the ccache file if we are performing auth */ + be_mark_dom_offline(state->domain, state->be_ctx); + kr->is_offline = true; + + if (kr->pd->cmd == SSS_PAM_CHAUTHTOK || + kr->pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM) { + DEBUG(SSSDBG_TRACE_FUNC, + "No KDC suitable for password change is available\n"); + state->pam_status = PAM_AUTHTOK_LOCK_BUSY; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + } + } else { + if (kr->krb5_ctx->kpasswd_service != NULL) { + state->search_kpasswd = true; + subreq = be_resolve_server_send(state, + state->ev, state->be_ctx, + state->krb5_ctx->kpasswd_service->name, + kr->kpasswd_srv == NULL ? true : false); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Resolver request failed.\n"); + ret = EIO; + goto done; + } + tevent_req_set_callback(subreq, krb5_auth_resolve_done, req); + return; + } + } + } + + if (!kr->is_offline) { + kr->is_offline = be_is_offline(state->be_ctx); + } + + if (!kr->is_offline + && sss_domain_get_state(state->domain) == DOM_INACTIVE) { + DEBUG(SSSDBG_TRACE_INTERNAL, + "Subdomain %s is inactive, will proceed offline\n", + state->domain->name); + kr->is_offline = true; + } + + if (kr->is_offline + && sss_krb5_realm_has_proxy(dp_opt_get_cstring(kr->krb5_ctx->opts, + KRB5_REALM))) { + DEBUG(SSSDBG_TRACE_FUNC, + "Resetting offline status, KDC proxy is in use\n"); + kr->is_offline = false; + } + + subreq = handle_child_send(state, state->ev, kr); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "handle_child_send failed.\n"); + ret = ENOMEM; + goto done; + } + tevent_req_set_callback(subreq, krb5_auth_done, req); + return; + +done: + if (ret == EOK) { + tevent_req_done(req); + } else { + tevent_req_error(req, ret); + } +} + +static void krb5_auth_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, struct tevent_req); + struct krb5_auth_state *state = tevent_req_data(req, struct krb5_auth_state); + struct krb5child_req *kr = state->kr; + struct pam_data *pd = state->pd; + int ret; + uint8_t *buf = NULL; + ssize_t len = -1; + struct krb5_child_response *res; + struct fo_server *search_srv; + krb5_deltat renew_interval_delta; + char *renew_interval_str; + time_t renew_interval_time = 0; + bool use_enterprise_principal; + bool canonicalize; + + ret = handle_child_recv(subreq, pd, &buf, &len); + talloc_zfree(subreq); + if (ret == ETIMEDOUT) { + + DEBUG(SSSDBG_CRIT_FAILURE, "child timed out!\n"); + + switch (pd->cmd) { + case SSS_PAM_AUTHENTICATE: + case SSS_CMD_RENEW: + state->search_kpasswd = false; + search_srv = kr->srv; + break; + case SSS_PAM_CHAUTHTOK: + case SSS_PAM_CHAUTHTOK_PRELIM: + if (state->kr->kpasswd_srv) { + state->search_kpasswd = true; + search_srv = kr->kpasswd_srv; + break; + } else { + state->search_kpasswd = false; + search_srv = kr->srv; + break; + } + case SSS_PAM_PREAUTH: + state->pam_status = PAM_CRED_UNAVAIL; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + default: + DEBUG(SSSDBG_CRIT_FAILURE, "Unexpected PAM task %d\n", pd->cmd); + ret = EINVAL; + goto done; + } + + be_fo_set_port_status(state->be_ctx, state->krb5_ctx->service->name, + search_srv, PORT_NOT_WORKING); + subreq = be_resolve_server_send(state, state->ev, state->be_ctx, + state->krb5_ctx->service->name, + search_srv == NULL ? true : false); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed resolved request.\n"); + ret = ENOMEM; + goto done; + } + tevent_req_set_callback(subreq, krb5_auth_resolve_done, req); + return; + + } else if (ret != EOK) { + + DEBUG(SSSDBG_CRIT_FAILURE, + "child failed (%d [%s])\n", ret, strerror(ret)); + goto done; + } + + /* EOK */ + + ret = parse_krb5_child_response(state, buf, len, pd, + state->be_ctx->domain->pwd_expiration_warning, + &res); + if (ret) { + DEBUG(SSSDBG_IMPORTANT_INFO, + "The krb5_child process returned an error. Please inspect the " + "krb5_child.log file or the journal for more information\n"); + DEBUG(SSSDBG_OP_FAILURE, "Could not parse child response [%d]: %s\n", + ret, strerror(ret)); + goto done; + } + + if (res->ccname) { + kr->ccname = talloc_strdup(kr, res->ccname); + if (!kr->ccname) { + ret = ENOMEM; + goto done; + } + } + + use_enterprise_principal = dp_opt_get_bool(kr->krb5_ctx->opts, + KRB5_USE_ENTERPRISE_PRINCIPAL); + canonicalize = dp_opt_get_bool(kr->krb5_ctx->opts, KRB5_CANONICALIZE); + + /* Check if the cases of our upn are correct and update it if needed. + * Fail if the upn differs by more than just the case for non-enterprise + * principals. */ + if (res->correct_upn != NULL && + strcmp(kr->upn, res->correct_upn) != 0) { + if (strcasecmp(kr->upn, res->correct_upn) == 0 || + canonicalize == true || + use_enterprise_principal == true) { + talloc_free(kr->upn); + kr->upn = talloc_strdup(kr, res->correct_upn); + if (kr->upn == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + ret = ENOMEM; + goto done; + } + + ret = check_if_cached_upn_needs_update(state->sysdb, state->domain, + pd->user, res->correct_upn); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "check_if_cached_upn_needs_update failed.\n"); + goto done; + } + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "UPN used in the request [%s] and " \ + "returned UPN [%s] differ by more " \ + "than just the case.\n", + kr->upn, res->correct_upn); + ret = EINVAL; + goto done; + } + } + + /* If the child request failed, but did not return an offline error code, + * return with the status */ + switch (res->msg_status) { + case ERR_OK: + /* If the child request was successful and we run the first pass of the + * change password request just return success. */ + if (pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM) { + state->pam_status = PAM_SUCCESS; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + } + break; + + case ERR_NETWORK_IO: + if (kr->kpasswd_srv != NULL && + (pd->cmd == SSS_PAM_CHAUTHTOK || + pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM)) { + /* if using a dedicated kpasswd server for a chpass operation... */ + + be_fo_set_port_status(state->be_ctx, + state->krb5_ctx->kpasswd_service->name, + kr->kpasswd_srv, PORT_NOT_WORKING); + /* ..try to resolve next kpasswd server */ + state->search_kpasswd = true; + subreq = be_resolve_server_send(state, state->ev, state->be_ctx, + state->krb5_ctx->kpasswd_service->name, + state->kr->kpasswd_srv == NULL ? true : false); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Resolver request failed.\n"); + ret = ENOMEM; + goto done; + } + tevent_req_set_callback(subreq, krb5_auth_resolve_done, req); + return; + } else if (kr->srv != NULL) { + /* failed to use the KDC... */ + be_fo_set_port_status(state->be_ctx, + state->krb5_ctx->service->name, + kr->srv, PORT_NOT_WORKING); + /* ..try to resolve next KDC */ + state->search_kpasswd = false; + subreq = be_resolve_server_send(state, state->ev, state->be_ctx, + state->krb5_ctx->service->name, + kr->srv == NULL ? true : false); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Resolver request failed.\n"); + ret = ENOMEM; + goto done; + } + tevent_req_set_callback(subreq, krb5_auth_resolve_done, req); + return; + } + break; + + case ERR_CREDS_EXPIRED_CCACHE: + ret = krb5_delete_ccname(state, state->sysdb, state->domain, + pd->user, kr->old_ccname); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_delete_ccname failed.\n"); + } + /* FALLTHROUGH */ + SSS_ATTRIBUTE_FALLTHROUGH; + + case ERR_CREDS_EXPIRED: + /* If the password is expired we can safely remove the ccache from the + * cache and disk if it is not actively used anymore. This will allow + * to create a new random ccache if sshd with privilege separation is + * used. */ + if (pd->cmd == SSS_PAM_AUTHENTICATE && !kr->active_ccache) { + if (kr->old_ccname != NULL) { + ret = krb5_delete_ccname(state, state->sysdb, state->domain, + pd->user, kr->old_ccname); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_delete_ccname failed.\n"); + } + } + } + + state->pam_status = PAM_NEW_AUTHTOK_REQD; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + + case ERR_CREDS_INVALID: + state->pam_status = PAM_CRED_ERR; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + + case ERR_ACCOUNT_EXPIRED: + state->pam_status = PAM_ACCT_EXPIRED; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + + case ERR_ACCOUNT_LOCKED: + state->pam_status = PAM_PERM_DENIED; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + + case ERR_NO_CREDS: + state->pam_status = PAM_CRED_UNAVAIL; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + + case ERR_AUTH_FAILED: + state->pam_status = PAM_AUTH_ERR; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + + case ERR_CHPASS_FAILED: + state->pam_status = PAM_AUTHTOK_ERR; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + + case ERR_NO_AUTH_METHOD_AVAILABLE: + state->pam_status = PAM_NO_MODULE_DATA; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + + default: + DEBUG(SSSDBG_IMPORTANT_INFO, + "The krb5_child process returned an error. Please inspect the " + "krb5_child.log file or the journal for more information\n"); + state->pam_status = PAM_SYSTEM_ERR; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + } + + if (kr->kpasswd_srv != NULL && + (pd->cmd == SSS_PAM_CHAUTHTOK || + pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM)) { + /* found a dedicated kpasswd server for a chpass operation */ + be_fo_set_port_status(state->be_ctx, + state->krb5_ctx->service->name, + kr->kpasswd_srv, PORT_WORKING); + } else if (kr->srv != NULL) { + /* found a KDC */ + be_fo_set_port_status(state->be_ctx, state->krb5_ctx->service->name, + kr->srv, PORT_WORKING); + } + + if (pd->cmd == SSS_PAM_PREAUTH) { + state->pam_status = PAM_SUCCESS; + state->dp_err = DP_ERR_OK; + ret = EOK; + goto done; + } + + /* Now only a successful authentication or password change is left. + * + * We expect that one of the messages in the received buffer contains + * the name of the credential cache file. */ + if (kr->ccname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing ccache name in child response.\n"); + ret = EINVAL; + goto done; + } + + ret = krb5_save_ccname(state, state->sysdb, state->domain, + pd->user, kr->ccname); + if (ret) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_save_ccname failed.\n"); + goto done; + } + renew_interval_str = dp_opt_get_string(kr->krb5_ctx->opts, + KRB5_RENEW_INTERVAL); + if (renew_interval_str != NULL) { + ret = krb5_string_to_deltat(renew_interval_str, &renew_interval_delta); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Reading krb5_renew_interval failed.\n"); + renew_interval_delta = 0; + } + renew_interval_time = renew_interval_delta; + } + if (res->msg_status == ERR_OK && renew_interval_time > 0 && + (pd->cmd == SSS_PAM_AUTHENTICATE || + pd->cmd == SSS_CMD_RENEW || + pd->cmd == SSS_PAM_CHAUTHTOK) && + (res->tgtt.renew_till > res->tgtt.endtime) && + (kr->ccname != NULL)) { + DEBUG(SSSDBG_TRACE_LIBS, + "Adding [%s] for automatic renewal.\n", kr->ccname); + ret = add_tgt_to_renew_table(kr->krb5_ctx, kr->ccname, &(res->tgtt), + pd, kr->upn); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "add_tgt_to_renew_table failed, " + "automatic renewal not possible.\n"); + } + } + + if (kr->is_offline) { + if (dp_opt_get_bool(kr->krb5_ctx->opts, + KRB5_STORE_PASSWORD_IF_OFFLINE) + && sss_authtok_get_type(pd->authtok) + == SSS_AUTHTOK_TYPE_PASSWORD) { + krb5_auth_cache_creds(state->kr->krb5_ctx, + state->domain, + state->be_ctx->cdb, + state->pd, state->kr->uid, + &state->pam_status, &state->dp_err); + } else { + DEBUG(SSSDBG_CONF_SETTINGS, + "Backend is marked offline, retry later!\n"); + state->pam_status = PAM_AUTHINFO_UNAVAIL; + state->dp_err = DP_ERR_OFFLINE; + } + ret = EOK; + goto done; + } + + if (state->be_ctx->domain->cache_credentials == TRUE + && (!res->otp + || (res->otp && sss_authtok_get_type(pd->authtok) == + SSS_AUTHTOK_TYPE_2FA))) { + krb5_auth_store_creds(state->domain, pd); + } + + /* The SSS_OTP message will prevent pam_sss from putting the entered + * password on the PAM stack for other modules to use. This is not needed + * when both factors were entered separately because here the first factor + * (long term password) can be passed to the other modules. */ + if (res->otp == true && pd->cmd == SSS_PAM_AUTHENTICATE + && sss_authtok_get_type(pd->authtok) != SSS_AUTHTOK_TYPE_2FA) { + uint32_t otp_flag = 1; + ret = pam_add_response(pd, SSS_OTP, sizeof(uint32_t), + (const uint8_t *) &otp_flag); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "pam_add_response failed: %d (%s).\n", + ret, sss_strerror(ret)); + state->pam_status = PAM_SYSTEM_ERR; + state->dp_err = DP_ERR_OK; + goto done; + } + } + + state->pam_status = PAM_SUCCESS; + state->dp_err = DP_ERR_OK; + ret = EOK; + +done: + if (ret == EOK) { + tevent_req_done(req); + } else { + tevent_req_error(req, ret); + } + +} + +int krb5_auth_recv(struct tevent_req *req, int *pam_status, int *dp_err) +{ + struct krb5_auth_state *state = tevent_req_data(req, struct krb5_auth_state); + *pam_status = state->pam_status; + *dp_err = state->dp_err; + + TEVENT_REQ_RETURN_ON_ERROR(req); + + return EOK; +} + +struct krb5_pam_handler_state { + struct pam_data *pd; +}; + +static void krb5_pam_handler_auth_done(struct tevent_req *subreq); +static void krb5_pam_handler_access_done(struct tevent_req *subreq); + +struct tevent_req * +krb5_pam_handler_send(TALLOC_CTX *mem_ctx, + struct krb5_ctx *krb5_ctx, + struct pam_data *pd, + struct dp_req_params *params) +{ + struct krb5_pam_handler_state *state; + struct tevent_req *subreq; + struct tevent_req *req; + + req = tevent_req_create(mem_ctx, &state, + struct krb5_pam_handler_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + state->pd = pd; + + switch (pd->cmd) { + case SSS_PAM_AUTHENTICATE: + case SSS_PAM_PREAUTH: + case SSS_CMD_RENEW: + case SSS_PAM_CHAUTHTOK_PRELIM: + case SSS_PAM_CHAUTHTOK: + subreq = krb5_auth_queue_send(state, params->ev, params->be_ctx, + pd, krb5_ctx); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_auth_send failed.\n"); + pd->pam_status = PAM_SYSTEM_ERR; + goto immediately; + } + + tevent_req_set_callback(subreq, krb5_pam_handler_auth_done, req); + break; + case SSS_PAM_ACCT_MGMT: + subreq = krb5_access_send(state, params->ev, params->be_ctx, + pd, krb5_ctx); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_access_send failed.\n"); + pd->pam_status = PAM_SYSTEM_ERR; + goto immediately; + } + + tevent_req_set_callback(subreq, krb5_pam_handler_access_done, req); + break; + case SSS_PAM_SETCRED: + case SSS_PAM_OPEN_SESSION: + case SSS_PAM_CLOSE_SESSION: + pd->pam_status = PAM_SUCCESS; + goto immediately; + break; + default: + DEBUG(SSSDBG_CONF_SETTINGS, + "krb5 does not handles pam task %d.\n", pd->cmd); + pd->pam_status = PAM_MODULE_UNKNOWN; + goto immediately; + } + + return req; + +immediately: + /* TODO For backward compatibility we always return EOK to DP now. */ + tevent_req_done(req); + tevent_req_post(req, params->ev); + + return req; +} + +static void krb5_pam_handler_auth_done(struct tevent_req *subreq) +{ + struct krb5_pam_handler_state *state; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct krb5_pam_handler_state); + + ret = krb5_auth_queue_recv(subreq, &state->pd->pam_status, NULL); + talloc_zfree(subreq); + if (ret != EOK) { + state->pd->pam_status = PAM_SYSTEM_ERR; + } + + /* PAM_CRED_ERR is used to indicate to the IPA provider that trying + * password migration would make sense. From this point on it isn't + * necessary to keep this status, so it can be translated to PAM_AUTH_ERR. + */ + if (state->pd->pam_status == PAM_CRED_ERR) { + state->pd->pam_status = PAM_AUTH_ERR; + } + + /* TODO For backward compatibility we always return EOK to DP now. */ + tevent_req_done(req); +} + +static void krb5_pam_handler_access_done(struct tevent_req *subreq) +{ + struct krb5_pam_handler_state *state; + struct tevent_req *req; + bool access_allowed; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct krb5_pam_handler_state); + + ret = krb5_access_recv(subreq, &access_allowed); + talloc_zfree(subreq); + if (ret != EOK) { + state->pd->pam_status = PAM_SYSTEM_ERR; + } + + + DEBUG(SSSDBG_TRACE_LIBS, "Access %s for user [%s].\n", + access_allowed ? "allowed" : "denied", state->pd->user); + state->pd->pam_status = access_allowed ? PAM_SUCCESS : PAM_PERM_DENIED; + + /* TODO For backward compatibility we always return EOK to DP now. */ + tevent_req_done(req); +} + +errno_t +krb5_pam_handler_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct pam_data **_data) +{ + struct krb5_pam_handler_state *state = NULL; + + state = tevent_req_data(req, struct krb5_pam_handler_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + *_data = talloc_steal(mem_ctx, state->pd); + + return EOK; +} diff --git a/src/providers/krb5/krb5_auth.h b/src/providers/krb5/krb5_auth.h new file mode 100644 index 0000000..bbdbf61 --- /dev/null +++ b/src/providers/krb5/krb5_auth.h @@ -0,0 +1,158 @@ +/* + SSSD + + Kerberos Backend, private header file + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2009 Red Hat + + + 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 <http://www.gnu.org/licenses/>. +*/ + +#ifndef __KRB5_AUTH_H__ +#define __KRB5_AUTH_H__ + + +#include "util/sss_regexp.h" +#include "util/sss_krb5.h" +#include "providers/backend.h" +#include "util/child_common.h" +#include "providers/krb5/krb5_common.h" +#include "providers/krb5/krb5_ccache.h" + +#define CCACHE_ENV_NAME "KRB5CCNAME" + +#define ILLEGAL_PATH_PATTERN "//|/\\./|/\\.\\./" + +#define CHILD_OPT_FAST_CCACHE_UID "fast-ccache-uid" +#define CHILD_OPT_FAST_CCACHE_GID "fast-ccache-gid" +#define CHILD_OPT_FAST_USE_ANONYMOUS_PKINIT "fast-use-anonymous-pkinit" +#define CHILD_OPT_REALM "realm" +#define CHILD_OPT_LIFETIME "lifetime" +#define CHILD_OPT_RENEWABLE_LIFETIME "renewable-lifetime" +#define CHILD_OPT_USE_FAST "use-fast" +#define CHILD_OPT_FAST_PRINCIPAL "fast-principal" +#define CHILD_OPT_CANONICALIZE "canonicalize" +#define CHILD_OPT_SSS_CREDS_PASSWORD "sss-creds-password" +#define CHILD_OPT_CHAIN_ID "chain-id" +#define CHILD_OPT_CHECK_PAC "check-pac" + +struct krb5child_req { + struct pam_data *pd; + struct krb5_ctx *krb5_ctx; + struct sss_domain_info *dom; + + const char *ccname; + const char *old_ccname; + const char *homedir; + char *upn; + uid_t uid; + gid_t gid; + bool is_offline; + struct fo_server *srv; + struct fo_server *kpasswd_srv; + bool active_ccache; + bool valid_tgt; + bool upn_from_different_realm; + bool send_pac; + + const char *user; + const char *kuserok_user; +}; + +errno_t krb5_setup(TALLOC_CTX *mem_ctx, + struct pam_data *pd, + struct sss_domain_info *dom, + struct krb5_ctx *krb5_ctx, + struct krb5child_req **_krb5_req); + +struct tevent_req * +krb5_pam_handler_send(TALLOC_CTX *mem_ctx, + struct krb5_ctx *krb5_ctx, + struct pam_data *pd, + struct dp_req_params *params); + +errno_t +krb5_pam_handler_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct pam_data **_data); + +/* Please use krb5_auth_send/recv *only* if you're certain there can't + * be concurrent logins happening. With some ccache back ends, the ccache + * files might clobber one another. Please use krb5_auth_queue_send() + * instead that queues the requests + */ +struct tevent_req *krb5_auth_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct be_ctx *be_ctx, + struct pam_data *pd, + struct krb5_ctx *krb5_ctx); +int krb5_auth_recv(struct tevent_req *req, int *pam_status, int *dp_err); + +struct tevent_req *handle_child_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct krb5child_req *kr); +int handle_child_recv(struct tevent_req *req, TALLOC_CTX *mem_ctx, + uint8_t **buf, ssize_t *len); + +struct krb5_child_response { + int32_t msg_status; + struct tgt_times tgtt; + char *ccname; + char *correct_upn; + bool otp; +}; + +errno_t +parse_krb5_child_response(TALLOC_CTX *mem_ctx, uint8_t *buf, ssize_t len, + struct pam_data *pd, int pwd_exp_warning, + struct krb5_child_response **_res); + +errno_t add_user_to_delayed_online_authentication(struct krb5_ctx *krb5_ctx, + struct sss_domain_info *domain, + struct pam_data *pd, + uid_t uid); +errno_t init_delayed_online_authentication(struct krb5_ctx *krb5_ctx, + struct be_ctx *be_ctx, + struct tevent_context *ev); + +errno_t init_renew_tgt(struct krb5_ctx *krb5_ctx, struct be_ctx *be_ctx, + struct tevent_context *ev, time_t renew_intv); +errno_t add_tgt_to_renew_table(struct krb5_ctx *krb5_ctx, const char *ccfile, + struct tgt_times *tgtt, struct pam_data *pd, + const char *upn); + +/* krb5_access.c */ +struct tevent_req *krb5_access_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct be_ctx *be_ctx, + struct pam_data *pd, + struct krb5_ctx *krb5_ctx); +int krb5_access_recv(struct tevent_req *req, bool *access_allowed); + +/* krb5_wait_queue.c */ +struct tevent_req *krb5_auth_queue_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct be_ctx *be_ctx, + struct pam_data *pd, + struct krb5_ctx *krb5_ctx); + +int krb5_auth_queue_recv(struct tevent_req *req, + int *_pam_status, + int *_dp_err); + +#endif /* __KRB5_AUTH_H__ */ diff --git a/src/providers/krb5/krb5_ccache.c b/src/providers/krb5/krb5_ccache.c new file mode 100644 index 0000000..88f75a8 --- /dev/null +++ b/src/providers/krb5/krb5_ccache.c @@ -0,0 +1,796 @@ +/* + SSSD + + Kerberos 5 Backend Module -- ccache related utilities + + Authors: + Sumit Bose <sbose@redhat.com> + Jakub Hrozek <jhrozek@redhat.com> + + Copyright (C) 2014 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#ifdef HAVE_KRB5_KRB5_H +#include <krb5/krb5.h> +#else +#include <krb5.h> +#endif + +#include "providers/krb5/krb5_ccache.h" +#include "util/sss_krb5.h" +#include "util/util.h" + +struct string_list { + struct string_list *next; + struct string_list *prev; + char *s; +}; + +static errno_t find_ccdir_parent_data(TALLOC_CTX *mem_ctx, + const char *ccdirname, + struct stat *parent_stat, + struct string_list **missing_parents) +{ + int ret = EFAULT; + char *parent = NULL; + char *end; + struct string_list *li; + + ret = stat(ccdirname, parent_stat); + if (ret == EOK) { + if ( !S_ISDIR(parent_stat->st_mode) ) { + DEBUG(SSSDBG_MINOR_FAILURE, + "[%s] is not a directory.\n", ccdirname); + return EINVAL; + } + return EOK; + } else { + if (errno != ENOENT) { + ret = errno; + DEBUG(SSSDBG_MINOR_FAILURE, + "stat for [%s] failed: [%d][%s].\n", ccdirname, ret, + strerror(ret)); + return ret; + } + } + + li = talloc_zero(mem_ctx, struct string_list); + if (li == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "talloc_zero failed.\n"); + return ENOMEM; + } + + li->s = talloc_strdup(li, ccdirname); + if (li->s == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "talloc_strdup failed.\n"); + return ENOMEM; + } + + DLIST_ADD(*missing_parents, li); + + parent = talloc_strdup(mem_ctx, ccdirname); + if (parent == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "talloc_strdup failed.\n"); + return ENOMEM; + } + + /* We'll remove all trailing slashes from the back so that + * we only pass /some/path to find_ccdir_parent_data, not + * /some/path */ + do { + end = strrchr(parent, '/'); + if (end == NULL || end == parent) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Cannot find parent directory of [%s], / is not allowed.\n", + ccdirname); + ret = EINVAL; + goto done; + } + *end = '\0'; + } while (*(end+1) == '\0'); + + ret = find_ccdir_parent_data(mem_ctx, parent, parent_stat, missing_parents); + +done: + talloc_free(parent); + return ret; +} + +static errno_t check_parent_stat(struct stat *parent_stat, uid_t uid) +{ + if (parent_stat->st_uid != 0 && parent_stat->st_uid != uid) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Private directory can only be created below a directory " + "belonging to root or to [%"SPRIuid"].\n", uid); + return EINVAL; + } + + if (parent_stat->st_uid == uid) { + if (!(parent_stat->st_mode & S_IXUSR)) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Parent directory does not have the search bit set for " + "the owner.\n"); + return EINVAL; + } + } else { + if (!(parent_stat->st_mode & S_IXOTH)) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Parent directory does not have the search bit set for " + "others.\n"); + return EINVAL; + } + } + + return EOK; +} + +static errno_t create_ccache_dir(const char *ccdirname, uid_t uid, gid_t gid) +{ + int ret = EFAULT; + struct stat parent_stat; + struct string_list *missing_parents = NULL; + struct string_list *li = NULL; + mode_t old_umask; + mode_t new_dir_mode; + TALLOC_CTX *tmp_ctx = NULL; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "talloc_new failed.\n"); + return ENOMEM; + } + + if (*ccdirname != '/') { + DEBUG(SSSDBG_MINOR_FAILURE, + "Only absolute paths are allowed, not [%s] .\n", ccdirname); + ret = EINVAL; + goto done; + } + + ret = find_ccdir_parent_data(tmp_ctx, ccdirname, &parent_stat, + &missing_parents); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, + "find_ccdir_parent_data failed.\n"); + goto done; + } + + ret = check_parent_stat(&parent_stat, uid); + if (ret != EOK) { + DEBUG(SSSDBG_FATAL_FAILURE, + "Check the ownership and permissions of krb5_ccachedir: [%s].\n", + ccdirname); + goto done; + } + + DLIST_FOR_EACH(li, missing_parents) { + DEBUG(SSSDBG_TRACE_INTERNAL, + "Creating directory [%s].\n", li->s); + new_dir_mode = 0700; + + old_umask = umask(0000); + ret = mkdir(li->s, new_dir_mode); + umask(old_umask); + if (ret != EOK) { + ret = errno; + DEBUG(SSSDBG_MINOR_FAILURE, + "mkdir [%s] failed: [%d][%s].\n", li->s, ret, + strerror(ret)); + goto done; + } + ret = chown(li->s, uid, gid); + if (ret != EOK) { + ret = errno; + DEBUG(SSSDBG_MINOR_FAILURE, + "chown failed [%d][%s].\n", ret, strerror(ret)); + goto done; + } + } + + ret = EOK; + +done: + talloc_free(tmp_ctx); + return ret; +} + +errno_t sss_krb5_precreate_ccache(const char *ccname, uid_t uid, gid_t gid) +{ + TALLOC_CTX *tmp_ctx = NULL; + const char *filename; + char *ccdirname; + char *end; + errno_t ret; + + if (ccname[0] == '/') { + filename = ccname; + } else if (strncmp(ccname, "FILE:", 5) == 0) { + filename = ccname + 5; + } else if (strncmp(ccname, "DIR:", 4) == 0) { + filename = ccname + 4; + } else { + /* only FILE and DIR types need precreation so far, we ignore any + * other type */ + return EOK; + } + + tmp_ctx = talloc_new(NULL); + if (!tmp_ctx) return ENOMEM; + + ccdirname = talloc_strdup(tmp_ctx, filename); + if (ccdirname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n"); + ret = ENOMEM; + goto done; + } + + /* We'll remove all trailing slashes from the back so that + * we only pass /some/path to find_ccdir_parent_data, not + * /some/path/ */ + do { + end = strrchr(ccdirname, '/'); + if (end == NULL || end == ccdirname) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot find parent directory of [%s], " + "/ is not allowed.\n", ccdirname); + ret = EINVAL; + goto done; + } + *end = '\0'; + } while (*(end+1) == '\0'); + + ret = create_ccache_dir(ccdirname, uid, gid); +done: + talloc_free(tmp_ctx); + return ret; +} + +struct sss_krb5_ccache { + struct sss_creds *creds; + krb5_context context; + krb5_ccache ccache; +}; + +static int sss_free_krb5_ccache(void *mem) +{ + struct sss_krb5_ccache *cc = talloc_get_type(mem, struct sss_krb5_ccache); + + if (cc->ccache) { + krb5_cc_close(cc->context, cc->ccache); + } + krb5_free_context(cc->context); + restore_creds(cc->creds); + return 0; +} + +static errno_t sss_open_ccache_as_user(TALLOC_CTX *mem_ctx, + const char *ccname, + uid_t uid, gid_t gid, + struct sss_krb5_ccache **ccache) +{ + struct sss_krb5_ccache *cc; + krb5_error_code kerr; + errno_t ret; + + cc = talloc_zero(mem_ctx, struct sss_krb5_ccache); + if (!cc) { + return ENOMEM; + } + talloc_set_destructor((TALLOC_CTX *)cc, sss_free_krb5_ccache); + + ret = switch_creds(cc, uid, gid, 0, NULL, &cc->creds); + if (ret) { + goto done; + } + + kerr = sss_krb5_init_context(&cc->context); + if (kerr) { + ret = EIO; + goto done; + } + + kerr = krb5_cc_resolve(cc->context, ccname, &cc->ccache); + if (kerr == KRB5_FCC_NOFILE || cc->ccache == NULL) { + DEBUG(SSSDBG_TRACE_FUNC, "ccache %s is missing or empty\n", ccname); + ret = ERR_NOT_FOUND; + goto done; + } else if (kerr != 0) { + KRB5_DEBUG(SSSDBG_OP_FAILURE, cc->context, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_cc_resolve failed.\n"); + ret = ERR_INTERNAL; + goto done; + } + + ret = EOK; + +done: + if (ret) { + talloc_free(cc); + } else { + *ccache = cc; + } + return ret; +} + +static errno_t sss_destroy_ccache(struct sss_krb5_ccache *cc) +{ + krb5_error_code kerr; + errno_t ret; + + kerr = krb5_cc_destroy(cc->context, cc->ccache); + if (kerr) { + KRB5_DEBUG(SSSDBG_OP_FAILURE, cc->context, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_cc_destroy failed.\n"); + ret = EIO; + } else { + ret = EOK; + } + + /* krb5_cc_destroy frees cc->ccache in all events */ + cc->ccache = NULL; + + return ret; +} + +errno_t sss_krb5_cc_destroy(const char *ccname, uid_t uid, gid_t gid) +{ + struct sss_krb5_ccache *cc = NULL; + TALLOC_CTX *tmp_ctx; + errno_t ret; + + if (ccname == NULL) { + /* nothing to remove */ + return EOK; + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_new failed.\n"); + return ENOMEM; + } + + ret = sss_open_ccache_as_user(tmp_ctx, ccname, uid, gid, &cc); + if (ret) { + goto done; + } + + ret = sss_destroy_ccache(cc); + +done: + talloc_free(tmp_ctx); + return ret; +} + +/* This function is called only as a way to validate that we have the + * right cache */ +errno_t sss_krb5_check_ccache_princ(krb5_context kctx, + const char *ccname, + krb5_principal user_princ) +{ + krb5_ccache kcc = NULL; + krb5_principal ccprinc = NULL; + krb5_error_code kerr; + const char *cc_type; + errno_t ret; + + kerr = krb5_cc_resolve(kctx, ccname, &kcc); + if (kerr) { + ret = ERR_INTERNAL; + goto done; + } + + cc_type = krb5_cc_get_type(kctx, kcc); + + kerr = krb5_cc_get_principal(kctx, kcc, &ccprinc); + if (kerr != 0) { + KRB5_DEBUG(SSSDBG_OP_FAILURE, kctx, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_cc_get_principal failed.\n"); + } + + if (ccprinc) { + if (krb5_principal_compare(kctx, user_princ, ccprinc) == TRUE) { + /* found in the primary ccache */ + ret = EOK; + goto done; + } + } + +#ifdef HAVE_KRB5_CC_COLLECTION + + if (krb5_cc_support_switch(kctx, cc_type)) { + + krb5_cc_close(kctx, kcc); + kcc = NULL; + + kerr = krb5_cc_set_default_name(kctx, ccname); + if (kerr != 0) { + KRB5_DEBUG(SSSDBG_MINOR_FAILURE, kctx, kerr); + /* try to continue despite failure */ + } + + kerr = krb5_cc_cache_match(kctx, user_princ, &kcc); + if (kerr == 0) { + ret = EOK; + goto done; + } + KRB5_DEBUG(SSSDBG_TRACE_INTERNAL, kctx, kerr); + } + +#endif /* HAVE_KRB5_CC_COLLECTION */ + + ret = ERR_NOT_FOUND; + +done: + if (ccprinc) { + krb5_free_principal(kctx, ccprinc); + } + if (kcc) { + krb5_cc_close(kctx, kcc); + } + return ret; +} + +static errno_t sss_low_level_path_check(const char *ccname) +{ + const char *filename; + struct stat buf; + int ret; + + if (ccname[0] == '/') { + filename = ccname; + } else if (strncmp(ccname, "FILE:", 5) == 0) { + filename = ccname + 5; + } else if (strncmp(ccname, "DIR:", 4) == 0) { + filename = ccname + 4; + if (filename[0] == ':') filename += 1; + } else { + /* only FILE and DIR types need file checks so far, we ignore any + * other type */ + return EOK; + } + + ret = stat(filename, &buf); + if (ret == -1) return errno; + return EOK; +} + +errno_t sss_krb5_cc_verify_ccache(const char *ccname, uid_t uid, gid_t gid, + const char *realm, const char *principal) +{ + struct sss_krb5_ccache *cc = NULL; + TALLOC_CTX *tmp_ctx = NULL; + krb5_principal tgt_princ = NULL; + krb5_principal princ = NULL; + char *tgt_name; + krb5_creds mcred = { 0 }; + krb5_creds cred = { 0 }; + krb5_error_code kerr; + errno_t ret; + + /* first of all verify if the old ccache file/dir exists as we may be + * trying to verify if an old ccache exists at all. If no file/dir + * exists bail out immediately otherwise a following krb5_cc_resolve() + * call may actually create paths and files we do not want to have + * around */ + ret = sss_low_level_path_check(ccname); + if (ret) { + return ret; + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed.\n"); + return ENOMEM; + } + + ret = sss_open_ccache_as_user(tmp_ctx, ccname, uid, gid, &cc); + if (ret) { + goto done; + } + + tgt_name = talloc_asprintf(tmp_ctx, "krbtgt/%s@%s", realm, realm); + if (!tgt_name) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed.\n"); + ret = ENOMEM; + goto done; + } + + kerr = krb5_parse_name(cc->context, tgt_name, &tgt_princ); + if (kerr) { + KRB5_DEBUG(SSSDBG_CRIT_FAILURE, cc->context, kerr); + if (kerr == KRB5_PARSE_MALFORMED) ret = EINVAL; + else ret = ERR_INTERNAL; + goto done; + } + + kerr = krb5_parse_name(cc->context, principal, &princ); + if (kerr) { + KRB5_DEBUG(SSSDBG_CRIT_FAILURE, cc->context, kerr); + if (kerr == KRB5_PARSE_MALFORMED) ret = EINVAL; + else ret = ERR_INTERNAL; + goto done; + } + + mcred.client = princ; + mcred.server = tgt_princ; + /* Type krb5_timestamp is a signed 32-bit integer, so we need to convert the + * 64-bit time_t value returned by time(). Just keeping the lower 32 bits + * should be enough as Kerberos seems to be planing on making this time + * unsigned to avoid the Y2K38 problem. + * Please check: + * https://web.mit.edu/kerberos/krb5-latest/doc/appdev/y2038.html + */ + mcred.times.endtime = time(NULL) & 0xFFFFFFFF; + + kerr = krb5_cc_retrieve_cred(cc->context, cc->ccache, + KRB5_TC_MATCH_TIMES, &mcred, &cred); + if (kerr) { + if (kerr == KRB5_CC_NOTFOUND || kerr == KRB5_FCC_NOFILE) { + DEBUG(SSSDBG_TRACE_INTERNAL, "TGT not found or expired.\n"); + ret = EINVAL; + } else { + KRB5_DEBUG(SSSDBG_CRIT_FAILURE, cc->context, kerr); + ret = ERR_INTERNAL; + } + } + krb5_free_cred_contents(cc->context, &cred); + +done: + if (tgt_princ) krb5_free_principal(cc->context, tgt_princ); + if (princ) krb5_free_principal(cc->context, princ); + talloc_free(tmp_ctx); + return ret; +} + +errno_t get_ccache_file_data(const char *ccache_file, const char *client_name, + struct tgt_times *tgtt) +{ + krb5_error_code kerr; + krb5_context ctx = NULL; + krb5_ccache cc = NULL; + krb5_principal client_princ = NULL; + krb5_principal server_princ = NULL; + char *server_name; + krb5_creds mcred; + krb5_creds cred; + const char *realm_name; + int realm_length; + + kerr = sss_krb5_init_context(&ctx); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_krb5_init_context failed.\n"); + goto done; + } + + kerr = krb5_parse_name(ctx, client_name, &client_princ); + if (kerr != 0) { + KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_parse_name failed.\n"); + goto done; + } + + sss_krb5_princ_realm(ctx, client_princ, &realm_name, &realm_length); + if (realm_length == 0) { + kerr = KRB5KRB_ERR_GENERIC; + DEBUG(SSSDBG_CRIT_FAILURE, "sss_krb5_princ_realm failed.\n"); + goto done; + } + + server_name = talloc_asprintf(NULL, "krbtgt/%.*s@%.*s", + realm_length, realm_name, + realm_length, realm_name); + if (server_name == NULL) { + kerr = KRB5_CC_NOMEM; + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + goto done; + } + + kerr = krb5_parse_name(ctx, server_name, &server_princ); + talloc_free(server_name); + if (kerr != 0) { + KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_parse_name failed.\n"); + goto done; + } + + kerr = krb5_cc_resolve(ctx, ccache_file, &cc); + if (kerr != 0) { + KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_cc_resolve failed.\n"); + goto done; + } + + memset(&mcred, 0, sizeof(mcred)); + memset(&cred, 0, sizeof(mcred)); + + mcred.server = server_princ; + mcred.client = client_princ; + + kerr = krb5_cc_retrieve_cred(ctx, cc, 0, &mcred, &cred); + if (kerr != 0) { + KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_cc_retrieve_cred failed.\n"); + goto done; + } + + tgtt->authtime = cred.times.authtime; + tgtt->starttime = cred.times.starttime; + tgtt->endtime = cred.times.endtime; + tgtt->renew_till = cred.times.renew_till; + + krb5_free_cred_contents(ctx, &cred); + + kerr = krb5_cc_close(ctx, cc); + cc = NULL; + if (kerr != 0) { + KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_cc_close failed.\n"); + goto done; + } + + kerr = 0; + +done: + if (cc != NULL) { + krb5_cc_close(ctx, cc); + } + + if (client_princ != NULL) { + krb5_free_principal(ctx, client_princ); + } + + if (server_princ != NULL) { + krb5_free_principal(ctx, server_princ); + } + + if (ctx != NULL) { + krb5_free_context(ctx); + } + + if (kerr != 0) { + return EIO; + } + + return EOK; +} + +errno_t safe_remove_old_ccache_file(const char *old_ccache, + const char *new_ccache, + uid_t uid, gid_t gid) +{ + if ((old_ccache == new_ccache) + || (old_ccache && new_ccache + && (strcmp(old_ccache, new_ccache) == 0))) { + DEBUG(SSSDBG_TRACE_FUNC, "New and old ccache file are the same, " + "none will be deleted.\n"); + return EOK; + } + + return sss_krb5_cc_destroy(old_ccache, uid, gid); +} + +krb5_error_code copy_ccache_into_memory(TALLOC_CTX *mem_ctx, krb5_context kctx, + const char *ccache_file, + char **_mem_name) +{ + krb5_error_code kerr; + krb5_ccache ccache; + krb5_ccache mem_ccache = NULL; + char *ccache_name = NULL; + krb5_principal princ = NULL; + char *mem_name = NULL; + char *sep; + + kerr = krb5_cc_resolve(kctx, ccache_file, &ccache); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "error resolving ccache [%s].\n", + ccache_file); + return kerr; + } + + kerr = krb5_cc_get_full_name(kctx, ccache, &ccache_name); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to read name for ccache [%s].\n", + ccache_file); + goto done; + } + + sep = strchr(ccache_name, ':'); + if (sep == NULL || sep[1] == '\0') { + DEBUG(SSSDBG_CRIT_FAILURE, + "Ccache name [%s] does not have delimiter[:] .\n", ccache_name); + kerr = KRB5KRB_ERR_GENERIC; + goto done; + } + + if (strncmp(ccache_name, "MEMORY:", sizeof("MEMORY:") -1) == 0) { + DEBUG(SSSDBG_TRACE_FUNC, "Ccache [%s] is already memory ccache.\n", + ccache_name); + *_mem_name = talloc_strdup(mem_ctx, ccache_name); + if(*_mem_name == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + kerr = KRB5KRB_ERR_GENERIC; + goto done; + } + kerr = 0; + goto done; + } + if (strncmp(ccache_name, "FILE:", sizeof("FILE:") -1) == 0) { + mem_name = talloc_asprintf(mem_ctx, "MEMORY:%s", sep + 1); + if (mem_name == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + kerr = KRB5KRB_ERR_GENERIC; + goto done; + } + } else { + DEBUG(SSSDBG_MINOR_FAILURE, "Unexpected ccache type for ccache [%s], " \ + "currently only FILE is supported.\n", + ccache_name); + kerr = KRB5KRB_ERR_GENERIC; + goto done; + } + + kerr = krb5_cc_resolve(kctx, mem_name, &mem_ccache); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "error resolving ccache [%s].\n", mem_name); + goto done; + } + + kerr = krb5_cc_get_principal(kctx, ccache, &princ); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "error reading principal from ccache [%s].\n", ccache_name); + goto done; + } + + kerr = krb5_cc_initialize(kctx, mem_ccache, princ); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to initialize ccache [%s].\n", mem_name); + goto done; + } + + kerr = krb5_cc_copy_creds(kctx, ccache, mem_ccache); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to copy ccache [%s] to [%s].\n", ccache_name, mem_name); + goto done; + } + + *_mem_name = mem_name; + kerr = 0; + +done: + if (kerr != 0) { + talloc_free(mem_name); + } + + krb5_free_string(kctx, ccache_name); + krb5_free_principal(kctx, princ); + + if (krb5_cc_close(kctx, ccache) != 0) { + DEBUG(SSSDBG_OP_FAILURE, "krb5_cc_close failed.\n"); + } + + if ((mem_ccache != NULL) && (krb5_cc_close(kctx, mem_ccache) != 0)) { + DEBUG(SSSDBG_OP_FAILURE, "krb5_cc_close failed.\n"); + } + + return kerr; +} diff --git a/src/providers/krb5/krb5_ccache.h b/src/providers/krb5/krb5_ccache.h new file mode 100644 index 0000000..f3928e6 --- /dev/null +++ b/src/providers/krb5/krb5_ccache.h @@ -0,0 +1,73 @@ +/* + SSSD + + Kerberos 5 Backend Module -- ccache related utilities + + Authors: + Sumit Bose <sbose@redhat.com> + Jakub Hrozek <jhrozek@redhat.com> + + Copyright (C) 2014 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#ifndef __KRB5_CCACHE_H__ +#define __KRB5_CCACHE_H__ + +#include "util/util.h" + +struct tgt_times { + time_t authtime; + time_t starttime; + time_t endtime; + time_t renew_till; +}; + +errno_t sss_krb5_precreate_ccache(const char *ccname, uid_t uid, gid_t gid); + +errno_t sss_krb5_cc_destroy(const char *ccname, uid_t uid, gid_t gid); + +errno_t sss_krb5_check_ccache_princ(krb5_context kctx, + const char *ccname, + krb5_principal user_princ); + +errno_t sss_krb5_cc_verify_ccache(const char *ccname, uid_t uid, gid_t gid, + const char *realm, const char *principal); + +errno_t get_ccache_file_data(const char *ccache_file, const char *client_name, + struct tgt_times *tgtt); + +errno_t safe_remove_old_ccache_file(const char *old_ccache, + const char *new_ccache, + uid_t uid, gid_t gid); + +/** + * @brief Copy given ccache into a MEMORY ccache + * + * @param[in] mem_ctx Talloc memory context the new ccache name should be + * allocated on + * @param[in] kctx Kerberos context + * @param[in] ccache_file Name of existing ccache + * @param[out] _mem_name Name of the new MEMORY ccache + * + * In contrast to MEMORY keytabs MEMORY ccaches can and must be removed + * explicitly with krb5_cc_destroy() from the memory. Just calling + * krb5_cc_close() will keep the MEMORY ccache in memory even if there are no + * open handles for the given MEMORY ccache. + */ +krb5_error_code copy_ccache_into_memory(TALLOC_CTX *mem_ctx, krb5_context kctx, + const char *ccache_file, + char **_mem_name); +#endif /* __KRB5_CCACHE_H__ */ diff --git a/src/providers/krb5/krb5_child.c b/src/providers/krb5/krb5_child.c new file mode 100644 index 0000000..704f650 --- /dev/null +++ b/src/providers/krb5/krb5_child.c @@ -0,0 +1,4247 @@ +/* + SSSD + + Kerberos 5 Backend Module -- tgt_req and changepw child + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2009-2010 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <sys/types.h> +#include <unistd.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <ctype.h> +#include <popt.h> +#include <sys/prctl.h> + +#include <security/pam_modules.h> + +#include "util/util.h" +#include "util/sss_krb5.h" +#include "util/user_info_msg.h" +#include "util/child_common.h" +#include "util/find_uid.h" +#include "util/sss_chain_id.h" +#include "util/sss_ptr_hash.h" +#include "src/util/util_errors.h" +#include "providers/backend.h" +#include "providers/krb5/krb5_auth.h" +#include "providers/krb5/krb5_utils.h" +#include "krb5_plugin/idp/idp.h" +#ifdef BUILD_PASSKEY +#include "responder/pam/pamsrv_passkey.h" +#include "krb5_plugin/passkey/passkey.h" +#endif /* BUILD_PASSKEY */ +#include "sss_cli.h" + +#define SSSD_KRB5_CHANGEPW_PRINCIPAL "kadmin/changepw" +#ifndef BUILD_PASSKEY +#define SSSD_PASSKEY_QUESTION "passkey" +#endif /* BUILD_PASSKEY */ + +typedef krb5_error_code +(*k5_init_creds_password_fn_t)(krb5_context context, krb5_creds *creds, + krb5_principal client, const char *password, + krb5_prompter_fct prompter, void *data, + krb5_deltat start_time, + const char *in_tkt_service, + krb5_get_init_creds_opt *k5_gic_options); + +enum k5c_fast_opt { + K5C_FAST_NEVER, + K5C_FAST_TRY, + K5C_FAST_DEMAND, +}; + +struct cli_opts { + char *realm; + char *lifetime; + char *rtime; + char *use_fast_str; + char *fast_principal; + uint32_t check_pac_flags; + bool canonicalize; + bool fast_use_anonymous_pkinit; +}; + +struct krb5_req { + krb5_context ctx; + krb5_principal princ; + krb5_principal princ_orig; + char* name; + krb5_creds *creds; + bool otp; + bool password_prompting; + bool pkinit_prompting; + char *otp_vendor; + char *otp_token_id; + char *otp_challenge; + krb5_get_init_creds_opt *options; + k5_init_creds_password_fn_t krb5_get_init_creds_password; + + struct pam_data *pd; + + char *realm; + char *ccname; + char *keytab; + bool validate; + bool posix_domain; + bool send_pac; + bool use_enterprise_princ; + char *fast_ccname; + + const char *upn; + uid_t uid; + gid_t gid; + + char *old_ccname; + bool old_cc_valid; + bool old_cc_active; + enum k5c_fast_opt fast_val; + + uid_t fast_uid; + gid_t fast_gid; + struct sss_creds *pcsc_saved_creds; + + struct cli_opts *cli_opts; +}; + +static krb5_context krb5_error_ctx; +#define KRB5_CHILD_DEBUG(level, error) KRB5_DEBUG(level, krb5_error_ctx, error) + +static errno_t k5c_attach_otp_info_msg(struct krb5_req *kr); +static errno_t k5c_attach_oauth2_info_msg(struct krb5_req *kr, struct sss_idp_oauth2 *data); +#ifdef BUILD_PASSKEY +static errno_t k5c_attach_passkey_msg(struct krb5_req *kr, struct sss_passkey_challenge *data); +#endif /* BUILD_PASSKEY */ +static errno_t k5c_attach_keep_alive_msg(struct krb5_req *kr); +static errno_t k5c_recv_data(struct krb5_req *kr, int fd, uint32_t *offline); +static errno_t k5c_send_data(struct krb5_req *kr, int fd, errno_t error); + +static errno_t k5c_become_user(uid_t uid, gid_t gid, bool is_posix) +{ + if (is_posix == false) { + DEBUG(SSSDBG_TRACE_FUNC, + "Will not drop privileges for a non-POSIX user\n"); + return EOK; + } + return become_user(uid, gid); +} + +static krb5_error_code set_lifetime_options(struct cli_opts *cli_opts, + krb5_get_init_creds_opt *options) +{ + krb5_error_code kerr; + krb5_deltat lifetime; + + if (cli_opts->rtime == NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, + "No specific renewable lifetime requested.\n"); + + /* Unset option flag to make sure defaults from krb5.conf are used. */ + options->flags &= ~(KRB5_GET_INIT_CREDS_OPT_RENEW_LIFE); + } else { + kerr = krb5_string_to_deltat(cli_opts->rtime, &lifetime); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "krb5_string_to_deltat failed for [%s].\n", cli_opts->rtime); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + DEBUG(SSSDBG_CONF_SETTINGS, "Renewable lifetime is set to [%s]\n", + cli_opts->rtime); + krb5_get_init_creds_opt_set_renew_life(options, lifetime); + } + + if (cli_opts->lifetime == NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, "No specific lifetime requested.\n"); + + /* Unset option flag to make sure defaults from krb5.conf are used. */ + options->flags &= ~(KRB5_GET_INIT_CREDS_OPT_TKT_LIFE); + } else { + kerr = krb5_string_to_deltat(cli_opts->lifetime, &lifetime); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "krb5_string_to_deltat failed for [%s].\n", + cli_opts->lifetime); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + DEBUG(SSSDBG_CONF_SETTINGS, "Lifetime is set to [%s]\n", + cli_opts->lifetime); + krb5_get_init_creds_opt_set_tkt_life(options, lifetime); + } + + return 0; +} + +static void set_canonicalize_option(struct cli_opts *cli_opts, + krb5_get_init_creds_opt *opts) +{ + int canonicalize = 0; + + canonicalize = cli_opts->canonicalize ? 1 : 0; + DEBUG(SSSDBG_CONF_SETTINGS, "Canonicalization is set to [%s]\n", + cli_opts->canonicalize ? "true" : "false"); + sss_krb5_get_init_creds_opt_set_canonicalize(opts, canonicalize); +} + +static void set_changepw_options(krb5_get_init_creds_opt *options) +{ + sss_krb5_get_init_creds_opt_set_canonicalize(options, 0); + krb5_get_init_creds_opt_set_forwardable(options, 0); + krb5_get_init_creds_opt_set_proxiable(options, 0); + krb5_get_init_creds_opt_set_renew_life(options, 0); + krb5_get_init_creds_opt_set_tkt_life(options, 5*60); +} + +static void revert_changepw_options(struct cli_opts *cli_opts, + krb5_get_init_creds_opt *options) +{ + krb5_error_code kerr; + + set_canonicalize_option(cli_opts, options); + + /* Currently we do not set forwardable and proxiable explicitly, the flags + * must be removed so that libkrb5 can take the defaults from krb5.conf */ + options->flags &= ~(KRB5_GET_INIT_CREDS_OPT_FORWARDABLE); + options->flags &= ~(KRB5_GET_INIT_CREDS_OPT_PROXIABLE); + + kerr = set_lifetime_options(cli_opts, options); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "set_lifetime_options failed.\n"); + } +} + + +static errno_t sss_send_pac(krb5_authdata **pac_authdata) +{ + struct sss_cli_req_data sss_data; + int ret; + int errnop; + + sss_data.len = pac_authdata[0]->length; + sss_data.data = pac_authdata[0]->contents; + + ret = sss_pac_make_request(SSS_PAC_ADD_PAC_USER, &sss_data, + NULL, NULL, &errnop); + DEBUG(SSSDBG_TRACE_ALL, + "NSS return code [%d], request return code [%d][%s].\n", ret, + errnop, sss_strerror(errnop)); + if (errnop == ERR_CHECK_PAC_FAILED) { + return ERR_CHECK_PAC_FAILED; + } + + if (ret == NSS_STATUS_UNAVAIL) { + DEBUG(SSSDBG_MINOR_FAILURE, "failed to contact PAC responder\n"); + return EIO; + } else if (ret != NSS_STATUS_SUCCESS || errnop != 0) { + DEBUG(SSSDBG_OP_FAILURE, "sss_pac_make_request failed [%d][%d].\n", + ret, errnop); + return EIO; + } + DEBUG(SSSDBG_TRACE_FUNC, + "PAC responder contacted. It might take a bit of time in case the " + "cache is not up to date.\n"); + + return EOK; +} + +static void sss_krb5_expire_callback_func(krb5_context context, void *data, + krb5_timestamp password_expiration, + krb5_timestamp account_expiration, + krb5_boolean is_last_req) +{ + int ret; + uint32_t *blob; + long exp_time; + struct krb5_req *kr = talloc_get_type(data, struct krb5_req); + + if (password_expiration == 0) { + return; + } + + exp_time = password_expiration - time(NULL); + if (exp_time < 0 || exp_time > UINT32_MAX) { + DEBUG(SSSDBG_CRIT_FAILURE, "Time to expire out of range.\n"); + return; + } + DEBUG(SSSDBG_TRACE_INTERNAL, "exp_time: [%ld]\n", exp_time); + + blob = talloc_array(kr->pd, uint32_t, 2); + if (blob == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_array failed.\n"); + return; + } + + blob[0] = SSS_PAM_USER_INFO_EXPIRE_WARN; + blob[1] = (uint32_t) exp_time; + + ret = pam_add_response(kr->pd, SSS_PAM_USER_INFO, 2 * sizeof(uint32_t), + (uint8_t *) blob); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + } + + return; +} + +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_RESPONDER +/* + * TODO: These features generally would requires a significant refactoring + * of SSSD and MIT krb5 doesn't support them anyway. They are listed here + * simply as a reminder of things that might become future feature potential. + * + * 1. tokeninfo selection + * 2. challenge + * 3. discreet token/PIN prompting + * 4. interactive OTP format correction + * 5. nextOTP + * + */ +typedef int (*checker)(int c); + +static inline checker pick_checker(int format) +{ + switch (format) { + case KRB5_RESPONDER_OTP_FORMAT_DECIMAL: + return isdigit; + case KRB5_RESPONDER_OTP_FORMAT_HEXADECIMAL: + return isxdigit; + case KRB5_RESPONDER_OTP_FORMAT_ALPHANUMERIC: + return isalnum; + } + + return NULL; +} + +static int token_pin_destructor(char *mem) +{ + return sss_erase_talloc_mem_securely(mem); +} + +static krb5_error_code tokeninfo_matches_2fa(TALLOC_CTX *mem_ctx, + const krb5_responder_otp_tokeninfo *ti, + const char *fa1, size_t fa1_len, + const char *fa2, size_t fa2_len, + char **out_token, char **out_pin) +{ + char *token = NULL, *pin = NULL; + checker check = NULL; + int i; + + if (ti->flags & KRB5_RESPONDER_OTP_FLAGS_NEXTOTP) { + return ENOTSUP; + } + + if (ti->challenge != NULL) { + return ENOTSUP; + } + + /* This is a non-sensical value. */ + if (ti->length == 0) { + return EPROTO; + } + + if (ti->flags & KRB5_RESPONDER_OTP_FLAGS_COLLECT_TOKEN) { + if (ti->length > 0 && ti->length != fa2_len) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Expected [%d] and given [%zu] token size " + "do not match.\n", ti->length, fa2_len); + return EMSGSIZE; + } + + if (ti->flags & KRB5_RESPONDER_OTP_FLAGS_COLLECT_PIN) { + if (ti->flags & KRB5_RESPONDER_OTP_FLAGS_SEPARATE_PIN) { + + pin = talloc_strndup(mem_ctx, fa1, fa1_len); + if (pin == NULL) { + talloc_free(token); + return ENOMEM; + } + talloc_set_destructor(pin, token_pin_destructor); + + token = talloc_strndup(mem_ctx, fa2, fa2_len); + if (token == NULL) { + return ENOMEM; + } + talloc_set_destructor(token, token_pin_destructor); + + check = pick_checker(ti->format); + } + } else { + token = talloc_asprintf(mem_ctx, "%s%s", fa1, fa2); + if (token == NULL) { + return ENOMEM; + } + talloc_set_destructor(token, token_pin_destructor); + + check = pick_checker(ti->format); + } + } else { + /* Assuming PIN only required */ + pin = talloc_strndup(mem_ctx, fa1, fa1_len); + if (pin == NULL) { + return ENOMEM; + } + talloc_set_destructor(pin, token_pin_destructor); + } + + /* If check is set, we need to verify the contents of the token. */ + for (i = 0; check != NULL && token[i] != '\0'; i++) { + if (!check(token[i])) { + talloc_free(token); + talloc_free(pin); + return EBADMSG; + } + } + + *out_token = token; + *out_pin = pin; + return 0; +} + +static krb5_error_code tokeninfo_matches_pwd(TALLOC_CTX *mem_ctx, + const krb5_responder_otp_tokeninfo *ti, + const char *pwd, size_t len, + char **out_token, char **out_pin) +{ + char *token = NULL, *pin = NULL; + checker check = NULL; + int i; + + + if (ti->flags & KRB5_RESPONDER_OTP_FLAGS_NEXTOTP) { + return ENOTSUP; + } + + if (ti->challenge != NULL) { + return ENOTSUP; + } + + /* This is a non-sensical value. */ + if (ti->length == 0) { + return EPROTO; + } + + if (ti->flags & KRB5_RESPONDER_OTP_FLAGS_COLLECT_TOKEN) { + /* ASSUMPTION: authtok has one of the following formats: + * 1. TokenValue + * 2. PIN+TokenValue + */ + token = talloc_strndup(mem_ctx, pwd, len); + if (token == NULL) { + return ENOMEM; + } + talloc_set_destructor(token, token_pin_destructor); + + if (ti->flags & KRB5_RESPONDER_OTP_FLAGS_COLLECT_PIN) { + /* If the server desires a separate PIN, we will split it. + * ASSUMPTION: Format of authtok is PIN+TokenValue. */ + if (ti->flags & KRB5_RESPONDER_OTP_FLAGS_SEPARATE_PIN) { + if (ti->length < 1) { + talloc_free(token); + return ENOTSUP; + } + + if (ti->length >= len) { + talloc_free(token); + return EMSGSIZE; + } + + /* Copy the PIN from the front of the value. */ + pin = talloc_strndup(NULL, pwd, len - ti->length); + if (pin == NULL) { + talloc_free(token); + return ENOMEM; + } + talloc_set_destructor(pin, token_pin_destructor); + + /* Remove the PIN from the front of the token value. */ + memmove(token, token + len - ti->length, ti->length + 1); + + check = pick_checker(ti->format); + } else { + if (ti->length > 0 && ti->length > len) { + talloc_free(token); + return EMSGSIZE; + } + } + } else { + if (ti->length > 0 && ti->length != len) { + talloc_free(token); + return EMSGSIZE; + } + + check = pick_checker(ti->format); + } + } else { + pin = talloc_strndup(mem_ctx, pwd, len); + if (pin == NULL) { + return ENOMEM; + } + talloc_set_destructor(pin, token_pin_destructor); + } + + /* If check is set, we need to verify the contents of the token. */ + for (i = 0; check != NULL && token[i] != '\0'; i++) { + if (!check(token[i])) { + talloc_free(token); + talloc_free(pin); + return EBADMSG; + } + } + + *out_token = token; + *out_pin = pin; + return 0; +} + +static krb5_error_code tokeninfo_matches(TALLOC_CTX *mem_ctx, + const krb5_responder_otp_tokeninfo *ti, + struct sss_auth_token *auth_tok, + char **out_token, char **out_pin) +{ + int ret; + const char *pwd; + size_t len; + const char *fa2; + size_t fa2_len; + + switch (sss_authtok_get_type(auth_tok)) { + case SSS_AUTHTOK_TYPE_PASSWORD: + ret = sss_authtok_get_password(auth_tok, &pwd, &len); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_authtok_get_password failed.\n"); + return ret; + } + + return tokeninfo_matches_pwd(mem_ctx, ti, pwd, len, out_token, out_pin); + break; + case SSS_AUTHTOK_TYPE_2FA_SINGLE: + ret = sss_authtok_get_2fa_single(auth_tok, &pwd, &len); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_authtok_get_password failed.\n"); + return ret; + } + + return tokeninfo_matches_pwd(mem_ctx, ti, pwd, len, out_token, out_pin); + break; + case SSS_AUTHTOK_TYPE_2FA: + ret = sss_authtok_get_2fa(auth_tok, &pwd, &len, &fa2, &fa2_len); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_authtok_get_2fa failed.\n"); + return ret; + } + + return tokeninfo_matches_2fa(mem_ctx, ti, pwd, len, fa2, fa2_len, + out_token, out_pin); + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "Unsupported authtok type %d\n", sss_authtok_get_type(auth_tok)); + } + + return EINVAL; +} + +static krb5_error_code answer_otp(krb5_context ctx, + struct krb5_req *kr, + krb5_responder_context rctx) +{ + krb5_responder_otp_challenge *chl; + char *token = NULL, *pin = NULL; + krb5_error_code ret; + size_t i; + + ret = krb5_responder_otp_get_challenge(ctx, rctx, &chl); + if (ret != EOK || chl == NULL) { + /* Either an error, or nothing to do. */ + return ret; + } + + if (chl->tokeninfo == NULL || chl->tokeninfo[0] == NULL) { + /* No tokeninfos? Absurd! */ + ret = EINVAL; + goto done; + } + + kr->otp = true; + + if (kr->pd->cmd == SSS_PAM_PREAUTH) { + for (i = 0; chl->tokeninfo[i] != NULL; i++) { + DEBUG(SSSDBG_TRACE_ALL, "[%zu] Vendor [%s].\n", + i, chl->tokeninfo[i]->vendor); + DEBUG(SSSDBG_TRACE_ALL, "[%zu] Token-ID [%s].\n", + i, chl->tokeninfo[i]->token_id); + DEBUG(SSSDBG_TRACE_ALL, "[%zu] Challenge [%s].\n", + i, chl->tokeninfo[i]->challenge); + DEBUG(SSSDBG_TRACE_ALL, "[%zu] Flags [%d].\n", + i, chl->tokeninfo[i]->flags); + } + + if (chl->tokeninfo[0]->vendor != NULL) { + kr->otp_vendor = talloc_strdup(kr, chl->tokeninfo[0]->vendor); + } + if (chl->tokeninfo[0]->token_id != NULL) { + kr->otp_token_id = talloc_strdup(kr, chl->tokeninfo[0]->token_id); + } + if (chl->tokeninfo[0]->challenge != NULL) { + kr->otp_challenge = talloc_strdup(kr, chl->tokeninfo[0]->challenge); + } + /* Allocation errors are ignored on purpose */ + + DEBUG(SSSDBG_TRACE_INTERNAL, "Exit answer_otp during pre-auth.\n"); + return EAGAIN; + } + + /* Find the first supported tokeninfo which matches our authtoken. */ + for (i = 0; chl->tokeninfo[i] != NULL; i++) { + ret = tokeninfo_matches(kr, chl->tokeninfo[i], kr->pd->authtok, + &token, &pin); + if (ret == EOK) { + break; + } + + switch (ret) { + case EBADMSG: + case EMSGSIZE: + case ENOTSUP: + case EPROTO: + break; + default: + goto done; + } + } + if (chl->tokeninfo[i] == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "No tokeninfos found which match our credentials.\n"); + ret = EOK; + goto done; + } + + if (chl->tokeninfo[i]->flags & KRB5_RESPONDER_OTP_FLAGS_COLLECT_TOKEN) { + /* Don't let SSSD cache the OTP authtoken since it is single-use. */ + ret = pam_add_response(kr->pd, SSS_OTP, 0, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + goto done; + } + } + + /* Respond with the appropriate answer. */ + ret = krb5_responder_otp_set_answer(ctx, rctx, i, token, pin); +done: + talloc_free(token); + talloc_free(pin); + krb5_responder_otp_challenge_free(ctx, rctx, chl); + return ret; +} + +static bool pkinit_identity_matches(const char *identity, + const char *token_name, + const char *module_name) +{ + TALLOC_CTX *tmp_ctx = NULL; + char *str; + bool res = false; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed.\n"); + return false; + } + + str = talloc_asprintf(tmp_ctx, "module_name=%s", module_name); + if (str == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + goto done; + } + + if (strstr(identity, str) == NULL) { + DEBUG(SSSDBG_TRACE_ALL, "Identity [%s] does not contain [%s].\n", + identity, str); + goto done; + } + DEBUG(SSSDBG_TRACE_ALL, "Found [%s] in identity [%s].\n", str, identity); + + str = talloc_asprintf(tmp_ctx, "token=%s", token_name); + if (str == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + goto done; + } + + if (strstr(identity, str) == NULL) { + DEBUG(SSSDBG_TRACE_ALL, "Identity [%s] does not contain [%s].\n", + identity, str); + goto done; + } + DEBUG(SSSDBG_TRACE_ALL, "Found [%s] in identity [%s].\n", str, identity); + + res = true; + +done: + talloc_free(tmp_ctx); + + return res; +} + +static krb5_error_code answer_pkinit(krb5_context ctx, + struct krb5_req *kr, + krb5_responder_context rctx) +{ + krb5_error_code kerr; + const char *pin = NULL; + const char *token_name = NULL; + const char *module_name = NULL; + krb5_responder_pkinit_challenge *chl = NULL; + size_t c; + + kerr = krb5_responder_pkinit_get_challenge(ctx, rctx, &chl); + if (kerr != EOK || chl == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "krb5_responder_pkinit_get_challenge failed.\n"); + return kerr; + } + if (chl->identities == NULL || chl->identities[0] == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "No identities for pkinit!\n"); + kerr = EINVAL; + goto done; + } + + for (c = 0; chl->identities[c] != NULL; c++) { + DEBUG(SSSDBG_TRACE_ALL, "[%zu] Identity [%s] flags [%"PRId32"].\n", + c, chl->identities[c]->identity, + chl->identities[c]->token_flags); + } + + DEBUG(SSSDBG_TRACE_ALL, "Setting pkinit_prompting.\n"); + kr->pkinit_prompting = true; + + if (kr->pd->cmd == SSS_PAM_AUTHENTICATE + && (sss_authtok_get_type(kr->pd->authtok) + == SSS_AUTHTOK_TYPE_SC_PIN + || sss_authtok_get_type(kr->pd->authtok) + == SSS_AUTHTOK_TYPE_SC_KEYPAD)) { + kerr = sss_authtok_get_sc(kr->pd->authtok, &pin, NULL, + &token_name, NULL, + &module_name, NULL, + NULL, NULL, NULL, NULL); + if (kerr != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "sss_authtok_get_sc failed.\n"); + goto done; + } + + for (c = 0; chl->identities[c] != NULL; c++) { + if (chl->identities[c]->identity != NULL + && pkinit_identity_matches(chl->identities[c]->identity, + token_name, module_name)) { + break; + } + } + + if (chl->identities[c] == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "No matching identity for [%s][%s] found in pkinit challenge.\n", + token_name, module_name); + kerr = EINVAL; + goto done; + } + + kerr = krb5_responder_pkinit_set_answer(ctx, rctx, + chl->identities[c]->identity, + pin); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, + "krb5_responder_set_answer failed.\n"); + } + + goto done; + } + + kerr = EOK; + +done: + krb5_responder_pkinit_challenge_free(ctx, rctx, chl); + + return kerr; +} + +static errno_t krb5_req_update(struct krb5_req *dest, struct krb5_req *src) +{ + /* Check request validity. This should never happen, but it is better to + * be little paranoid. */ + if (strcmp(dest->ccname, src->ccname) != 0) { + return EINVAL; + } + + if (strcmp(dest->upn, src->upn) != 0) { + return EINVAL; + } + + if (dest->uid != src->uid || dest->gid != src->gid) { + return EINVAL; + } + + /* Update PAM data. */ + talloc_free(dest->pd); + dest->pd = talloc_steal(dest, src->pd); + + return EOK; +} + +static krb5_error_code idp_oauth2_preauth(struct krb5_req *kr, + struct sss_idp_oauth2 *oauth2) +{ + struct krb5_req *tmpkr = NULL; + uint32_t offline; + errno_t ret; + + if (oauth2->verification_uri == NULL || oauth2->user_code == NULL) { + ret = EINVAL; + goto done; + } + + /* Challenge was presented. We need to continue the authentication + * with this exact child process in order to maintain internal Kerberos + * state so we are able to respond to this particular challenge. */ + + ret = k5c_attach_oauth2_info_msg(kr, oauth2); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "k5c_attach_oauth2_info_msg failed.\n"); + return ret; + } + + ret = k5c_attach_keep_alive_msg(kr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "k5c_attach_keep_alive_msg failed.\n"); + return ret; + } + + tmpkr = talloc_zero(NULL, struct krb5_req); + if (tmpkr == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + ret = ENOMEM; + goto done; + } + + /* Send reply and wait for next step. */ + ret = k5c_send_data(kr, STDOUT_FILENO, ret); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to send reply\n"); + } + + ret = k5c_recv_data(tmpkr, STDIN_FILENO, &offline); + if (ret != EOK) { + goto done; + } + + ret = krb5_req_update(kr, tmpkr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to update krb request [%d]: %s\n", + ret, sss_strerror(ret)); + goto done; + } + +done: + talloc_free(tmpkr); + return ret; +} + +static krb5_error_code answer_idp_oauth2(krb5_context kctx, + struct krb5_req *kr, + krb5_responder_context rctx) +{ + enum sss_authtok_type type; + struct sss_idp_oauth2 *data; + const char *challenge; + const char *token; + size_t token_len; + krb5_error_code kerr; + + challenge = krb5_responder_get_challenge(kctx, rctx, + SSSD_IDP_OAUTH2_QUESTION); + if (challenge == NULL) { + return ENOENT; + } + + data = sss_idp_oauth2_decode_challenge(challenge); + if (data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to parse OAuth2 challenge\n"); + return EINVAL; + } + + if (kr->pd->cmd == SSS_PAM_PREAUTH) { + kerr = idp_oauth2_preauth(kr, data); + if (kerr != EOK) { + goto done; + } + } + + if (kr->pd->cmd != SSS_PAM_AUTHENTICATE) { + DEBUG(SSSDBG_OP_FAILURE, "Unexpected command [%d]\n", kr->pd->cmd); + kerr = EINVAL; + goto done; + } + + type = sss_authtok_get_type(kr->pd->authtok); + if (type != SSS_AUTHTOK_TYPE_OAUTH2) { + DEBUG(SSSDBG_OP_FAILURE, "Unexpected authentication token type [%s]\n", + sss_authtok_type_to_str(type)); + kerr = EINVAL; + goto done; + } + + kerr = sss_authtok_get_oauth2(kr->pd->authtok, &token, &token_len); + if (kerr != EOK) { + goto done; + } + + if (strlen(data->user_code) != token_len && strcmp(data->user_code, token) != 0) { + DEBUG(SSSDBG_OP_FAILURE, "User code do not match!\n"); + kerr = EINVAL; + goto done; + } + + /* Don't let SSSD cache the authtoken since it is single-use. */ + kerr = pam_add_response(kr->pd, SSS_OTP, 0, NULL); + if (kerr != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + goto done; + } + + /* The answer is arbitrary but we need to provide some since krb5 lib + * expects it. So we choose the pin. */ + kerr = krb5_responder_set_answer(kctx, rctx, SSSD_IDP_OAUTH2_QUESTION, + data->user_code); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to set IdP answer [%d]\n", kerr); + goto done; + } + + kerr = EOK; + +done: + sss_idp_oauth2_free(data); + + return kerr; +} + +#ifdef BUILD_PASSKEY +static errno_t k5c_attach_passkey_msg(struct krb5_req *kr, + struct sss_passkey_challenge *data) +{ + uint8_t *msg; + const char *user_verification; + int i; + size_t msg_len = 0; + size_t domain_len = 0; + size_t crypto_len = 0; + size_t num_creds = 0; + size_t cred_len = 0; + size_t verification_len = 0; + size_t idx = 0; + errno_t ret; + + if (data->domain == NULL || data->credential_id_list == NULL + || data->cryptographic_challenge == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Empty passkey domain, credential id list, or cryptographic " + "challenge\n"); + return EINVAL; + } + + user_verification = data->user_verification == 0 ? "false" : "true"; + verification_len = strlen(user_verification) + 1; + msg_len += verification_len; + + crypto_len = strlen(data->cryptographic_challenge) + 1; + msg_len += crypto_len; + + domain_len = strlen(data->domain) + 1; + msg_len += domain_len; + + /* credentials list size */ + msg_len += sizeof(uint32_t); + + for (i = 0; data->credential_id_list[i] != NULL; i++) { + msg_len += (strlen(data->credential_id_list[i]) + 1); + } + num_creds = i; + + msg = talloc_zero_size(kr, msg_len); + if (msg == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_size failed.\n"); + return ENOMEM; + } + + /* To avoid sending extraneous data back and forth to pam_sss, + * (and reduce boilerplate memcpy code) only the user + * verification and cryptographic challenge are retrieved in pam_sss. + * + * The remaining passkey data (domain, creds list, num_creds) + * is sent to the PAM responder and stored in a hash table. The + * challenge is used as a unique key of the hash table. The pam_sss + * reply includes the challenge which is used to lookup the passkey + * data in the PAM responder, ensuring it matches the originating + * request */ + memcpy(msg + idx, user_verification, verification_len); + idx += verification_len; + + memcpy(msg + idx, data->cryptographic_challenge, crypto_len); + idx += crypto_len; + + memcpy(msg + idx, data->domain, domain_len); + idx += domain_len; + + SAFEALIGN_COPY_UINT32(msg + idx, &num_creds, &idx); + + for (i = 0; data->credential_id_list[i] != NULL; i++) { + cred_len = strlen(data->credential_id_list[i]) + 1; + memcpy(msg + idx, data->credential_id_list[i], cred_len); + idx += cred_len; + } + + ret = pam_add_response(kr->pd, SSS_PAM_PASSKEY_KRB_INFO, msg_len, msg); + talloc_zfree(msg); + + return ret; +} + +static krb5_error_code passkey_preauth(struct krb5_req *kr, + struct sss_passkey_challenge *passkey) +{ + struct krb5_req *tmpkr = NULL; + uint32_t offline; + errno_t ret; + + if (passkey->domain == NULL || passkey->credential_id_list == NULL + || passkey->cryptographic_challenge == NULL) { + ret = EINVAL; + goto done; + } + + ret = k5c_attach_passkey_msg(kr, passkey); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "k5c_attach_passkey_info_msg failed.\n"); + return ret; + } + + /* Challenge was presented. We need to continue the authentication + * with this exact child process in order to maintain internal Kerberos + * state so we are able to respond to this particular challenge. */ + ret = k5c_attach_keep_alive_msg(kr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "k5c_attach_keep_alive_msg failed.\n"); + return ret; + } + + tmpkr = talloc_zero(NULL, struct krb5_req); + if (tmpkr == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + ret = ENOMEM; + goto done; + } + + /* Send reply and wait for next step. */ + ret = k5c_send_data(kr, STDOUT_FILENO, ret); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to send reply\n"); + } + + ret = k5c_recv_data(tmpkr, STDIN_FILENO, &offline); + if (ret != EOK) { + goto done; + } + + ret = krb5_req_update(kr, tmpkr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to update krb request [%d]: %s\n", + ret, sss_strerror(ret)); + goto done; + } + +done: + talloc_free(tmpkr); + return ret; +} +#endif /* BUILD_PASSKEY */ + +static krb5_error_code answer_passkey(krb5_context kctx, + struct krb5_req *kr, + krb5_responder_context rctx) +{ +#ifndef BUILD_PASSKEY + DEBUG(SSSDBG_TRACE_FUNC, "Passkey auth not possible, SSSD built without passkey support!\n"); + return EINVAL; +#else + enum sss_authtok_type type; + struct sss_passkey_message *msg; + struct sss_passkey_message *reply_msg = NULL; + const char *challenge; + const char *reply; + char *reply_str = NULL; + enum sss_passkey_phase phase; + const char *state; + size_t reply_len; + krb5_error_code kerr; + + challenge = krb5_responder_get_challenge(kctx, rctx, + SSSD_PASSKEY_QUESTION); + if (challenge == NULL) { + return ENOENT; + } + + msg = sss_passkey_message_decode(challenge); + if (msg == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to decode passkey message\n"); + return EINVAL; + } + + if (kr->pd->cmd == SSS_PAM_PREAUTH) { + kerr = passkey_preauth(kr, msg->data.challenge); + if (kerr != EOK) { + goto done; + } + } + + if (kr->pd->cmd != SSS_PAM_AUTHENTICATE) { + DEBUG(SSSDBG_OP_FAILURE, "Unexpected command [%d]\n", kr->pd->cmd); + kerr = EINVAL; + goto done; + } + + type = sss_authtok_get_type(kr->pd->authtok); + if (type != SSS_AUTHTOK_TYPE_PASSKEY_REPLY) { + DEBUG(SSSDBG_OP_FAILURE, "Unexpected authentication token type [%s]\n", + sss_authtok_type_to_str(type)); + kerr = EINVAL; + goto done; + } + + kerr = sss_authtok_get_passkey_reply(kr->pd->authtok, &reply, &reply_len); + if (kerr != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Unexpected command [%d]\n", kr->pd->cmd); + goto done; + } + + phase = SSS_PASSKEY_PHASE_REPLY; + state = SSSD_PASSKEY_REPLY_STATE; + reply_msg = sss_passkey_message_from_reply_json(phase, state, reply); + if (reply_msg == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to prefix passkey message\n"); + kerr = EINVAL; + goto done; + } + + reply_str = sss_passkey_message_encode(reply_msg); + if (reply_str == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to encode passkey message\n"); + kerr = EINVAL; + goto done; + } + + /* Don't let SSSD cache the authtoken since it is single-use. */ + kerr = pam_add_response(kr->pd, SSS_OTP, 0, NULL); + if (kerr != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + goto done; + } + + kerr = krb5_responder_set_answer(kctx, rctx, SSSD_PASSKEY_QUESTION, + reply_str); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to set passkey answer [%d]\n", kerr); + goto done; + } + + kerr = EOK; + +done: + if (reply_str != NULL) { + free(reply_str); + } + if (reply_msg != NULL) { + sss_passkey_message_free(reply_msg); + } + + return kerr; +#endif /* BUILD_PASSKEY */ +} + +static krb5_error_code sss_krb5_responder(krb5_context ctx, + void *data, + krb5_responder_context rctx) +{ + struct krb5_req *kr = talloc_get_type(data, struct krb5_req); + const char * const *question_list; + size_t c; + const char *pwd; + int ret; + krb5_error_code kerr; + + if (kr == NULL) { + return EINVAL; + } + + question_list = krb5_responder_list_questions(ctx, rctx); + + if (question_list != NULL) { + for (c = 0; question_list[c] != NULL; c++) { + DEBUG(SSSDBG_TRACE_ALL, "Got question [%s].\n", question_list[c]); + + if (strcmp(question_list[c], + KRB5_RESPONDER_QUESTION_PASSWORD) == 0) { + kr->password_prompting = true; + + if ((kr->pd->cmd == SSS_PAM_AUTHENTICATE + || kr->pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM + || kr->pd->cmd == SSS_PAM_CHAUTHTOK) + && sss_authtok_get_type(kr->pd->authtok) + == SSS_AUTHTOK_TYPE_PASSWORD) { + ret = sss_authtok_get_password(kr->pd->authtok, &pwd, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "sss_authtok_get_password failed.\n"); + return ret; + } + + kerr = krb5_responder_set_answer(ctx, rctx, + KRB5_RESPONDER_QUESTION_PASSWORD, + pwd); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, + "krb5_responder_set_answer failed.\n"); + } + + return kerr; + } + } else if (strcmp(question_list[c], + KRB5_RESPONDER_QUESTION_PKINIT) == 0 + && (sss_authtok_get_type(kr->pd->authtok) + == SSS_AUTHTOK_TYPE_SC_PIN + || sss_authtok_get_type(kr->pd->authtok) + == SSS_AUTHTOK_TYPE_SC_KEYPAD)) { + return answer_pkinit(ctx, kr, rctx); + } else if (strcmp(question_list[c], SSSD_IDP_OAUTH2_QUESTION) == 0) { + return answer_idp_oauth2(ctx, kr, rctx); + } else if (strcmp(question_list[c], SSSD_PASSKEY_QUESTION) == 0) { + return answer_passkey(ctx, kr, rctx); + } + } + } + + return answer_otp(ctx, kr, rctx); +} +#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_RESPONDER */ + +static char *password_or_responder(const char *password) +{ +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_RESPONDER + /* If the new responder interface is available, we will handle even simple + * passwords in the responder. */ + return NULL; +#else + return discard_const(password); +#endif +} + +static krb5_error_code sss_krb5_prompter(krb5_context context, void *data, + const char *name, const char *banner, + int num_prompts, krb5_prompt prompts[]) +{ + int ret; + size_t c; + struct krb5_req *kr = talloc_get_type(data, struct krb5_req); + + if (kr == NULL) { + return EINVAL; + } + + DEBUG(SSSDBG_TRACE_ALL, + "sss_krb5_prompter name [%s] banner [%s] num_prompts [%d] EINVAL.\n", + name, banner, num_prompts); + + if (num_prompts != 0) { + for (c = 0; c < num_prompts; c++) { + DEBUG(SSSDBG_TRACE_ALL, "Prompt [%zu][%s].\n", c, + prompts[c].prompt); + } + + DEBUG(SSSDBG_FUNC_DATA, "Prompter interface isn't used for password prompts by SSSD.\n"); + return KRB5_LIBOS_CANTREADPWD; + } + + if (banner == NULL || *banner == '\0') { + DEBUG(SSSDBG_FUNC_DATA, + "Prompter called with empty banner, nothing to do.\n"); + return EOK; + } + + DEBUG(SSSDBG_FUNC_DATA, "Prompter called with [%s].\n", banner); + + ret = pam_add_response(kr->pd, SSS_PAM_TEXT_MSG, strlen(banner)+1, + (const uint8_t *) banner); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + } + + return EOK; +} + + +static krb5_error_code create_empty_cred(krb5_context ctx, krb5_principal princ, + krb5_creds **_cred) +{ + krb5_error_code kerr; + krb5_creds *cred = NULL; + krb5_data *krb5_realm; + + cred = calloc(sizeof(krb5_creds), 1); + if (cred == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "calloc failed.\n"); + return ENOMEM; + } + + kerr = krb5_copy_principal(ctx, princ, &cred->client); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_copy_principal failed.\n"); + goto done; + } + + krb5_realm = krb5_princ_realm(ctx, princ); + + kerr = krb5_build_principal_ext(ctx, &cred->server, + krb5_realm->length, krb5_realm->data, + KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME, + krb5_realm->length, krb5_realm->data, 0); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_build_principal_ext failed.\n"); + goto done; + } + + DEBUG(SSSDBG_TRACE_INTERNAL, "Created empty krb5_creds.\n"); + +done: + if (kerr != 0) { + krb5_free_cred_contents(ctx, cred); + free(cred); + } else { + *_cred = cred; + } + + return kerr; +} + + +static errno_t handle_randomized(char *in) +{ + size_t ccname_len; + char *ccname = NULL; + int ret; + + /* We only treat the FILE type case in a special way due to the history + * of storing FILE type ccache in /tmp and associated security issues */ + if (in[0] == '/') { + ccname = in; + } else if (strncmp(in, "FILE:", 5) == 0) { + ccname = in + 5; + } else { + return EOK; + } + + ccname_len = strlen(ccname); + if (ccname_len >= 6 && strcmp(ccname + (ccname_len - 6), "XXXXXX") == 0) { + /* NOTE: this call is only used to create a unique name, as later + * krb5_cc_initialize() will unlink and recreate the file. + * This is ok because this part of the code is called with + * privileges already dropped when handling user ccache, or the ccache + * is stored in a private directory. So we do not have huge issues if + * something races, we mostly care only about not accidentally use + * an existing name and thus failing in the process of saving the + * cache. Malicious races can only be avoided by libkrb5 itself. */ + ret = sss_unique_filename(NULL, ccname); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "mkstemp(\"%s\") failed [%d]: %s!\n", + ccname, ret, strerror(ret)); + return ret; + } + } + + return EOK; +} + +/* NOTE: callers rely on 'name' being *changed* if it needs to be randomized, + * as they will then send the name back to the new name via the return call + * k5c_attach_ccname_msg(). Callers will send in a copy of the name if they + * do not care for changes. */ +static krb5_error_code create_ccache(char *ccname, krb5_creds *creds) +{ + krb5_context kctx = NULL; + krb5_ccache kcc = NULL; + const char *type; + krb5_error_code kerr; +#ifdef HAVE_KRB5_CC_COLLECTION + krb5_ccache cckcc; + bool switch_to_cc = false; +#endif + + /* Set a restrictive umask, just in case we end up creating any file or a + * directory. */ + if (strncmp(ccname, "DIR:", 4) == 0) { + umask(SSS_DFL_X_UMASK); + } else { + umask(SSS_DFL_UMASK); + } + + /* we create a new context here as the main process one may have been + * opened as root and contain possibly references (even open handles?) + * to resources we do not have or do not want to have access to */ + kerr = krb5_init_context(&kctx); + if (kerr) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return ERR_INTERNAL; + } + + kerr = handle_randomized(ccname); + if (kerr) { + DEBUG(SSSDBG_CRIT_FAILURE, "handle_randomized failed: %d\n", kerr); + goto done; + } + + kerr = krb5_cc_resolve(kctx, ccname, &kcc); + if (kerr) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + type = krb5_cc_get_type(kctx, kcc); + DEBUG(SSSDBG_TRACE_ALL, "Initializing ccache of type [%s]\n", type); + +#ifdef HAVE_KRB5_CC_COLLECTION + if (krb5_cc_support_switch(kctx, type)) { + DEBUG(SSSDBG_TRACE_ALL, "CC supports switch\n"); + kerr = krb5_cc_set_default_name(kctx, ccname); + if (kerr) { + DEBUG(SSSDBG_TRACE_ALL, "Cannot set default name!\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + kerr = krb5_cc_cache_match(kctx, creds->client, &cckcc); + if (kerr == KRB5_CC_NOTFOUND) { + DEBUG(SSSDBG_TRACE_ALL, "Match not found\n"); + kerr = krb5_cc_new_unique(kctx, type, NULL, &cckcc); + switch_to_cc = true; + } + if (kerr) { + DEBUG(SSSDBG_TRACE_ALL, "krb5_cc_cache_match failed\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + krb5_cc_close(kctx, kcc); + kcc = cckcc; + } +#endif + + kerr = krb5_cc_initialize(kctx, kcc, creds->client); + if (kerr) { + DEBUG(SSSDBG_TRACE_ALL, "krb5_cc_initialize failed\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + kerr = krb5_cc_store_cred(kctx, kcc, creds); + if (kerr) { + DEBUG(SSSDBG_TRACE_ALL, "krb5_cc_store_cred failed\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + +#ifdef HAVE_KRB5_CC_COLLECTION + if (switch_to_cc) { + DEBUG(SSSDBG_TRACE_ALL, "switch_to_cc\n"); + kerr = krb5_cc_switch(kctx, kcc); + if (kerr) { + DEBUG(SSSDBG_TRACE_ALL, "krb5_cc_switch\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + } +#endif + + DEBUG(SSSDBG_TRACE_ALL, "returning: %d\n", kerr); +done: + if (kcc) { + /* FIXME: should we krb5_cc_destroy in case of error? */ + krb5_cc_close(kctx, kcc); + } + + krb5_free_context(kctx); + + return kerr; +} + +static errno_t pack_response_packet(TALLOC_CTX *mem_ctx, errno_t error, + struct response_data *resp_list, + uint8_t **_buf, size_t *_len) +{ + uint8_t *buf; + size_t size = 0; + size_t p = 0; + struct response_data *pdr; + + /* A buffer with the following structure must be created: + * int32_t status of the request (required) + * message (zero or more) + * + * A message consists of: + * int32_t type of the message + * int32_t length of the following data + * uint8_t[len] data + */ + + size = sizeof(int32_t); + + for (pdr = resp_list; pdr != NULL; pdr = pdr->next) { + size += 2*sizeof(int32_t) + pdr->len; + } + + buf = talloc_array(mem_ctx, uint8_t, size); + if (!buf) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_array failed\n"); + return ENOMEM; + } + + SAFEALIGN_SET_INT32(&buf[p], error, &p); + + for (pdr = resp_list; pdr != NULL; pdr = pdr->next) { + SAFEALIGN_SET_INT32(&buf[p], pdr->type, &p); + SAFEALIGN_SET_INT32(&buf[p], pdr->len, &p); + safealign_memcpy(&buf[p], pdr->data, pdr->len, &p); + } + + DEBUG(SSSDBG_TRACE_INTERNAL, "response packet size: [%zu]\n", p); + + *_buf = buf; + *_len = p; + return EOK; +} + +static errno_t k5c_attach_otp_info_msg(struct krb5_req *kr) +{ + uint8_t *msg = NULL; + size_t msg_len; + int ret; + size_t vendor_len = 0; + size_t token_id_len = 0; + size_t challenge_len = 0; + size_t idx = 0; + + msg_len = 3; + if (kr->otp_vendor != NULL) { + vendor_len = strlen(kr->otp_vendor); + msg_len += vendor_len; + } + + if (kr->otp_token_id != NULL) { + token_id_len = strlen(kr->otp_token_id); + msg_len += token_id_len; + } + + if (kr->otp_challenge != NULL) { + challenge_len = strlen(kr->otp_challenge); + msg_len += challenge_len; + } + + msg = talloc_zero_size(kr, msg_len); + if (msg == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_size failed.\n"); + return ENOMEM; + } + + if (kr->otp_vendor != NULL) { + memcpy(msg, kr->otp_vendor, vendor_len); + } + idx += vendor_len +1; + + if (kr->otp_token_id != NULL) { + memcpy(msg + idx, kr->otp_token_id, token_id_len); + } + idx += token_id_len +1; + + if (kr->otp_challenge != NULL) { + memcpy(msg + idx, kr->otp_challenge, challenge_len); + } + + ret = pam_add_response(kr->pd, SSS_PAM_OTP_INFO, msg_len, msg); + talloc_zfree(msg); + + return ret; +} + +static errno_t k5c_attach_oauth2_info_msg(struct krb5_req *kr, + struct sss_idp_oauth2 *data) +{ + uint8_t *msg; + const char *curi; + size_t msg_len; + size_t uri_len = 0; + size_t curi_len = 0; + size_t user_code_len = 0; + size_t idx = 0; + errno_t ret; + + if (data->verification_uri == NULL || data->user_code == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Empty oauth2 verification_uri or user_code\n"); + return EINVAL; + } + + msg_len = 0; + + uri_len = strlen(data->verification_uri) + 1; + msg_len += uri_len; + + if (data->verification_uri_complete != NULL) { + curi = data->verification_uri_complete; + curi_len = strlen(curi) + 1; + } else { + curi = ""; + curi_len = 1; + } + msg_len += curi_len; + + user_code_len = strlen(data->user_code) + 1; + msg_len += user_code_len; + + msg = talloc_zero_size(NULL, msg_len); + if (msg == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_size failed.\n"); + return ENOMEM; + } + + memcpy(msg, data->verification_uri, uri_len); + idx += uri_len; + + memcpy(msg + idx, curi, curi_len); + idx += curi_len; + + memcpy(msg + idx, data->user_code, user_code_len); + + ret = pam_add_response(kr->pd, SSS_PAM_OAUTH2_INFO, msg_len, msg); + talloc_zfree(msg); + + return ret; +} + + +static errno_t k5c_attach_keep_alive_msg(struct krb5_req *kr) +{ + uint8_t *msg; + pid_t pid; + int ret; + + pid = getpid(); + + msg = talloc_memdup(kr, &pid, sizeof(pid_t)); + if (msg == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_size failed.\n"); + return ENOMEM; + } + + /* Indicate that the krb5 child must be kept alive to continue + * authentication with correct internal state of Kerberos API. + * + * Further communication must be done against the same child process */ + ret = pam_add_response(kr->pd, SSS_CHILD_KEEP_ALIVE, sizeof(pid_t), msg); + talloc_zfree(msg); + + return ret; +} + +static errno_t k5c_attach_ccname_msg(struct krb5_req *kr) +{ + char *msg = NULL; + int ret; + + if (kr->ccname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Error obtaining ccname.\n"); + return ERR_INTERNAL; + } + + msg = talloc_asprintf(kr, "%s=%s",CCACHE_ENV_NAME, kr->ccname); + if (msg == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + return ENOMEM; + } + + ret = pam_add_response(kr->pd, SSS_PAM_ENV_ITEM, + strlen(msg) + 1, (uint8_t *)msg); + talloc_zfree(msg); + + return ret; +} + +static errno_t k5c_send_data(struct krb5_req *kr, int fd, errno_t error) +{ + ssize_t written; + uint8_t *buf; + size_t len; + int ret; + + DEBUG(SSSDBG_FUNC_DATA, "Received error code %d\n", error); + + ret = pack_response_packet(kr, error, kr->pd->resp_list, &buf, &len); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pack_response_packet failed.\n"); + return ret; + } + + errno = 0; + written = sss_atomic_write_safe_s(fd, buf, len); + if (written == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "write failed [%d][%s].\n", ret, strerror(ret)); + return ret; + } + + if (written != len) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Write error, wrote [%zu] bytes, expected [%zu]\n", + written, len); + return EOK; + } + + DEBUG(SSSDBG_TRACE_ALL, "Response sent.\n"); + + return EOK; +} + +static errno_t get_pkinit_identity(TALLOC_CTX *mem_ctx, + struct sss_auth_token *authtok, + char **_identity) +{ + int ret; + char *identity; + const char *token_name; + const char *module_name; + const char *key_id; + const char *label; + + ret = sss_authtok_get_sc(authtok, NULL, NULL, + &token_name, NULL, + &module_name, NULL, + &key_id, NULL, &label, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_authtok_get_sc failed.\n"); + return ret; + } + + DEBUG(SSSDBG_TRACE_ALL, "Got [%s][%s].\n", token_name, module_name); + + if (module_name == NULL || *module_name == '\0') { + module_name = "p11-kit-proxy.so"; + } + + identity = talloc_asprintf(mem_ctx, "PKCS11:module_name=%s", module_name); + if (identity == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + return ENOMEM; + } + + if (token_name != NULL && *token_name != '\0') { + identity = talloc_asprintf_append(identity, ":token=%s", + token_name); + if (identity == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "talloc_asprintf_append failed.\n"); + return ENOMEM; + } + } + + if (key_id != NULL && *key_id != '\0') { + identity = talloc_asprintf_append(identity, ":certid=%s", key_id); + if (identity == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "talloc_asprintf_append failed.\n"); + return ENOMEM; + } + } + + if (label != NULL && *label != '\0') { + identity = talloc_asprintf_append(identity, ":certlabel=%s", label); + if (identity == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "talloc_asprintf_append failed.\n"); + return ENOMEM; + } + } + + *_identity = identity; + + DEBUG(SSSDBG_TRACE_ALL, "Using pkinit identity [%s].\n", identity); + + return EOK; +} + +static errno_t add_ticket_times_and_upn_to_response(struct krb5_req *kr) +{ + int ret; + int64_t t[4]; + krb5_error_code kerr; + char *upn = NULL; + unsigned int upn_len = 0; + + t[0] = (int64_t) kr->creds->times.authtime; + t[1] = (int64_t) kr->creds->times.starttime; + t[2] = (int64_t) kr->creds->times.endtime; + t[3] = (int64_t) kr->creds->times.renew_till; + + ret = pam_add_response(kr->pd, SSS_KRB5_INFO_TGT_LIFETIME, + 4*sizeof(int64_t), (uint8_t *) t); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + goto done; + } + + kerr = krb5_unparse_name_ext(kr->ctx, kr->creds->client, &upn, &upn_len); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "krb5_unparse_name_ext failed.\n"); + goto done; + } + + ret = pam_add_response(kr->pd, SSS_KRB5_INFO_UPN, upn_len, + (uint8_t *) upn); + krb5_free_unparsed_name(kr->ctx, upn); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + goto done; + } + +done: + return ret; +} + +static krb5_error_code validate_tgt(struct krb5_req *kr) +{ + krb5_error_code kerr; + krb5_error_code kt_err; + char *principal = NULL; + krb5_keytab keytab; + krb5_kt_cursor cursor; + krb5_keytab_entry entry; + krb5_verify_init_creds_opt opt; + krb5_principal validation_princ = NULL; + bool realm_entry_found = false; + krb5_ccache validation_ccache = NULL; + krb5_authdata **pac_authdata = NULL; + + memset(&keytab, 0, sizeof(keytab)); + kerr = krb5_kt_resolve(kr->ctx, kr->keytab, &keytab); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "error resolving keytab [%s], " \ + "not verifying TGT.\n", kr->keytab); + return kerr; + } + + memset(&cursor, 0, sizeof(cursor)); + kerr = krb5_kt_start_seq_get(kr->ctx, keytab, &cursor); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "error reading keytab [%s], " \ + "not verifying TGT.\n", kr->keytab); + krb5_kt_close(kr->ctx, keytab); + return kerr; + } + + /* We look for the first entry from our realm or take the last one */ + memset(&entry, 0, sizeof(entry)); + while ((kt_err = krb5_kt_next_entry(kr->ctx, keytab, &entry, &cursor)) == 0) { + if (validation_princ != NULL) { + krb5_free_principal(kr->ctx, validation_princ); + validation_princ = NULL; + } + kerr = krb5_copy_principal(kr->ctx, entry.principal, + &validation_princ); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_copy_principal failed.\n"); + krb5_kt_end_seq_get(kr->ctx, keytab, &cursor); + goto done; + } + + kerr = sss_krb5_free_keytab_entry_contents(kr->ctx, &entry); + if (kerr != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, "Failed to free keytab entry.\n"); + } + memset(&entry, 0, sizeof(entry)); + + if (krb5_realm_compare(kr->ctx, validation_princ, kr->creds->client)) { + DEBUG(SSSDBG_TRACE_INTERNAL, + "Found keytab entry with the realm of the credential.\n"); + realm_entry_found = true; + break; + } + } + + if (!realm_entry_found) { + DEBUG(SSSDBG_TRACE_INTERNAL, + "Keytab entry with the realm of the credential not found " + "in keytab. Using the last entry.\n"); + } + + /* Close the keytab here. Even though we're using cursors, the file + * handle is stored in the krb5_keytab structure, and it gets + * overwritten when the verify_init_creds() call below creates its own + * cursor, creating a leak. */ + kerr = krb5_kt_end_seq_get(kr->ctx, keytab, &cursor); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_kt_end_seq_get failed, " \ + "not verifying TGT.\n"); + goto done; + } + + /* check if we got any errors from krb5_kt_next_entry */ + if (kt_err != 0 && kt_err != KRB5_KT_END) { + DEBUG(SSSDBG_CRIT_FAILURE, "error reading keytab [%s], " \ + "not verifying TGT.\n", kr->keytab); + goto done; + } + + /* Get the principal to which the key belongs, for logging purposes. */ + principal = NULL; + kerr = krb5_unparse_name(kr->ctx, validation_princ, &principal); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "internal error parsing principal name, " + "not verifying TGT.\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + + krb5_verify_init_creds_opt_init(&opt); + krb5_verify_init_creds_opt_set_ap_req_nofail(&opt, TRUE); + kerr = krb5_verify_init_creds(kr->ctx, kr->creds, validation_princ, keytab, + &validation_ccache, &opt); + + if (kerr == 0) { + DEBUG(SSSDBG_TRACE_FUNC, "TGT verified using key for [%s].\n", + principal); + } else { + DEBUG(SSSDBG_CRIT_FAILURE ,"TGT failed verification using key " \ + "for [%s].\n", principal); + goto done; + } + + /* Try to find and send the PAC to the PAC responder. + * Failures are not critical. */ + if (kr->send_pac || kr->cli_opts->check_pac_flags != 0) { + kerr = sss_extract_pac(kr->ctx, validation_ccache, validation_princ, + kr->creds->client, keytab, + kr->cli_opts->check_pac_flags, &pac_authdata); + if (kerr != 0) { + if (kerr == ERR_CHECK_PAC_FAILED) { + DEBUG(SSSDBG_CRIT_FAILURE, + "PAC check failed for principal [%s].\n", kr->name); + goto done; + } + DEBUG(SSSDBG_OP_FAILURE, "sss_extract_and_send_pac failed, group " \ + "membership for user with principal [%s] " \ + "might not be correct.\n", kr->name); + kerr = 0; + goto done; + } + } + + if (kr->send_pac) { + if(unsetenv("_SSS_LOOPS") != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to unset _SSS_LOOPS, " + "sss_pac_make_request will most certainly fail.\n"); + } + + kerr = sss_send_pac(pac_authdata); + + if(setenv("_SSS_LOOPS", "NO", 0) != 0) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set _SSS_LOOPS.\n"); + } + + if (kerr != 0) { + if (kerr == ERR_CHECK_PAC_FAILED) { + DEBUG(SSSDBG_CRIT_FAILURE, + "PAC for principal [%s] is not valid.\n", kr->name); + goto done; + } + if (kr->cli_opts->check_pac_flags != 0) { + DEBUG(SSSDBG_IMPORTANT_INFO, + "pac_check is set but PAC responder is not running, " + "failed to properly validate PAC, ignored, " + "authentication for [%s] can proceed.\n", kr->name); + } + DEBUG(SSSDBG_OP_FAILURE, "sss_send_pac failed, group " \ + "membership for user with principal [%s] " \ + "might not be correct.\n", kr->name); + kerr = 0; + } + } + +done: + krb5_free_authdata(kr->ctx, pac_authdata); + if (validation_ccache != NULL) { + krb5_cc_destroy(kr->ctx, validation_ccache); + } + + if (krb5_kt_close(kr->ctx, keytab) != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, "krb5_kt_close failed\n"); + } + if (validation_princ != NULL) { + krb5_free_principal(kr->ctx, validation_princ); + } + if (principal != NULL) { + sss_krb5_free_unparsed_name(kr->ctx, principal); + } + + return kerr; + +} + +static krb5_error_code get_and_save_tgt_with_keytab(krb5_context ctx, + struct cli_opts *cli_opts, + krb5_principal princ, + krb5_keytab keytab, + char *ccname) +{ + krb5_error_code kerr = 0; + krb5_creds creds; + krb5_get_init_creds_opt options; + + memset(&creds, 0, sizeof(creds)); + memset(&options, 0, sizeof(options)); + + krb5_get_init_creds_opt_set_address_list(&options, NULL); + krb5_get_init_creds_opt_set_forwardable(&options, 0); + krb5_get_init_creds_opt_set_proxiable(&options, 0); + set_canonicalize_option(cli_opts, &options); + + kerr = krb5_get_init_creds_keytab(ctx, &creds, princ, keytab, 0, NULL, + &options); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + /* Use the updated principal in the creds in case canonicalized */ + kerr = create_ccache(ccname, &creds); + if (kerr != 0) { + goto done; + } + kerr = 0; + +done: + krb5_free_cred_contents(ctx, &creds); + + return kerr; + +} + +/* [MS-KILE]: Kerberos Protocol Extensions + * https://msdn.microsoft.com/en-us/library/cc233855.aspx + * http://download.microsoft.com/download/9/5/E/95EF66AF-9026-4BB0-A41D-A4F81802D92C/%5BMS-KILE%5D.pdf + * 2.2.1 KERB-EXT-ERROR + */ +bool have_ms_kile_ext_error(unsigned char *data, unsigned int length, + uint32_t *_ntstatus) +{ + /* [MS-KILE] 2.2.2 KERB-ERROR-DATA + * Kerberos V5 messages are defined using Abstract Syntax Notation One + * (ASN.1) + * KERB-ERROR-DATA ::= SEQUENCE { + * data-type [1] INTEGER, + * data-value [2] OCTET STRING OPTIONAL + * } + * We are interested in data-type 3 KERB_ERR_TYPE_EXTENDED + */ + uint8_t kile_asn1_begining[] = { + 0x30, 0x15, /* 0x30 is SEQUENCE, 0x15 length */ + 0xA1, 0x03, /* 0xA1 is 1st element of sequence, 0x03 length */ + 0x02, 0x01, 0x03, /* 0x02 is INTEGER, 0x01 length, 0x03 value */ + 0xA2, 0x0E, /* 0xA2 is 2nd element of sequence, 0x0E length */ + 0x04, 0x0C, /* 0x04 is OCTET STRING, 0x0C length (12 bytes) */ + }; + const size_t offset = sizeof(kile_asn1_begining); + uint32_t value; + + if (length != 23 || data == NULL) { + return false; + } + + if (memcmp(data, kile_asn1_begining, offset) != 0) { + return false; + } + + /* [MS-KILE] 2.2.1 KERB-EXT-ERROR + * typedef struct KERB_EXT_ERROR { + * unsigned long status; + * unsigned long reserved; + * unsigned long flags; + * } KERB_EXT_ERROR; + * Status: An NTSTATUS value. See [MS-ERREF] section 2.3. + */ + value = data[offset + 3] << 24 + | data[offset + 2] << 16 + | data[offset + 1] << 8 + | data[offset + 0]; + + *_ntstatus = value; + return true; +} + +/* Following NTSTATUS values are from: + * [MS-ERREF]: Windows Error Codes -> Section 2.3.1 + * https://msdn.microsoft.com/en-us/library/cc231196.aspx + * http://download.microsoft.com/download/9/5/E/95EF66AF-9026-4BB0-A41D-A4F81802D92C/%5BMS-ERREF%5D.pdf + */ +#define NT_STATUS_ACCOUNT_EXPIRED 0xC0000193 +#define NT_STATUS_ACCOUNT_DISABLED 0xC0000072 + +void check_ms_kile_ext_krb5err(krb5_context context, + krb5_init_creds_context init_cred_ctx, + krb5_error_code *_kerr) +{ + krb5_error_code err; + krb5_error *error = NULL; + uint32_t ntstatus; + + err = krb5_init_creds_get_error(context, init_cred_ctx, &error); + if (err != 0 || error == NULL) { + KRB5_CHILD_DEBUG(SSSDBG_TRACE_FUNC, err); + return; + } + + if (have_ms_kile_ext_error((unsigned char *)error->e_data.data, error->e_data.length, + &ntstatus)) { + switch (ntstatus) { + case NT_STATUS_ACCOUNT_EXPIRED: + *_kerr = KRB5KDC_ERR_NAME_EXP; + break; + case NT_STATUS_ACCOUNT_DISABLED: + *_kerr = KRB5KDC_ERR_CLIENT_REVOKED; + break; + } + } +} + +krb5_error_code +sss_krb5_get_init_creds_password(krb5_context context, krb5_creds *creds, + krb5_principal client, const char *password, + krb5_prompter_fct prompter, void *data, + krb5_deltat start_time, + const char *in_tkt_service, + krb5_get_init_creds_opt *k5_gic_options) +{ + krb5_error_code kerr; + krb5_init_creds_context init_cred_ctx = NULL; + + kerr = krb5_init_creds_init(context, client, prompter, data, + start_time, k5_gic_options, + &init_cred_ctx); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + if (password != NULL) { + kerr = krb5_init_creds_set_password(context, init_cred_ctx, password); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + } + + if (in_tkt_service != NULL) { + kerr = krb5_init_creds_set_service(context, init_cred_ctx, + in_tkt_service); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + } + + kerr = krb5_init_creds_get(context, init_cred_ctx); + if (kerr == KRB5KDC_ERR_CLIENT_REVOKED) { + check_ms_kile_ext_krb5err(context, init_cred_ctx, &kerr); + } + + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + kerr = krb5_init_creds_get_creds(context, init_cred_ctx, creds); + +done: + krb5_init_creds_free(context, init_cred_ctx); + return kerr; +} + +static krb5_error_code get_and_save_tgt(struct krb5_req *kr, + const char *password) +{ + const char *realm_name; + int realm_length; + krb5_error_code kerr; + char *cc_name; + int ret; + char *identity = NULL; + + kerr = sss_krb5_get_init_creds_opt_set_expire_callback(kr->ctx, kr->options, + sss_krb5_expire_callback_func, + kr); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to set expire callback, continue without.\n"); + } + + sss_krb5_princ_realm(kr->ctx, kr->princ, &realm_name, &realm_length); + if (realm_length == 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_krb5_princ_realm failed.\n"); + return KRB5KRB_ERR_GENERIC; + } + + if (sss_authtok_get_type(kr->pd->authtok) == SSS_AUTHTOK_TYPE_SC_PIN + || sss_authtok_get_type(kr->pd->authtok) + == SSS_AUTHTOK_TYPE_SC_KEYPAD) { + DEBUG(SSSDBG_TRACE_ALL, + "Found Smartcard credentials, trying pkinit.\n"); + + ret = get_pkinit_identity(kr, kr->pd->authtok, &identity); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "get_pkinit_identity failed.\n"); + return ret; + } + + kerr = krb5_get_init_creds_opt_set_pa(kr->ctx, kr->options, + "X509_user_identity", identity); + talloc_free(identity); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "krb5_get_init_creds_opt_set_pa failed.\n"); + return kerr; + } + + /* TODO: Maybe X509_anchors should be added here as well */ + } + + DEBUG(SSSDBG_TRACE_FUNC, + "Attempting kinit for realm [%s]\n",realm_name); + kerr = kr->krb5_get_init_creds_password(kr->ctx, kr->creds, kr->princ, + password_or_responder(password), + sss_krb5_prompter, kr, 0, NULL, + kr->options); + if (kr->pd->cmd == SSS_PAM_PREAUTH && kerr != KRB5KDC_ERR_KEY_EXP) { + /* Any errors except KRB5KDC_ERR_KEY_EXP are ignored during pre-auth, + * only data is collected to be send back to the client. + * KRB5KDC_ERR_KEY_EXP must be handled separately to figure out the + * possible authentication methods to update the password. */ + DEBUG(SSSDBG_TRACE_FUNC, + "krb5_get_init_creds_password returned [%d] during pre-auth.\n", + kerr); + return 0; + } else { + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + + /* Special case for IPA password migration */ + if (kr->pd->cmd == SSS_PAM_AUTHENTICATE + && kerr == KRB5_PREAUTH_FAILED + && kr->pkinit_prompting == false + && kr->password_prompting == false + && kr->otp == false + && sss_authtok_get_type(kr->pd->authtok) + == SSS_AUTHTOK_TYPE_PASSWORD) { + return ERR_CREDS_INVALID; + } + + /* If during authentication either the MIT Kerberos pkinit + * pre-auth module is missing or no Smartcard is inserted and only + * pkinit is available KRB5_PREAUTH_FAILED is returned. + * ERR_NO_AUTH_METHOD_AVAILABLE is used to indicate to the + * frontend that local authentication might be tried. + * Same is true if Smartcard credentials are given but only other + * authentication methods are available. */ + if (kr->pd->cmd == SSS_PAM_AUTHENTICATE + && kerr == KRB5_PREAUTH_FAILED + && kr->pkinit_prompting == false + && (( kr->password_prompting == false + && kr->otp == false) + || ((kr->otp == true + || kr->password_prompting == true) + && IS_SC_AUTHTOK(kr->pd->authtok))) ) { + return ERR_NO_AUTH_METHOD_AVAILABLE; + } + return kerr; + } + } + + if (kr->validate) { + kerr = validate_tgt(kr); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + } else { + DEBUG(SSSDBG_CONF_SETTINGS, "TGT validation is disabled.\n"); + } + + /* In a non-POSIX environment, we only care about the return code from + * krb5_child, so let's not even attempt to create the ccache + */ + if (kr->posix_domain == false) { + DEBUG(SSSDBG_TRACE_LIBS, + "Finished authentication in a non-POSIX domain\n"); + goto done; + } + + kerr = restore_creds(kr->pcsc_saved_creds); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "restore_creds failed.\n"); + } + /* Make sure ccache is created and written as the user */ + if (geteuid() != kr->uid || getegid() != kr->gid) { + kerr = k5c_become_user(kr->uid, kr->gid, kr->posix_domain); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "become_user failed.\n"); + goto done; + } + } + + DEBUG(SSSDBG_TRACE_INTERNAL, + "Running as [%"SPRIuid"][%"SPRIgid"].\n", geteuid(), getegid()); + + /* If kr->ccname is cache collection (DIR:/...), we want to work + * directly with file ccache (DIR::/...), but cache collection + * should be returned back to back end. + */ + cc_name = sss_get_ccache_name_for_principal(kr->pd, kr->ctx, + kr->creds->client, + kr->ccname); + if (cc_name == NULL) { + cc_name = kr->ccname; + } + + /* Use the updated principal in the creds in case canonicalized */ + kerr = create_ccache(cc_name, kr->creds); + if (kerr != 0) { + goto done; + } + + /* Successful authentication! Check if ccache contains the + * right principal... + */ + kerr = sss_krb5_check_ccache_princ(kr->ctx, kr->ccname, kr->creds->client); + if (kerr) { + DEBUG(SSSDBG_CRIT_FAILURE, + "No ccache for %s in %s?\n", kr->upn, kr->ccname); + goto done; + } + + kerr = safe_remove_old_ccache_file(kr->old_ccname, kr->ccname, + kr->uid, kr->gid); + if (kerr != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to remove old ccache file [%s], " + "please remove it manually.\n", kr->old_ccname); + } + + kerr = add_ticket_times_and_upn_to_response(kr); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "add_ticket_times_and_upn_to_response failed.\n"); + } + + kerr = 0; + +done: + krb5_free_cred_contents(kr->ctx, kr->creds); + + return kerr; + +} + +static errno_t map_krb5_error(krb5_error_code kerr) +{ + /* just pass SSSD's internal error codes */ + if (kerr > 0 && IS_SSSD_ERROR(kerr)) { + DEBUG(SSSDBG_CRIT_FAILURE, "[%d][%s].\n", kerr, sss_strerror(kerr)); + return kerr; + } + + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + } + + switch (kerr) { + case 0: + return ERR_OK; + + case KRB5_LIBOS_CANTREADPWD: + return ERR_NO_CREDS; + + case KRB5_KDCREP_SKEW: + case KRB5KRB_AP_ERR_SKEW: + case KRB5KRB_AP_ERR_TKT_EXPIRED: + case KRB5KRB_AP_ERR_TKT_NYV: + case KRB5_KDC_UNREACH: + case KRB5_REALM_CANT_RESOLVE: + case KRB5_REALM_UNKNOWN: + return ERR_NETWORK_IO; + + case KRB5KDC_ERR_CLIENT_REVOKED: + return ERR_ACCOUNT_LOCKED; + + case KRB5KDC_ERR_NAME_EXP: + return ERR_ACCOUNT_EXPIRED; + + case KRB5KDC_ERR_KEY_EXP: + return ERR_CREDS_EXPIRED; + + case KRB5KRB_AP_ERR_BAD_INTEGRITY: + return ERR_AUTH_FAILED; + + /* ERR_CREDS_INVALID is used to indicate to the IPA provider that trying + * password migration would make sense. All Kerberos error codes which can + * be seen while migrating LDAP users to IPA should be added here. */ + case KRB5_PROG_ETYPE_NOSUPP: + case KRB5_PREAUTH_FAILED: + case KRB5KDC_ERR_PREAUTH_FAILED: + return ERR_CREDS_INVALID; + + /* Please do not remove KRB5KRB_ERR_GENERIC here, it is a _generic_ error + * code and we cannot make any assumptions about the reason for the error. + * As a consequence we cannot return a different error code than a generic + * one which unfortunately might result in a unspecific system error + * message to the user. + * + * If there are cases where libkrb5 calls return KRB5KRB_ERR_GENERIC where + * SSSD should behave differently this has to be detected by different + * means, e.g. by evaluation error messages, and then the error code + * should be changed to a more suitable KRB5* error code or immediately to + * an SSSD ERR_* error code to avoid the default handling here. */ + case KRB5KRB_ERR_GENERIC: + default: + return ERR_INTERNAL; + } +} + +static errno_t changepw_child(struct krb5_req *kr, bool prelim) +{ + int ret; + krb5_error_code kerr = 0; + const char *password = NULL; + const char *newpassword = NULL; + int result_code = -1; + krb5_data result_code_string; + krb5_data result_string; + char *user_error_message = NULL; + size_t user_resp_len; + uint8_t *user_resp; + krb5_prompter_fct prompter = NULL; + const char *realm_name; + int realm_length; + size_t msg_len; + uint8_t *msg; + uint32_t user_info_type; + + DEBUG(SSSDBG_TRACE_LIBS, "Password change operation\n"); + + if (sss_authtok_get_type(kr->pd->authtok) == SSS_AUTHTOK_TYPE_PASSWORD) { + ret = sss_authtok_get_password(kr->pd->authtok, &password, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to fetch current password [%d] %s.\n", + ret, strerror(ret)); + return ERR_NO_CREDS; + } + } + + if (!prelim) { + /* We do not need a password expiration warning here. */ + prompter = sss_krb5_prompter; + } + + set_changepw_options(kr->options); + sss_krb5_princ_realm(kr->ctx, kr->princ, &realm_name, &realm_length); + if (realm_length == 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_krb5_princ_realm failed.\n"); + return ERR_INTERNAL; + } + + DEBUG(SSSDBG_TRACE_FUNC, + "Attempting kinit for realm [%s]\n",realm_name); + kerr = kr->krb5_get_init_creds_password(kr->ctx, kr->creds, kr->princ, + password_or_responder(password), + prompter, kr, 0, + SSSD_KRB5_CHANGEPW_PRINCIPAL, + kr->options); + DEBUG(SSSDBG_TRACE_INTERNAL, + "chpass is%s using OTP\n", kr->otp ? "" : " not"); + if (kerr != 0) { + ret = pack_user_info_chpass_error(kr->pd, "Old password not accepted.", + &msg_len, &msg); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "pack_user_info_chpass_error failed [%d]\n", ret); + } else { + ret = pam_add_response(kr->pd, SSS_PAM_USER_INFO, msg_len, + msg); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + } + } + return kerr; + } + + sss_authtok_set_empty(kr->pd->authtok); + + if (prelim) { + DEBUG(SSSDBG_TRACE_LIBS, + "Initial authentication for change password operation " + "successful.\n"); + krb5_free_cred_contents(kr->ctx, kr->creds); + return EOK; + } + + ret = sss_authtok_get_password(kr->pd->newauthtok, &newpassword, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to fetch new password [%d] %s.\n", + ret, strerror(ret)); + return ERR_NO_CREDS; + } + + memset(&result_code_string, 0, sizeof(krb5_data)); + memset(&result_string, 0, sizeof(krb5_data)); + kerr = krb5_change_password(kr->ctx, kr->creds, + discard_const(newpassword), &result_code, + &result_code_string, &result_string); + + if (kerr == KRB5_KDC_UNREACH) { + return ERR_NETWORK_IO; + } + + if (kerr != 0 || result_code != 0) { + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + } + + if (result_code_string.length > 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "krb5_change_password failed [%d][%.*s].\n", result_code, + result_code_string.length, result_code_string.data); + user_error_message = talloc_strndup(kr->pd, result_code_string.data, + result_code_string.length); + if (user_error_message == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strndup failed.\n"); + } + } + + if (result_string.length > 0 && result_string.data[0] != '\0') { + DEBUG(SSSDBG_CRIT_FAILURE, + "krb5_change_password failed [%d][%.*s].\n", result_code, + result_string.length, result_string.data); + talloc_free(user_error_message); + user_error_message = talloc_strndup(kr->pd, result_string.data, + result_string.length); + if (user_error_message == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strndup failed.\n"); + } + } else if (result_code == KRB5_KPASSWD_SOFTERROR) { + user_error_message = talloc_strdup(kr->pd, "Please make sure the " + "password meets the complexity constraints."); + if (user_error_message == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strndup failed.\n"); + } + } + + if (user_error_message != NULL) { + ret = pack_user_info_chpass_error(kr->pd, user_error_message, + &user_resp_len, &user_resp); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "pack_user_info_chpass_error failed [%d]\n", ret); + } else { + ret = pam_add_response(kr->pd, SSS_PAM_USER_INFO, user_resp_len, + user_resp); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + } + } + } + + return ERR_CHPASS_FAILED; + } + + krb5_free_cred_contents(kr->ctx, kr->creds); + + if (kr->otp == true) { + user_info_type = SSS_PAM_USER_INFO_OTP_CHPASS; + ret = pam_add_response(kr->pd, SSS_PAM_USER_INFO, sizeof(uint32_t), + (const uint8_t *) &user_info_type); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + /* Not fatal */ + } + + sss_authtok_set_empty(kr->pd->newauthtok); + return map_krb5_error(kerr); + } + + /* We changed some of the GIC options for the password change, now we have + * to change them back to get a fresh TGT. */ + revert_changepw_options(kr->cli_opts, kr->options); + + ret = sss_authtok_set_password(kr->pd->authtok, newpassword, 0); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to set password for fresh TGT.\n"); + return ret; + } + + kerr = get_and_save_tgt(kr, newpassword); + + sss_authtok_set_empty(kr->pd->authtok); + sss_authtok_set_empty(kr->pd->newauthtok); + + if (kerr == 0) { + kerr = k5c_attach_ccname_msg(kr); + } + return map_krb5_error(kerr); +} + +static errno_t pam_add_prompting(struct krb5_req *kr) +{ + int ret; + + /* add OTP tokeninfo message if available */ + if (kr->otp) { + ret = k5c_attach_otp_info_msg(kr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "k5c_attach_otp_info_msg failed.\n"); + return ret; + } + } + + if (kr->password_prompting) { + ret = pam_add_response(kr->pd, SSS_PASSWORD_PROMPTING, 0, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + return ret; + } + } + + if (kr->pkinit_prompting) { + ret = pam_add_response(kr->pd, SSS_CERT_AUTH_PROMPTING, 0, + NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + return ret; + } + } + + return EOK; +} + +static errno_t tgt_req_child(struct krb5_req *kr) +{ + const char *password = NULL; + krb5_error_code kerr; + int ret; + + DEBUG(SSSDBG_TRACE_LIBS, "Attempting to get a TGT\n"); + + /* No password is needed for pre-auth or if we have 2FA or SC */ + if (kr->pd->cmd != SSS_PAM_PREAUTH + && sss_authtok_get_type(kr->pd->authtok) != SSS_AUTHTOK_TYPE_2FA + && sss_authtok_get_type(kr->pd->authtok) != SSS_AUTHTOK_TYPE_2FA_SINGLE + && sss_authtok_get_type(kr->pd->authtok) != SSS_AUTHTOK_TYPE_SC_PIN + && sss_authtok_get_type(kr->pd->authtok) + != SSS_AUTHTOK_TYPE_SC_KEYPAD) { + ret = sss_authtok_get_password(kr->pd->authtok, &password, NULL); + switch (ret) { + case EOK: + break; + + case EACCES: + DEBUG(SSSDBG_OP_FAILURE, "Invalid authtok type\n"); + return ERR_INVALID_CRED_TYPE; + break; + + default: + DEBUG(SSSDBG_OP_FAILURE, "No credentials available\n"); + return ERR_NO_CREDS; + break; + } + } + + kerr = get_and_save_tgt(kr, password); + + if (kerr != KRB5KDC_ERR_KEY_EXP) { + if (kr->pd->cmd == SSS_PAM_PREAUTH) { + ret = pam_add_prompting(kr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_prompting failed.\n"); + goto done; + } + } else { + if (kerr == 0) { + kerr = k5c_attach_ccname_msg(kr); + } + } + ret = map_krb5_error(kerr); + goto done; + } + + /* If the password is expired, the KDC will always return + KRB5KDC_ERR_KEY_EXP regardless if the supplied password is correct or + not. In general the password can still be used to get a changepw ticket. + So we validate the password by trying to get a changepw ticket. */ + DEBUG(SSSDBG_TRACE_LIBS, "Password was expired\n"); + kerr = sss_krb5_get_init_creds_opt_set_expire_callback(kr->ctx, + kr->options, + NULL, NULL); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to unset expire callback, continue ...\n"); + } + + set_changepw_options(kr->options); + kerr = kr->krb5_get_init_creds_password(kr->ctx, kr->creds, kr->princ_orig, + password_or_responder(password), + sss_krb5_prompter, kr, 0, + SSSD_KRB5_CHANGEPW_PRINCIPAL, + kr->options); + + krb5_free_cred_contents(kr->ctx, kr->creds); + + if (kr->pd->cmd == SSS_PAM_PREAUTH) { + /* Any errors are ignored during pre-auth, only data is collected to + * be send back to the client. Even if the password is expired we + * should now know which authentication methods are available to + * update the password. */ + DEBUG(SSSDBG_TRACE_FUNC, + "krb5_get_init_creds_password returned [%d] during pre-auth, " + "ignored.\n", kerr); + ret = pam_add_prompting(kr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_prompting failed.\n"); + goto done; + } + goto done; + } + + if (kerr == 0) { + ret = ERR_CREDS_EXPIRED; + + /* If the password is expired, we can safely remove the ccache from the + * cache and disk if it is not actively used anymore. This will allow + * to create a new random ccache if sshd with privilege separation is + * used. */ + if (kr->old_cc_active == false && kr->old_ccname) { + ret = safe_remove_old_ccache_file(kr->old_ccname, NULL, + kr->uid, kr->gid); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to remove old ccache file [%s], " + "please remove it manually.\n", kr->old_ccname); + } + ret = ERR_CREDS_EXPIRED_CCACHE; + } + } else { + ret = map_krb5_error(kerr); + } + +done: + sss_authtok_set_empty(kr->pd->authtok); + return ret; +} + +static errno_t kuserok_child(struct krb5_req *kr) +{ + krb5_boolean access_allowed; + krb5_error_code kerr; + + DEBUG(SSSDBG_TRACE_LIBS, "Verifying if principal can log in as user\n"); + + /* krb5_kuserok tries to verify that kr->pd->user is a locally known + * account, so we have to unset _SSS_LOOPS to make getpwnam() work. */ + if (unsetenv("_SSS_LOOPS") != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to unset _SSS_LOOPS, " + "krb5_kuserok will most certainly fail.\n"); + } + + kerr = krb5_set_default_realm(kr->ctx, kr->realm); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_set_default_realm failed, " + "krb5_kuserok may fail.\n"); + } + + access_allowed = krb5_kuserok(kr->ctx, kr->princ, kr->pd->user); + DEBUG(SSSDBG_TRACE_LIBS, + "Access was %s\n", access_allowed ? "allowed" : "denied"); + + if (access_allowed) { + return EOK; + } + + return ERR_AUTH_DENIED; +} + +static errno_t renew_tgt_child(struct krb5_req *kr) +{ + const char *ccname; + krb5_ccache ccache = NULL; + krb5_error_code kerr; + int ret; + + DEBUG(SSSDBG_TRACE_LIBS, "Renewing a ticket\n"); + + ret = sss_authtok_get_ccfile(kr->pd->authtok, &ccname, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Unsupported authtok type for TGT renewal [%d].\n", + sss_authtok_get_type(kr->pd->authtok)); + return ERR_INVALID_CRED_TYPE; + } + + kerr = krb5_cc_resolve(kr->ctx, ccname, &ccache); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + kerr = krb5_get_renewed_creds(kr->ctx, kr->creds, kr->princ, ccache, NULL); + if (kerr != 0) { + goto done; + } + + if (kr->validate) { + kerr = validate_tgt(kr); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + } else { + DEBUG(SSSDBG_CONF_SETTINGS, "TGT validation is disabled.\n"); + } + + kerr = krb5_cc_initialize(kr->ctx, ccache, kr->princ); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + kerr = krb5_cc_store_cred(kr->ctx, ccache, kr->creds); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + goto done; + } + + kerr = add_ticket_times_and_upn_to_response(kr); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "add_ticket_times_and_upn_to_response failed.\n"); + } + + kerr = k5c_attach_ccname_msg(kr); + +done: + krb5_free_cred_contents(kr->ctx, kr->creds); + + if (ccache != NULL) { + krb5_cc_close(kr->ctx, ccache); + } + + if (kerr == KRB5KRB_AP_ERR_TKT_EXPIRED) { + DEBUG(SSSDBG_TRACE_LIBS, + "Attempted to renew an expired TGT, changing the error code " + "to expired creds internally\n"); + /* map_krb5_error() won't touch the SSSD-internal code */ + kerr = ERR_CREDS_EXPIRED; + } + + return map_krb5_error(kerr); +} + +static errno_t create_empty_ccache(struct krb5_req *kr) +{ + krb5_creds *creds = NULL; + krb5_error_code kerr; + + if (kr->old_cc_valid == false) { + DEBUG(SSSDBG_TRACE_LIBS, "Creating empty ccache\n"); + kerr = create_empty_cred(kr->ctx, kr->princ, &creds); + if (kerr == 0) { + kerr = create_ccache(kr->ccname, creds); + } + } else { + DEBUG(SSSDBG_TRACE_LIBS, "Existing ccache still valid, reusing\n"); + kerr = 0; + } + + if (kerr == 0) { + kerr = k5c_attach_ccname_msg(kr); + } + + krb5_free_creds(kr->ctx, creds); + + return map_krb5_error(kerr); +} + +static errno_t unpack_authtok(struct sss_auth_token *tok, + uint8_t *buf, size_t size, size_t *p) +{ + uint32_t auth_token_type; + uint32_t auth_token_length; + errno_t ret = EOK; + + SAFEALIGN_COPY_UINT32_CHECK(&auth_token_type, buf + *p, size, p); + SAFEALIGN_COPY_UINT32_CHECK(&auth_token_length, buf + *p, size, p); + if (auth_token_length > (size - *p)) { + return EINVAL; + } + switch (auth_token_type) { + case SSS_AUTHTOK_TYPE_EMPTY: + sss_authtok_set_empty(tok); + break; + case SSS_AUTHTOK_TYPE_PASSWORD: + ret = sss_authtok_set_password(tok, (char *)(buf + *p), 0); + break; + case SSS_AUTHTOK_TYPE_CCFILE: + ret = sss_authtok_set_ccfile(tok, (char *)(buf + *p), 0); + break; + case SSS_AUTHTOK_TYPE_2FA_SINGLE: + ret = sss_authtok_set_2fa_single(tok, (char *)(buf + *p), 0); + break; + case SSS_AUTHTOK_TYPE_2FA: + case SSS_AUTHTOK_TYPE_SC_PIN: + case SSS_AUTHTOK_TYPE_SC_KEYPAD: + case SSS_AUTHTOK_TYPE_OAUTH2: + case SSS_AUTHTOK_TYPE_PASSKEY: + case SSS_AUTHTOK_TYPE_PASSKEY_KRB: + case SSS_AUTHTOK_TYPE_PASSKEY_REPLY: + ret = sss_authtok_set(tok, auth_token_type, (buf + *p), + auth_token_length); + break; + default: + return EINVAL; + } + + if (ret == EOK) { + *p += auth_token_length; + } + return ret; +} + +static const char *krb5_child_command_to_str(int cmd) +{ + switch (cmd) { + case SSS_PAM_AUTHENTICATE: + return "auth"; + case SSS_PAM_CHAUTHTOK: + return "password change"; + case SSS_PAM_CHAUTHTOK_PRELIM: + return "password change checks"; + case SSS_PAM_ACCT_MGMT: + return "account management"; + case SSS_CMD_RENEW: + return "ticket renewal"; + case SSS_PAM_PREAUTH: + return "pre-auth"; + } + + DEBUG(SSSDBG_MINOR_FAILURE, "Unexpected command %d\n", cmd); + return "-unexpected-"; +} + +static errno_t unpack_buffer(uint8_t *buf, size_t size, + struct krb5_req *kr, uint32_t *offline) +{ + size_t p = 0; + uint32_t len; + uint32_t validate; + uint32_t posix_domain; + uint32_t send_pac; + uint32_t use_enterprise_princ; + struct pam_data *pd; + errno_t ret; + + DEBUG(SSSDBG_TRACE_LIBS, "total buffer size: [%zu]\n", size); + + if (!offline || !kr) return EINVAL; + + pd = create_pam_data(kr); + if (pd == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "create_pam_data failed.\n"); + return ENOMEM; + } + kr->pd = pd; + + SAFEALIGN_COPY_UINT32_CHECK(&pd->cmd, buf + p, size, &p); + SAFEALIGN_COPY_UINT32_CHECK(&kr->uid, buf + p, size, &p); + SAFEALIGN_COPY_UINT32_CHECK(&kr->gid, buf + p, size, &p); + SAFEALIGN_COPY_UINT32_CHECK(&validate, buf + p, size, &p); + kr->validate = (validate == 0) ? false : true; + SAFEALIGN_COPY_UINT32_CHECK(&posix_domain, buf + p, size, &p); + kr->posix_domain = (posix_domain == 0) ? false : true; + SAFEALIGN_COPY_UINT32_CHECK(offline, buf + p, size, &p); + SAFEALIGN_COPY_UINT32_CHECK(&send_pac, buf + p, size, &p); + kr->send_pac = (send_pac == 0) ? false : true; + SAFEALIGN_COPY_UINT32_CHECK(&use_enterprise_princ, buf + p, size, &p); + kr->use_enterprise_princ = (use_enterprise_princ == 0) ? false : true; + SAFEALIGN_COPY_UINT32_CHECK(&len, buf + p, size, &p); + if (len > size - p) return EINVAL; + kr->upn = talloc_strndup(kr, (char *)(buf + p), len); + if (kr->upn == NULL) return ENOMEM; + p += len; + + DEBUG(SSSDBG_CONF_SETTINGS, + "cmd [%d (%s)] uid [%llu] gid [%llu] validate [%s] " + "enterprise principal [%s] offline [%s] UPN [%s]\n", + pd->cmd, krb5_child_command_to_str(pd->cmd), + (unsigned long long) kr->uid, (unsigned long long) kr->gid, + kr->validate ? "true" : "false", + kr->use_enterprise_princ ? "true" : "false", + *offline ? "true" : "false", kr->upn ? kr->upn : "none"); + + if (pd->cmd == SSS_PAM_AUTHENTICATE || + pd->cmd == SSS_PAM_PREAUTH || + pd->cmd == SSS_CMD_RENEW || + pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM || pd->cmd == SSS_PAM_CHAUTHTOK) { + SAFEALIGN_COPY_UINT32_CHECK(&len, buf + p, size, &p); + if (len > size - p) return EINVAL; + kr->ccname = talloc_strndup(kr, (char *)(buf + p), len); + if (kr->ccname == NULL) return ENOMEM; + p += len; + + SAFEALIGN_COPY_UINT32_CHECK(&len, buf + p, size, &p); + if (len > size - p) return EINVAL; + + if (len > 0) { + kr->old_ccname = talloc_strndup(kr, (char *)(buf + p), len); + if (kr->old_ccname == NULL) return ENOMEM; + p += len; + } else { + DEBUG(SSSDBG_TRACE_INTERNAL, "No old ccache\n"); + } + + SAFEALIGN_COPY_UINT32_CHECK(&len, buf + p, size, &p); + if (len > size - p) return EINVAL; + + if (len > 0) { + kr->keytab = talloc_strndup(kr, (char *)(buf + p), len); + p += len; + } else { + kr->keytab = NULL; + } + + ret = unpack_authtok(pd->authtok, buf, size, &p); + if (ret) { + return ret; + } + + DEBUG(SSSDBG_CONF_SETTINGS, + "ccname: [%s] old_ccname: [%s] keytab: [%s]\n", + kr->ccname, + kr->old_ccname ? kr->old_ccname : "not set", + kr->keytab ? kr->keytab : "not set"); + } else { + kr->ccname = NULL; + kr->old_ccname = NULL; + kr->keytab = NULL; + sss_authtok_set_empty(pd->authtok); + } + + if (pd->cmd == SSS_PAM_CHAUTHTOK) { + ret = unpack_authtok(pd->newauthtok, buf, size, &p); + if (ret) { + return ret; + } + } else { + sss_authtok_set_empty(pd->newauthtok); + } + + if (pd->cmd == SSS_PAM_ACCT_MGMT) { + SAFEALIGN_COPY_UINT32_CHECK(&len, buf + p, size, &p); + if (len > size - p) return EINVAL; + pd->user = talloc_strndup(pd, (char *)(buf + p), len); + if (pd->user == NULL) return ENOMEM; + p += len; + DEBUG(SSSDBG_CONF_SETTINGS, "user: [%s]\n", pd->user); + } else { + pd->user = NULL; + } + + return EOK; +} + +static int krb5_cleanup(struct krb5_req *kr) +{ + if (kr == NULL) return EOK; + + if (kr->options != NULL) { + sss_krb5_get_init_creds_opt_free(kr->ctx, kr->options); + } + + if (kr->creds != NULL) { + krb5_free_cred_contents(kr->ctx, kr->creds); + krb5_free_creds(kr->ctx, kr->creds); + } + if (kr->name != NULL) + sss_krb5_free_unparsed_name(kr->ctx, kr->name); + if (kr->princ != NULL) + krb5_free_principal(kr->ctx, kr->princ); + if (kr->princ_orig != NULL) + krb5_free_principal(kr->ctx, kr->princ_orig); + if (kr->ctx != NULL) + krb5_free_context(kr->ctx); + + memset(kr, 0, sizeof(struct krb5_req)); + + return EOK; +} + +static krb5_error_code get_tgt_times(krb5_context ctx, const char *ccname, + krb5_principal server_principal, + krb5_principal client_principal, + sss_krb5_ticket_times *tgtt) +{ + krb5_error_code krberr; + krb5_ccache ccache = NULL; + krb5_creds mcred; + krb5_creds cred; + + krberr = krb5_cc_resolve(ctx, ccname, &ccache); + if (krberr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_cc_resolve failed.\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, krberr); + goto done; + } + + memset(&mcred, 0, sizeof(mcred)); + memset(&cred, 0, sizeof(mcred)); + + mcred.server = server_principal; + mcred.client = client_principal; + + krberr = krb5_cc_retrieve_cred(ctx, ccache, 0, &mcred, &cred); + if (krberr == KRB5_FCC_NOFILE) { + DEBUG(SSSDBG_TRACE_LIBS, "FAST ccache must be recreated\n"); + } else if (krberr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_cc_retrieve_cred failed\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, krberr); + krberr = 0; + goto done; + } + + tgtt->authtime = cred.times.authtime; + tgtt->starttime = cred.times.starttime; + tgtt->endtime = cred.times.endtime; + tgtt->renew_till = cred.times.renew_till; + + krb5_free_cred_contents(ctx, &cred); + + krberr = 0; + +done: + if (ccache != NULL) { + krb5_cc_close(ctx, ccache); + } + + return krberr; +} + +static krb5_error_code get_fast_ccache_with_anonymous_pkinit(krb5_context ctx, + uid_t fast_uid, + gid_t fast_gid, + bool posix_domain, + struct cli_opts *cli_opts, + krb5_keytab keytab, + krb5_principal client_princ, + char *ccname, + const char *realm) +{ + krb5_error_code kerr; + krb5_get_init_creds_opt *options; + struct sss_creds *saved_creds = NULL; + krb5_preauthtype pkinit = KRB5_PADATA_PK_AS_REQ; + krb5_creds creds = { 0 }; + + kerr = sss_krb5_get_init_creds_opt_alloc(ctx, &options); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + krb5_get_init_creds_opt_set_tkt_life(options, 10 * 60); + krb5_get_init_creds_opt_set_renew_life(options, 0); + krb5_get_init_creds_opt_set_forwardable(options, 0); + krb5_get_init_creds_opt_set_proxiable(options, 0); + krb5_get_init_creds_opt_set_canonicalize(options, 1); + krb5_get_init_creds_opt_set_preauth_list(options, &pkinit, 1); + + kerr = krb5_build_principal(ctx, &creds.server, strlen(realm), realm, + KRB5_TGS_NAME, realm, NULL); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to create principal.\n"); + goto done; + } + + creds.client = client_princ; + + kerr = krb5_get_init_creds_password(ctx, &creds, client_princ, NULL, + sss_krb5_prompter, NULL, 0, NULL, + options); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to get FAST credential with anonymous PKINIT.\n"); + goto done; + } + + kerr = switch_creds(NULL, fast_uid, fast_gid, 0, NULL, &saved_creds); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to switch credentials to store FAST ccache with " + "expected permissions.\n"); + goto done; + } + + kerr = create_ccache(ccname, &creds); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to store FAST ccache.\n"); + goto done; + } + + kerr = restore_creds(saved_creds); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to restore credentials, krb5_child might run with wrong " + "permissions, aborting.\n"); + goto done; + } + +done: + sss_krb5_get_init_creds_opt_free(ctx, options); + talloc_free(saved_creds); + + return kerr; +} + +static krb5_error_code get_fast_ccache_with_keytab(krb5_context ctx, + uid_t fast_uid, + gid_t fast_gid, + bool posix_domain, + struct cli_opts *cli_opts, + krb5_keytab keytab, + krb5_principal client_princ, + char *ccname) +{ + krb5_error_code kerr; + pid_t fchild_pid; + int status; + + fchild_pid = fork(); + switch (fchild_pid) { + case -1: + DEBUG(SSSDBG_CRIT_FAILURE, "fork failed\n"); + return EIO; + case 0: + /* Child */ + debug_prg_name = talloc_asprintf(NULL, "krb5_child[%d]", getpid()); + if (debug_prg_name == NULL) { + debug_prg_name = "krb5_child"; + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + /* Try to carry on */ + } + + kerr = k5c_become_user(fast_uid, fast_gid, posix_domain); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "become_user failed: %d\n", kerr); + exit(1); + } + DEBUG(SSSDBG_TRACE_INTERNAL, + "Running as [%"SPRIuid"][%"SPRIgid"].\n", geteuid(), getegid()); + + kerr = get_and_save_tgt_with_keytab(ctx, cli_opts, client_princ, + keytab, ccname); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "get_and_save_tgt_with_keytab failed: %d\n", kerr); + exit(2); + } + exit(0); + default: + /* Parent */ + do { + errno = 0; + kerr = waitpid(fchild_pid, &status, 0); + } while (kerr == -1 && errno == EINTR); + + if (kerr > 0) { + if (WIFEXITED(status)) { + kerr = WEXITSTATUS(status); + /* Don't blindly fail if the child fails, but check + * the ccache again */ + if (kerr != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Creating FAST ccache failed, krb5_child will " + "likely fail!\n"); + } + } else { + DEBUG(SSSDBG_CRIT_FAILURE, + "krb5_child subprocess %d terminated unexpectedly\n", + fchild_pid); + } + } else { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to wait for child %d\n", fchild_pid); + /* Let the code re-check the TGT times and fail if we + * can't find the updated principal */ + } + } + + return 0; +} + +static krb5_error_code check_fast_ccache(TALLOC_CTX *mem_ctx, + krb5_context ctx, + uid_t fast_uid, + gid_t fast_gid, + bool posix_domain, + struct cli_opts *cli_opts, + const char *primary, + const char *realm, + const char *keytab_name, + char **fast_ccname) +{ + TALLOC_CTX *tmp_ctx = NULL; + krb5_error_code kerr; + char *ccname; + char *server_name; + sss_krb5_ticket_times tgtt; + krb5_keytab keytab = NULL; + krb5_principal client_princ = NULL; + krb5_principal server_princ = NULL; + krb5_principal client_search_princ = NULL; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed.\n"); + return ENOMEM; + } + + ccname = talloc_asprintf(tmp_ctx, "FILE:%s/fast_ccache_%s", DB_PATH, realm); + if (ccname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + kerr = ENOMEM; + goto done; + } + + if (cli_opts->fast_use_anonymous_pkinit) { + kerr = krb5_build_principal(ctx, &client_princ, strlen(realm), realm, + KRB5_WELLKNOWN_NAMESTR, + KRB5_ANONYMOUS_PRINCSTR, NULL); + if (kerr != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to create anonymous PKINIT principal.\n"); + goto done; + } + + /* Anonymous pkinit is using the canonical principal + * WELLKNOWN/ANONYMOUS@WELLKNOWN:ANONYMOUS so we need an additional + * client_search_princ to find it in the ccache to determine the + * lifetime. */ + kerr = krb5_build_principal(ctx, &client_search_princ, + strlen(KRB5_ANONYMOUS_REALMSTR), + KRB5_ANONYMOUS_REALMSTR, + KRB5_WELLKNOWN_NAMESTR, + KRB5_ANONYMOUS_PRINCSTR, NULL); + if (kerr != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to create anonymous PKINIT principal.\n"); + goto done; + } + } else { + if (keytab_name != NULL) { + kerr = krb5_kt_resolve(ctx, keytab_name, &keytab); + } else { + kerr = krb5_kt_default(ctx, &keytab); + } + if (kerr) { + const char *__err_msg = sss_krb5_get_error_message(ctx, kerr); + DEBUG(SSSDBG_FATAL_FAILURE, + "Failed to read keytab file [%s]: %s\n", + sss_printable_keytab_name(ctx, keytab_name), + __err_msg); + sss_krb5_free_error_message(ctx, __err_msg); + goto done; + } + + kerr = find_principal_in_keytab(ctx, keytab, primary, realm, &client_princ); + if (kerr != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, + "find_principal_in_keytab failed for principal %s@%s.\n", + primary, realm); + goto done; + } + } + + server_name = talloc_asprintf(tmp_ctx, "krbtgt/%s@%s", realm, realm); + if (server_name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + kerr = ENOMEM; + goto done; + } + + kerr = krb5_parse_name(ctx, server_name, &server_princ); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_parse_name failed.\n"); + goto done; + } + + memset(&tgtt, 0, sizeof(tgtt)); + kerr = get_tgt_times(ctx, ccname, server_princ, + client_search_princ != NULL ? client_search_princ + : client_princ, + &tgtt); + if (kerr == 0) { + if (tgtt.endtime > time(NULL)) { + DEBUG(SSSDBG_FUNC_DATA, "FAST TGT is still valid.\n"); + goto done; + } + } + + /* Need to recreate the FAST ccache */ + if (cli_opts->fast_use_anonymous_pkinit) { + kerr = get_fast_ccache_with_anonymous_pkinit(ctx, fast_uid, fast_gid, + posix_domain, cli_opts, + keytab, client_princ, + ccname, realm); + if (kerr != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, "Creating FAST ccache with anonymous " + "PKINIT failed, krb5_child will " + "likely fail!\n"); + } + } else { + kerr = get_fast_ccache_with_keytab(ctx, fast_uid, fast_gid, posix_domain, + cli_opts, keytab, client_princ, ccname); + if (kerr != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, "Creating FAST ccache with keytab failed, " + "krb5_child will likely fail!\n"); + } + } + + /* Check the ccache times again. Should be updated ... */ + memset(&tgtt, 0, sizeof(tgtt)); + kerr = get_tgt_times(ctx, ccname, server_princ, + client_search_princ != NULL ? client_search_princ + : client_princ, + &tgtt); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "get_tgt_times() failed\n"); + goto done; + } + + if (tgtt.endtime < time(NULL)) { + DEBUG(SSSDBG_OP_FAILURE, + "FAST TGT was renewed but is already expired, please check that " + "time is synchronized with server.\n"); + kerr = ERR_CREDS_EXPIRED; + goto done; + } + DEBUG(SSSDBG_FUNC_DATA, "FAST TGT was successfully recreated!\n"); + +done: + if (client_princ != NULL) { + krb5_free_principal(ctx, client_princ); + } + if (client_search_princ != NULL) { + krb5_free_principal(ctx, client_search_princ); + } + if (server_princ != NULL) { + krb5_free_principal(ctx, server_princ); + } + + if (kerr == 0) { + *fast_ccname = talloc_steal(mem_ctx, ccname); + } + talloc_free(tmp_ctx); + + if (keytab != NULL) { + krb5_kt_close(ctx, keytab); + } + + return kerr; +} + +static errno_t k5c_recv_data(struct krb5_req *kr, int fd, uint32_t *offline) +{ + uint8_t buf[IN_BUF_SIZE]; + ssize_t len; + errno_t ret; + + errno = 0; + len = sss_atomic_read_safe_s(fd, buf, IN_BUF_SIZE, NULL); + if (len == -1) { + ret = errno; + ret = (ret == 0) ? EINVAL: ret; + DEBUG(SSSDBG_CRIT_FAILURE, + "read failed [%d][%s].\n", ret, strerror(ret)); + return ret; + } + + ret = unpack_buffer(buf, len, kr, offline); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "unpack_buffer failed.\n"); + } + + return ret; +} + +static int k5c_setup_fast(struct krb5_req *kr, bool demand) +{ + krb5_principal fast_princ_struct; + krb5_data *realm_data; + char *fast_principal_realm; + char *fast_principal; + krb5_error_code kerr; + char *tmp_str = NULL; + char *new_ccname; + + if (kr->cli_opts->fast_principal) { + DEBUG(SSSDBG_CONF_SETTINGS, "Fast principal is set to [%s]\n", + kr->cli_opts->fast_principal); + kerr = krb5_parse_name(kr->ctx, kr->cli_opts->fast_principal, + &fast_princ_struct); + if (kerr) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_parse_name failed.\n"); + return kerr; + } + kerr = sss_krb5_unparse_name_flags(kr->ctx, fast_princ_struct, + KRB5_PRINCIPAL_UNPARSE_NO_REALM, + &tmp_str); + if (kerr) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_krb5_unparse_name_flags failed.\n"); + return kerr; + } + fast_principal = talloc_strdup(kr, tmp_str); + if (!fast_principal) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n"); + return KRB5KRB_ERR_GENERIC; + } + free(tmp_str); + realm_data = krb5_princ_realm(kr->ctx, fast_princ_struct); + fast_principal_realm = talloc_asprintf(kr, "%.*s", realm_data->length, + realm_data->data); + if (!fast_principal_realm) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + return ENOMEM; + } + } else { + fast_principal_realm = kr->realm; + fast_principal = NULL; + } + + kerr = check_fast_ccache(kr, kr->ctx, kr->fast_uid, kr->fast_gid, + kr->posix_domain, kr->cli_opts, + fast_principal, fast_principal_realm, + kr->keytab, &kr->fast_ccname); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "check_fast_ccache failed.\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + kerr = copy_ccache_into_memory(kr, kr->ctx, kr->fast_ccname, &new_ccname); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "copy_ccache_into_memory failed.\n"); + return kerr; + } + + talloc_free(kr->fast_ccname); + kr->fast_ccname = new_ccname; + + kerr = sss_krb5_get_init_creds_opt_set_fast_ccache_name(kr->ctx, + kr->options, + kr->fast_ccname); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "sss_krb5_get_init_creds_opt_set_fast_ccache_name " + "failed.\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + if (demand) { + kerr = sss_krb5_get_init_creds_opt_set_fast_flags(kr->ctx, + kr->options, + SSS_KRB5_FAST_REQUIRED); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "sss_krb5_get_init_creds_opt_set_fast_flags " + "failed.\n"); + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + } + + return EOK; +} + +static errno_t check_use_fast(const char *use_fast_str, + enum k5c_fast_opt *_fast_val) +{ + enum k5c_fast_opt fast_val; + + if (use_fast_str == NULL || strcasecmp(use_fast_str, "never") == 0) { + DEBUG(SSSDBG_CONF_SETTINGS, "Not using FAST.\n"); + fast_val = K5C_FAST_NEVER; + } else if (strcasecmp(use_fast_str, "try") == 0) { + fast_val = K5C_FAST_TRY; + } else if (strcasecmp(use_fast_str, "demand") == 0) { + fast_val = K5C_FAST_DEMAND; + } else { + DEBUG(SSSDBG_CRIT_FAILURE, + "Unsupported value [%s] for krb5_use_fast.\n", + use_fast_str); + return EINVAL; + } + + *_fast_val = fast_val; + return EOK; +} + +static errno_t old_ccache_valid(struct krb5_req *kr, bool *_valid) +{ + errno_t ret; + bool valid; + + valid = false; + + ret = sss_krb5_cc_verify_ccache(kr->old_ccname, + kr->uid, kr->gid, + kr->realm, kr->upn); + switch (ret) { + case ERR_NOT_FOUND: + case ENOENT: + DEBUG(SSSDBG_TRACE_FUNC, + "Saved ccache %s doesn't exist, ignoring\n", kr->old_ccname); + break; + case EINVAL: + /* cache found but no TGT or expired */ + case EOK: + valid = true; + break; + default: + DEBUG(SSSDBG_OP_FAILURE, + "Cannot check if saved ccache %s is valid\n", + kr->old_ccname); + return ret; + } + + *_valid = valid; + return EOK; +} + +static int k5c_check_old_ccache(struct krb5_req *kr) +{ + errno_t ret; + + if (kr->old_ccname) { + ret = old_ccache_valid(kr, &kr->old_cc_valid); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "old_ccache_valid failed.\n"); + return ret; + } + + ret = check_if_uid_is_active(kr->uid, &kr->old_cc_active); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "check_if_uid_is_active failed.\n"); + return ret; + } + + DEBUG(SSSDBG_TRACE_ALL, + "Ccache_file is [%s] and is %s active and TGT is %s valid.\n", + kr->old_ccname ? kr->old_ccname : "not set", + kr->old_cc_active ? "" : "not", + kr->old_cc_valid ? "" : "not"); + } + + return EOK; +} + +static int k5c_precreate_ccache(struct krb5_req *kr, uint32_t offline) +{ + errno_t ret; + + /* The ccache file should be (re)created if one of the following conditions + * is true: + * - it doesn't exist (kr->old_ccname == NULL) + * - the backend is online and the current ccache file is not used, i.e + * the related user is currently not logged in and it is not a renewal + * request + * (offline && !kr->old_cc_active && kr->pd->cmd != SSS_CMD_RENEW) + * - the backend is offline and the current cache file not used and + * it does not contain a valid TGT + * (offline && !kr->old_cc_active && !kr->valid_tgt) + */ + if (kr->old_ccname == NULL || + (offline && !kr->old_cc_active && !kr->old_cc_valid) || + (!offline && !kr->old_cc_active && kr->pd->cmd != SSS_CMD_RENEW)) { + DEBUG(SSSDBG_TRACE_ALL, "Recreating ccache\n"); + + ret = sss_krb5_precreate_ccache(kr->ccname, kr->uid, kr->gid); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "ccache creation failed.\n"); + return ret; + } + } else { + /* We can reuse the old ccache */ + kr->ccname = kr->old_ccname; + } + + return EOK; +} + +static int k5c_ccache_setup(struct krb5_req *kr, uint32_t offline) +{ + errno_t ret; + + if (kr->pd->cmd == SSS_PAM_ACCT_MGMT) { + return EOK; + } + + ret = k5c_check_old_ccache(kr); + if (ret != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot check old ccache [%s]: [%d][%s]. " \ + "Assuming old cache is invalid " \ + "and not used.\n", + kr->old_ccname, ret, sss_strerror(ret)); + } + + /* Pre-creating the ccache must be done as root, otherwise we can't mkdir + * some of the DIR: cache components. One example is /run/user/$UID because + * logind doesn't create the directory until the session phase, whereas + * we need the directory during the auth phase already + */ + ret = k5c_precreate_ccache(kr, offline); + if (ret != 0) { + DEBUG(SSSDBG_OP_FAILURE, "Cannot precreate ccache\n"); + return ret; + } + + return EOK; +} + +static int k5c_setup(struct krb5_req *kr, uint32_t offline) +{ + krb5_error_code kerr; + int parse_flags; + + /* Set the global error context */ + krb5_error_ctx = kr->ctx; + + if (debug_level & SSSDBG_TRACE_ALL) { + kerr = sss_child_set_krb5_tracing(kr->ctx); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_MINOR_FAILURE, kerr); + return EIO; + } + } + + /* Enterprise principals require that a default realm is available. To + * make SSSD more robust in the case that the default realm option is + * missing in krb5.conf or to allow SSSD to work with multiple unconnected + * realms (e.g. AD domains without trust between them) the default realm + * will be set explicitly. */ + if (kr->use_enterprise_princ) { + kerr = krb5_set_default_realm(kr->ctx, kr->realm); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_set_default_realm failed.\n"); + } + } + + parse_flags = kr->use_enterprise_princ ? KRB5_PRINCIPAL_PARSE_ENTERPRISE : 0; + kerr = sss_krb5_parse_name_flags(kr->ctx, kr->upn, parse_flags, &kr->princ); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + kerr = krb5_parse_name(kr->ctx, kr->upn, &kr->princ_orig); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + kerr = krb5_unparse_name(kr->ctx, kr->princ, &kr->name); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + kr->creds = calloc(1, sizeof(krb5_creds)); + if (kr->creds == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "calloc failed.\n"); + return ENOMEM; + } + +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_RESPONDER + kerr = krb5_get_init_creds_opt_set_responder(kr->ctx, kr->options, + sss_krb5_responder, kr); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } +#endif + +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT + /* A prompter is used to catch messages about when a password will + * expire. The library shall not use the prompter to ask for a new password + * but shall return KRB5KDC_ERR_KEY_EXP. */ + krb5_get_init_creds_opt_set_change_password_prompt(kr->options, 0); +#endif + + kerr = set_lifetime_options(kr->cli_opts, kr->options); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "set_lifetime_options failed.\n"); + return kerr; + } + + if (!offline) { + set_canonicalize_option(kr->cli_opts, kr->options); + } + +/* TODO: set options, e.g. + * krb5_get_init_creds_opt_set_forwardable + * krb5_get_init_creds_opt_set_proxiable + * krb5_get_init_creds_opt_set_etype_list + * krb5_get_init_creds_opt_set_address_list + * krb5_get_init_creds_opt_set_preauth_list + * krb5_get_init_creds_opt_set_salt + * krb5_get_init_creds_opt_set_change_password_prompt + * krb5_get_init_creds_opt_set_pa + */ + + return kerr; +} + +static krb5_error_code check_keytab_name(struct krb5_req *kr) +{ + krb5_error_code kerr; + char krb5_conf_keytab[MAX_KEYTAB_NAME_LEN]; + char *path_start = NULL; + + if (kr->keytab == NULL && ( + kr->pd->cmd == SSS_PAM_AUTHENTICATE || + kr->pd->cmd == SSS_PAM_PREAUTH || + kr->pd->cmd == SSS_CMD_RENEW || + kr->pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM || + kr->pd->cmd == SSS_PAM_CHAUTHTOK)) { + + DEBUG(SSSDBG_TRACE_FUNC, + "Missing krb5_keytab option for domain, looking for default one\n"); + + kerr = krb5_kt_default_name(kr->ctx, krb5_conf_keytab, sizeof(krb5_conf_keytab)); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Unable to get default keytab location from krb.conf\n"); + return kerr; + } + + DEBUG(SSSDBG_TRACE_FUNC, + "krb5_kt_default_name() returned: %s\n", + krb5_conf_keytab); + + /* krb5_kt_default_name() can return file path with "FILE:" prefix, + it need to be removed */ + if (0 == strncmp(krb5_conf_keytab, "FILE:", strlen("FILE:"))) { + path_start = krb5_conf_keytab + strlen("FILE:"); + } else { + path_start = krb5_conf_keytab; + } + + kr->keytab = talloc_strndup(kr->pd, path_start, strlen(path_start)); + + DEBUG(SSSDBG_TRACE_FUNC, "krb5_child will default to: %s\n", path_start); + } + + return 0; +} + +static krb5_error_code privileged_krb5_setup(struct krb5_req *kr, + uint32_t offline) +{ + krb5_error_code kerr; + int ret; + char *mem_keytab; + + kr->realm = kr->cli_opts->realm; + if (kr->realm == NULL) { + DEBUG(SSSDBG_MINOR_FAILURE, "Realm not available.\n"); + } + + kerr = krb5_init_context(&kr->ctx); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + kerr = check_keytab_name(kr); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + kerr = sss_krb5_get_init_creds_opt_alloc(kr->ctx, &kr->options); + if (kerr != 0) { + KRB5_CHILD_DEBUG(SSSDBG_CRIT_FAILURE, kerr); + return kerr; + } + + ret = check_use_fast(kr->cli_opts->use_fast_str, &kr->fast_val); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "check_use_fast failed.\n"); + return ret; + } + + /* For ccache types FILE: and DIR: we might need to create some directory + * components as root. Cache files are not needed during preauth. */ + if (kr->pd->cmd != SSS_PAM_PREAUTH) { + ret = k5c_ccache_setup(kr, offline); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "k5c_ccache_setup failed.\n"); + return ret; + } + } + + if (!(offline || + (kr->fast_val == K5C_FAST_NEVER && kr->validate == false))) { + /* A Keytab is not used if fast with anonymous pkinit is used (and validate is false)*/ + if (!(kr->cli_opts->fast_use_anonymous_pkinit == true && kr->validate == false)) { + kerr = copy_keytab_into_memory(kr, kr->ctx, kr->keytab, &mem_keytab, + NULL); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "copy_keytab_into_memory failed.\n"); + return kerr; + } + + talloc_free(kr->keytab); + kr->keytab = mem_keytab; + } + + if (kr->fast_val != K5C_FAST_NEVER) { + kerr = k5c_setup_fast(kr, kr->fast_val == K5C_FAST_DEMAND); + if (kerr != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Cannot set up FAST\n"); + return kerr; + } + } + } + + if (kr->send_pac) { + ret = sss_pac_check_and_open(); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, "Cannot open the PAC responder socket\n"); + /* Not fatal */ + } + } + + return 0; +} + +static void try_open_krb5_conf(void) +{ + int fd; + int ret; + + fd = open("/etc/krb5.conf", O_RDONLY); + if (fd != -1) { + close(fd); + } else { + ret = errno; + if (ret == EACCES || ret == EPERM) { + DEBUG(SSSDBG_CRIT_FAILURE, + "User with uid:%"SPRIuid" gid:%"SPRIgid" cannot read " + "/etc/krb5.conf. It might cause problems\n", + geteuid(), getegid()); + } else { + DEBUG(SSSDBG_MINOR_FAILURE, + "Cannot open /etc/krb5.conf [%d]: %s\n", + ret, strerror(ret)); + } + } +} + +int main(int argc, const char *argv[]) +{ + struct krb5_req *kr = NULL; + uint32_t offline; + int opt; + poptContext pc; + int dumpable = 1; + int debug_fd = -1; + const char *opt_logger = NULL; + errno_t ret; + krb5_error_code kerr; + uid_t fast_uid = 0; + gid_t fast_gid = 0; + long chain_id = 0; + struct cli_opts cli_opts = { 0 }; + int sss_creds_password = 0; + long dummy_long = 0; + + struct poptOption long_options[] = { + POPT_AUTOHELP + SSSD_DEBUG_OPTS + {"dumpable", 0, POPT_ARG_INT, &dumpable, 0, + _("Allow core dumps"), NULL }, + {"debug-fd", 0, POPT_ARG_INT, &debug_fd, 0, + _("An open file descriptor for the debug logs"), NULL}, + SSSD_LOGGER_OPTS + {CHILD_OPT_FAST_CCACHE_UID, 0, POPT_ARG_INT, &fast_uid, 0, + _("The user to create FAST ccache as"), NULL}, + {CHILD_OPT_FAST_CCACHE_GID, 0, POPT_ARG_INT, &fast_gid, 0, + _("The group to create FAST ccache as"), NULL}, + {CHILD_OPT_FAST_USE_ANONYMOUS_PKINIT, 0, POPT_ARG_NONE, NULL, 'A', + _("Use anonymous PKINIT to request FAST armor ticket"), NULL}, + {CHILD_OPT_REALM, 0, POPT_ARG_STRING, &cli_opts.realm, 0, + _("Kerberos realm to use"), NULL}, + {CHILD_OPT_LIFETIME, 0, POPT_ARG_STRING, &cli_opts.lifetime, 0, + _("Requested lifetime of the ticket"), NULL}, + {CHILD_OPT_RENEWABLE_LIFETIME, 0, POPT_ARG_STRING, &cli_opts.rtime, 0, + _("Requested renewable lifetime of the ticket"), NULL}, + {CHILD_OPT_USE_FAST, 0, POPT_ARG_STRING, &cli_opts.use_fast_str, 0, + _("FAST options ('never', 'try', 'demand')"), NULL}, + {CHILD_OPT_FAST_PRINCIPAL, 0, POPT_ARG_STRING, + &cli_opts.fast_principal, 0, + _("Specifies the server principal to use for FAST"), NULL}, + {CHILD_OPT_CANONICALIZE, 0, POPT_ARG_NONE, NULL, 'C', + _("Requests canonicalization of the principal name"), NULL}, + {CHILD_OPT_SSS_CREDS_PASSWORD, 0, POPT_ARG_NONE, &sss_creds_password, + 0, _("Use custom version of krb5_get_init_creds_password"), NULL}, + {CHILD_OPT_CHAIN_ID, 0, POPT_ARG_LONG, &chain_id, + 0, _("Tevent chain ID used for logging purposes"), NULL}, + {CHILD_OPT_CHECK_PAC, 0, POPT_ARG_LONG, &dummy_long, 0, + _("Check PAC flags"), NULL}, + POPT_TABLEEND + }; + + /* Set debug level to invalid value so we can decide if -d 0 was used. */ + debug_level = SSSDBG_INVALID; + + cli_opts.canonicalize = false; + cli_opts.fast_use_anonymous_pkinit = false; + + pc = poptGetContext(argv[0], argc, argv, long_options, 0); + while((opt = poptGetNextOpt(pc)) != -1) { + switch(opt) { + case 'A': + cli_opts.fast_use_anonymous_pkinit = true; + break; + case 'C': + cli_opts.canonicalize = true; + break; + default: + fprintf(stderr, "\nInvalid option %s: %s\n\n", + poptBadOption(pc, 0), poptStrerror(opt)); + poptPrintUsage(pc, stderr, 0); + _exit(-1); + } + } + + cli_opts.check_pac_flags = 0; + if (dummy_long >= 0 && dummy_long <= UINT32_MAX) { + cli_opts.check_pac_flags = (uint32_t) dummy_long; + } else { + fprintf(stderr, "\nInvalid value [%ld] of check-pac option\n\n", + dummy_long); + poptPrintUsage(pc, stderr, 0); + _exit(-1); + } + + poptFreeContext(pc); + + prctl(PR_SET_DUMPABLE, (dumpable == 0) ? 0 : 1); + + debug_prg_name = talloc_asprintf(NULL, "krb5_child[%d]", getpid()); + if (!debug_prg_name) { + debug_prg_name = "krb5_child"; + ERROR("talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + + if (debug_fd != -1) { + opt_logger = sss_logger_str[FILES_LOGGER]; + ret = set_debug_file_from_fd(debug_fd); + if (ret != EOK) { + opt_logger = sss_logger_str[STDERR_LOGGER]; + ERROR("set_debug_file_from_fd failed.\n"); + } + } + + sss_chain_id_set_format(DEBUG_CHAIN_ID_FMT_RID); + sss_chain_id_set((uint64_t)chain_id); + + DEBUG_INIT(debug_level, opt_logger); + + DEBUG(SSSDBG_TRACE_FUNC, "krb5_child started.\n"); + + kr = talloc_zero(NULL, struct krb5_req); + if (kr == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + ret = ENOMEM; + goto done; + } + talloc_steal(kr, debug_prg_name); + + kr->fast_uid = fast_uid; + kr->fast_gid = fast_gid; + kr->cli_opts = &cli_opts; + if (sss_creds_password != 0) { + kr->krb5_get_init_creds_password = sss_krb5_get_init_creds_password; + } else { + kr->krb5_get_init_creds_password = krb5_get_init_creds_password; + } + + ret = k5c_recv_data(kr, STDIN_FILENO, &offline); + if (ret != EOK) { + goto done; + } + + if (cli_opts.check_pac_flags != 0 && !kr->validate) { + DEBUG(SSSDBG_IMPORTANT_INFO, + "PAC check is requested but krb5_validate is set to false. " + "PAC checks will be skipped.\n"); + } + + kerr = privileged_krb5_setup(kr, offline); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "privileged_krb5_setup failed.\n"); + ret = EFAULT; + goto done; + } + + /* For PKINIT we might need access to the pcscd socket which by default + * is only allowed for authenticated users. Since PKINIT is part of + * the authentication and the user is not authenticated yet, we have + * to use different privileges and can only drop it only after the TGT is + * received. The fast_uid and fast_gid are the IDs the backend is running + * with. This can be either root or the 'sssd' user. Root is allowed by + * default and the 'sssd' user is allowed with the help of the + * sssd-pcsc.rules policy-kit rule. So those IDs are a suitable choice. We + * can only call switch_creds() because after the TGT is returned we have + * to switch to the IDs of the user to store the TGT. + * If we are offline we have to switch to the user's credentials directly + * to make sure the empty ccache is created with the expected + * ownership. */ + if (IS_SC_AUTHTOK(kr->pd->authtok) && !offline) { + kerr = switch_creds(kr, kr->fast_uid, kr->fast_gid, 0, NULL, + &kr->pcsc_saved_creds); + } else { + kerr = k5c_become_user(kr->uid, kr->gid, kr->posix_domain); + } + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "become_user failed.\n"); + ret = EFAULT; + goto done; + } + + DEBUG(SSSDBG_TRACE_INTERNAL, + "Running as [%"SPRIuid"][%"SPRIgid"].\n", geteuid(), getegid()); + + try_open_krb5_conf(); + + ret = k5c_setup(kr, offline); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "k5c_setup failed.\n"); + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, + "Will perform %s\n", krb5_child_command_to_str(kr->pd->cmd)); + switch(kr->pd->cmd) { + case SSS_PAM_AUTHENTICATE: + /* If we are offline, we need to create an empty ccache file */ + if (offline) { + DEBUG(SSSDBG_TRACE_FUNC, "Will perform offline auth\n"); + ret = create_empty_ccache(kr); + } else { + DEBUG(SSSDBG_TRACE_FUNC, "Will perform online auth\n"); + ret = tgt_req_child(kr); + } + break; + case SSS_PAM_CHAUTHTOK: + ret = changepw_child(kr, false); + break; + case SSS_PAM_CHAUTHTOK_PRELIM: + ret = changepw_child(kr, true); + break; + case SSS_PAM_ACCT_MGMT: + ret = kuserok_child(kr); + break; + case SSS_CMD_RENEW: + if (offline) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot renew TGT while offline\n"); + ret = KRB5_KDC_UNREACH; + goto done; + } + ret = renew_tgt_child(kr); + break; + case SSS_PAM_PREAUTH: + ret = tgt_req_child(kr); + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "PAM command [%d] not supported.\n", kr->pd->cmd); + ret = EINVAL; + goto done; + } + + ret = k5c_send_data(kr, STDOUT_FILENO, ret); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to send reply\n"); + } + +done: + if (ret == EOK) { + DEBUG(SSSDBG_TRACE_FUNC, "krb5_child completed successfully\n"); + ret = 0; + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_child failed!\n"); + ret = -1; + } + krb5_cleanup(kr); + talloc_free(kr); + exit(ret); +} diff --git a/src/providers/krb5/krb5_child_handler.c b/src/providers/krb5/krb5_child_handler.c new file mode 100644 index 0000000..54088e4 --- /dev/null +++ b/src/providers/krb5/krb5_child_handler.c @@ -0,0 +1,1022 @@ +/* + SSSD + + Kerberos 5 Backend Module - Manage krb5_child + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2010 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <signal.h> + +#include "util/util.h" +#include "util/child_common.h" +#include "util/sss_chain_id.h" +#include "providers/krb5/krb5_common.h" +#include "providers/krb5/krb5_auth.h" +#include "src/providers/krb5/krb5_utils.h" +#include "util/sss_ptr_hash.h" + +#ifndef KRB5_CHILD_DIR +#ifndef SSSD_LIBEXEC_PATH +#error "SSSD_LIBEXEC_PATH not defined" +#endif /* SSSD_LIBEXEC_PATH */ + +#define KRB5_CHILD_DIR SSSD_LIBEXEC_PATH +#endif /* KRB5_CHILD_DIR */ + +#define KRB5_CHILD KRB5_CHILD_DIR"/krb5_child" + +#define TIME_T_MAX LONG_MAX +#define int64_to_time_t(val) ((time_t)((val) < TIME_T_MAX ? val : TIME_T_MAX)) + +struct handle_child_state { + struct tevent_context *ev; + struct krb5child_req *kr; + uint8_t *buf; + ssize_t len; + + struct tevent_timer *timeout_handler; + pid_t child_pid; + + struct child_io_fds *io; +}; + +static errno_t pack_authtok(struct io_buffer *buf, size_t *rp, + struct sss_auth_token *tok) +{ + uint32_t auth_token_type; + uint32_t auth_token_length = 0; + const char *data; + size_t len; + errno_t ret = EOK; + + auth_token_type = sss_authtok_get_type(tok); + + switch (auth_token_type) { + case SSS_AUTHTOK_TYPE_EMPTY: + auth_token_length = 0; + data = ""; + break; + case SSS_AUTHTOK_TYPE_PASSWORD: + ret = sss_authtok_get_password(tok, &data, &len); + auth_token_length = len + 1; + break; + case SSS_AUTHTOK_TYPE_CCFILE: + ret = sss_authtok_get_ccfile(tok, &data, &len); + auth_token_length = len + 1; + break; + case SSS_AUTHTOK_TYPE_2FA_SINGLE: + ret = sss_authtok_get_2fa_single(tok, &data, &len); + auth_token_length = len + 1; + break; + case SSS_AUTHTOK_TYPE_2FA: + case SSS_AUTHTOK_TYPE_SC_PIN: + case SSS_AUTHTOK_TYPE_SC_KEYPAD: + case SSS_AUTHTOK_TYPE_OAUTH2: + case SSS_AUTHTOK_TYPE_PASSKEY: + case SSS_AUTHTOK_TYPE_PASSKEY_KRB: + case SSS_AUTHTOK_TYPE_PASSKEY_REPLY: + data = (char *) sss_authtok_get_data(tok); + auth_token_length = sss_authtok_get_size(tok); + break; + default: + ret = EINVAL; + } + + if (ret == EOK) { + SAFEALIGN_COPY_UINT32(&buf->data[*rp], &auth_token_type, rp); + SAFEALIGN_COPY_UINT32(&buf->data[*rp], &auth_token_length, rp); + if (data != NULL) { + safealign_memcpy(&buf->data[*rp], data, auth_token_length, rp); + } + } + + return ret; +} + +static errno_t create_send_buffer(struct krb5child_req *kr, + struct io_buffer **io_buf) +{ + struct io_buffer *buf; + size_t rp; + const char *keytab; + uint32_t validate; + uint32_t send_pac; + uint32_t use_enterprise_principal; + uint32_t posix_domain = 0; + size_t username_len = 0; + errno_t ret; + + keytab = dp_opt_get_cstring(kr->krb5_ctx->opts, KRB5_KEYTAB); + if (keytab == NULL) { + DEBUG(SSSDBG_TRACE_FUNC, "krb5_keytab not set for domain in sssd.conf\n"); + keytab = ""; + } + + validate = dp_opt_get_bool(kr->krb5_ctx->opts, KRB5_VALIDATE) ? 1 : 0; + + /* Always send PAC except for local IPA users and IPA server mode */ + switch (kr->krb5_ctx->config_type) { + case K5C_IPA_CLIENT: + send_pac = kr->upn_from_different_realm ? 1 : 0; + break; + case K5C_IPA_SERVER: + send_pac = 0; + break; + default: + send_pac = 1; + break; + } + + /* Renewals from KCM do not initialize kr->dom */ + if (kr->pd->cmd == SSS_CMD_RENEW || kr->dom->type == DOM_TYPE_POSIX) { + posix_domain = 1; + } else if (kr->dom->type != DOM_TYPE_APPLICATION) { + return EINVAL; + } + + if (kr->pd->cmd == SSS_CMD_RENEW || kr->pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM + || kr->pd->cmd == SSS_PAM_CHAUTHTOK || kr->is_offline) { + use_enterprise_principal = false; + } else { + use_enterprise_principal = dp_opt_get_bool(kr->krb5_ctx->opts, + KRB5_USE_ENTERPRISE_PRINCIPAL) ? 1 : 0; + } + + buf = talloc(kr, struct io_buffer); + if (buf == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc failed.\n"); + return ENOMEM; + } + + buf->size = 9*sizeof(uint32_t) + strlen(kr->upn); + + if (kr->pd->cmd == SSS_PAM_AUTHENTICATE || + kr->pd->cmd == SSS_PAM_PREAUTH || + kr->pd->cmd == SSS_CMD_RENEW || + kr->pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM || + kr->pd->cmd == SSS_PAM_CHAUTHTOK) { + buf->size += 4*sizeof(uint32_t) + strlen(kr->ccname) + strlen(keytab) + + sss_authtok_get_size(kr->pd->authtok); + + buf->size += sizeof(uint32_t); + if (kr->old_ccname) { + buf->size += strlen(kr->old_ccname); + } + } + + if (kr->pd->cmd == SSS_PAM_CHAUTHTOK) { + buf->size += 2*sizeof(uint32_t) + + sss_authtok_get_size(kr->pd->newauthtok); + } + + if (kr->pd->cmd == SSS_PAM_ACCT_MGMT) { + username_len = strlen(kr->kuserok_user); + buf->size += sizeof(uint32_t) + username_len; + } + + buf->data = talloc_size(kr, buf->size); + if (buf->data == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_size failed.\n"); + talloc_free(buf); + return ENOMEM; + } + + rp = 0; + SAFEALIGN_COPY_UINT32(&buf->data[rp], &kr->pd->cmd, &rp); + SAFEALIGN_COPY_UINT32(&buf->data[rp], &kr->uid, &rp); + SAFEALIGN_COPY_UINT32(&buf->data[rp], &kr->gid, &rp); + SAFEALIGN_COPY_UINT32(&buf->data[rp], &validate, &rp); + SAFEALIGN_COPY_UINT32(&buf->data[rp], &posix_domain, &rp); + SAFEALIGN_COPY_UINT32(&buf->data[rp], &kr->is_offline, &rp); + SAFEALIGN_COPY_UINT32(&buf->data[rp], &send_pac, &rp); + SAFEALIGN_COPY_UINT32(&buf->data[rp], &use_enterprise_principal, &rp); + + SAFEALIGN_SET_UINT32(&buf->data[rp], strlen(kr->upn), &rp); + safealign_memcpy(&buf->data[rp], kr->upn, strlen(kr->upn), &rp); + + if (kr->pd->cmd == SSS_PAM_AUTHENTICATE || + kr->pd->cmd == SSS_PAM_PREAUTH || + kr->pd->cmd == SSS_CMD_RENEW || + kr->pd->cmd == SSS_PAM_CHAUTHTOK_PRELIM || + kr->pd->cmd == SSS_PAM_CHAUTHTOK) { + SAFEALIGN_SET_UINT32(&buf->data[rp], strlen(kr->ccname), &rp); + safealign_memcpy(&buf->data[rp], kr->ccname, strlen(kr->ccname), &rp); + + if (kr->old_ccname) { + SAFEALIGN_SET_UINT32(&buf->data[rp], strlen(kr->old_ccname), &rp); + safealign_memcpy(&buf->data[rp], kr->old_ccname, + strlen(kr->old_ccname), &rp); + } else { + SAFEALIGN_SET_UINT32(&buf->data[rp], 0, &rp); + } + + SAFEALIGN_SET_UINT32(&buf->data[rp], strlen(keytab), &rp); + safealign_memcpy(&buf->data[rp], keytab, strlen(keytab), &rp); + + ret = pack_authtok(buf, &rp, kr->pd->authtok); + if (ret) { + return ret; + } + } + + if (kr->pd->cmd == SSS_PAM_CHAUTHTOK) { + ret = pack_authtok(buf, &rp, kr->pd->newauthtok); + if (ret) { + return ret; + } + } + + if (kr->pd->cmd == SSS_PAM_ACCT_MGMT) { + SAFEALIGN_SET_UINT32(&buf->data[rp], username_len, &rp); + safealign_memcpy(&buf->data[rp], kr->kuserok_user, username_len, &rp); + } + + *io_buf = buf; + + return EOK; +} + +static void krb5_child_terminate(pid_t pid) +{ + int ret; + + if (pid == 0) { + return; + } + + ret = kill(pid, SIGKILL); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, "kill failed [%d]: %s\n", + ret, sss_strerror(ret)); + } +} + +static void krb5_child_timeout(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval tv, void *pvt) +{ + struct tevent_req *req = talloc_get_type(pvt, struct tevent_req); + struct handle_child_state *state = tevent_req_data(req, + struct handle_child_state); + + if (state->timeout_handler == NULL) { + return; + } + + /* No I/O expected anymore, make sure sockets are closed properly */ + state->io->in_use = false; + + DEBUG(SSSDBG_IMPORTANT_INFO, + "Timeout for child [%d] reached. In case KDC is distant or network " + "is slow you may consider increasing value of krb5_auth_timeout.\n", + state->child_pid); + + krb5_child_terminate(state->child_pid); + + tevent_req_error(req, ETIMEDOUT); +} + +static errno_t activate_child_timeout_handler(struct tevent_req *req, + struct tevent_context *ev, + const uint32_t timeout_seconds) +{ + struct timeval tv; + struct handle_child_state *state = tevent_req_data(req, + struct handle_child_state); + + tv = tevent_timeval_current(); + tv = tevent_timeval_add(&tv, timeout_seconds, 0); + state->timeout_handler = tevent_add_timer(ev, state, tv, + krb5_child_timeout, req); + if (state->timeout_handler == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_add_timer failed.\n"); + return ENOMEM; + } + + return EOK; +} + +errno_t set_extra_args(TALLOC_CTX *mem_ctx, struct krb5_ctx *krb5_ctx, + struct sss_domain_info *domain, + const char ***krb5_child_extra_args) +{ + const char **extra_args; + const char *krb5_realm; + uint64_t chain_id; + size_t c = 0; + int ret; + + if (krb5_ctx == NULL || krb5_child_extra_args == NULL) { + return EINVAL; + } + + extra_args = talloc_zero_array(mem_ctx, const char *, 12); + if (extra_args == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_zero_array failed.\n"); + return ENOMEM; + } + + extra_args[c] = talloc_asprintf(extra_args, + "--"CHILD_OPT_FAST_CCACHE_UID"=%"SPRIuid, + getuid()); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + + extra_args[c] = talloc_asprintf(extra_args, + "--"CHILD_OPT_FAST_CCACHE_GID"=%"SPRIgid, + getgid()); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + + krb5_realm = krb5_ctx->realm; + if (domain != NULL && IS_SUBDOMAIN(domain) && dp_opt_get_bool(krb5_ctx->opts, KRB5_USE_SUBDOMAIN_REALM)) { + DEBUG(SSSDBG_CONF_SETTINGS, "Use subdomain realm %s.\n", domain->realm); + krb5_realm = domain->realm; + } + + if (krb5_ctx->realm != NULL) { + extra_args[c] = talloc_asprintf(extra_args, "--"CHILD_OPT_REALM"=%s", + krb5_realm); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + } + + if (krb5_ctx->lifetime_str != NULL) { + extra_args[c] = talloc_asprintf(extra_args, "--"CHILD_OPT_LIFETIME"=%s", + krb5_ctx->lifetime_str); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + } + + if (krb5_ctx->rlife_str != NULL) { + extra_args[c] = talloc_asprintf(extra_args, + "--"CHILD_OPT_RENEWABLE_LIFETIME"=%s", + krb5_ctx->rlife_str); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + } + + if (krb5_ctx->use_fast_str != NULL) { + extra_args[c] = talloc_asprintf(extra_args, "--"CHILD_OPT_USE_FAST"=%s", + krb5_ctx->use_fast_str); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + + if (krb5_ctx->fast_principal != NULL) { + extra_args[c] = talloc_asprintf(extra_args, + "--"CHILD_OPT_FAST_PRINCIPAL"=%s", + krb5_ctx->fast_principal); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + } + + if (krb5_ctx->fast_use_anonymous_pkinit) { + extra_args[c] = talloc_strdup(extra_args, + "--" CHILD_OPT_FAST_USE_ANONYMOUS_PKINIT); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + } + } + + if (krb5_ctx->check_pac_flags != 0) { + extra_args[c] = talloc_asprintf(extra_args, + "--"CHILD_OPT_CHECK_PAC"=%"PRIu32, + krb5_ctx->check_pac_flags); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + } + + if (krb5_ctx->canonicalize) { + extra_args[c] = talloc_strdup(extra_args, + "--" CHILD_OPT_CANONICALIZE); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + } + + if (krb5_ctx->sss_creds_password) { + extra_args[c] = talloc_strdup(extra_args, + "--" CHILD_OPT_SSS_CREDS_PASSWORD); + if (extra_args[c] == NULL) { + ret = ENOMEM; + goto done; + } + c++; + } + + chain_id = sss_chain_id_get(); + extra_args[c] = talloc_asprintf(extra_args, + "--"CHILD_OPT_CHAIN_ID"=%lu", + chain_id); + if (extra_args[c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + c++; + + extra_args[c] = NULL; + + *krb5_child_extra_args = extra_args; + + ret = EOK; + +done: + + if (ret != EOK) { + talloc_free(extra_args); + } + + return ret; +} + +static void child_exited(int child_status, + struct tevent_signal *sige, + void *pvt) +{ + struct child_io_fds *io = talloc_get_type(pvt, struct child_io_fds); + + /* Do not free it if we still need to read some data. Just mark that the + * child has exited so we know we need to free it later. */ + if (io->in_use) { + io->child_exited = true; + return; + } + + /* The child has finished and we don't need to use the file descriptors + * any more. This will close them and remove them from io hash table. */ + talloc_free(io); +} + +static void child_keep_alive_timeout(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval tv, + void *pvt) +{ + struct child_io_fds *io = talloc_get_type(pvt, struct child_io_fds); + + DEBUG(SSSDBG_IMPORTANT_INFO, "Keep alive timeout for child [%d] reached.\n", + io->pid); + + /* No I/O expected anymore, make sure sockets are closed properly */ + io->in_use = false; + + krb5_child_terminate(io->pid); +} + +static errno_t fork_child(struct tevent_context *ev, + struct krb5child_req *kr, + pid_t *_child_pid, + struct child_io_fds **_io) +{ + TALLOC_CTX *tmp_ctx; + int pipefd_to_child[2] = PIPE_INIT; + int pipefd_from_child[2] = PIPE_INIT; + const char **krb5_child_extra_args; + struct child_io_fds *io; + struct tevent_timer *te; + struct timeval tv; + char *io_key; + pid_t pid = 0; + errno_t ret; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + return ENOMEM; + } + + ret = set_extra_args(tmp_ctx, kr->krb5_ctx, kr->dom, &krb5_child_extra_args); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "set_extra_args failed.\n"); + goto done; + } + + ret = pipe(pipefd_from_child); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "pipe (from) failed [%d][%s].\n", errno, strerror(errno)); + goto done; + } + + ret = pipe(pipefd_to_child); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "pipe (to) failed [%d][%s].\n", errno, strerror(errno)); + goto done; + } + + pid = fork(); + + if (pid == 0) { /* child */ + exec_child_ex(tmp_ctx, + pipefd_to_child, pipefd_from_child, + KRB5_CHILD, KRB5_CHILD_LOG_FILE, + krb5_child_extra_args, false, + STDIN_FILENO, STDOUT_FILENO); + + /* We should never get here */ + DEBUG(SSSDBG_CRIT_FAILURE, "BUG: Could not exec KRB5 child\n"); + ret = ERR_INTERNAL; + goto done; + } else if (pid < 0) { /* error */ + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, "fork failed [%d]: %s\n", ret, strerror(ret)); + goto done; + } + + /* parent */ + + io = talloc_zero(tmp_ctx, struct child_io_fds); + if (io == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc failed.\n"); + ret = ENOMEM; + goto done; + } + talloc_set_destructor((void*)io, child_io_destructor); + + io->pid = pid; + + /* Set file descriptors. */ + io->read_from_child_fd = pipefd_from_child[0]; + io->write_to_child_fd = pipefd_to_child[1]; + PIPE_FD_CLOSE(pipefd_from_child[1]); + PIPE_FD_CLOSE(pipefd_to_child[0]); + sss_fd_nonblocking(io->read_from_child_fd); + sss_fd_nonblocking(io->write_to_child_fd); + + /* Add io to pid:io hash table. */ + io_key = talloc_asprintf(tmp_ctx, "%d", pid); + if (io_key == NULL) { + ret = ENOMEM; + goto done; + } + + ret = sss_ptr_hash_add(kr->krb5_ctx->io_table, io_key, io, + struct child_io_fds); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to add child io to hash table " + "[%d]: %s\n", ret, sss_strerror(ret)); + goto done; + } + + /* Setup child's keep alive timeout for open file descriptors. This timeout + * is quite big to allow additional user interactions when the child is kept + * alive for further communication. */ + tv = tevent_timeval_current_ofs(300, 0); + te = tevent_add_timer(ev, io, tv, child_keep_alive_timeout, io); + if (te == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to setup child timeout\n"); + ret = ENOMEM; + goto done; + } + + /* Setup the child handler. It will free io and remove it from the hash + * table when it exits. */ + ret = child_handler_setup(ev, pid, child_exited, io, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Could not set up child signal handler " + "[%d]: %s\n", ret, sss_strerror(ret)); + goto done; + } + + /* Steal the io pair so it can outlive this request if needed. */ + talloc_steal(kr->krb5_ctx->io_table, io); + + *_child_pid = pid; + *_io = io; + + ret = EOK; + +done: + if (ret != EOK) { + PIPE_CLOSE(pipefd_from_child); + PIPE_CLOSE(pipefd_to_child); + krb5_child_terminate(pid); + } + + talloc_free(tmp_ctx); + return ret; +} + +static void handle_child_step(struct tevent_req *subreq); +static void handle_child_done(struct tevent_req *subreq); + +struct tevent_req *handle_child_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct krb5child_req *kr) +{ + struct tevent_req *req, *subreq; + struct handle_child_state *state; + char *io_key; + int ret; + struct io_buffer *buf = NULL; + + req = tevent_req_create(mem_ctx, &state, struct handle_child_state); + if (req == NULL) { + return NULL; + } + + if (kr->krb5_ctx->io_table == NULL) { + /* Create IO/pipe table if it does not exist. */ + kr->krb5_ctx->io_table = sss_ptr_hash_create(kr->krb5_ctx, NULL, NULL); + if (kr->krb5_ctx->io_table == NULL) { + ret = ENOMEM; + goto fail; + } + } + + state->ev = ev; + state->kr = kr; + state->buf = NULL; + state->len = 0; + state->child_pid = -1; + state->timeout_handler = NULL; + + ret = create_send_buffer(kr, &buf); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "create_send_buffer failed.\n"); + goto fail; + } + + if (kr->pd->child_pid == 0) { + /* Create new child. */ + ret = fork_child(ev, kr, &state->child_pid, &state->io); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "fork_child failed.\n"); + goto fail; + } + + /* Setup timeout. If failed, terminate the child process. */ + ret = activate_child_timeout_handler(req, ev, + dp_opt_get_int(kr->krb5_ctx->opts, KRB5_AUTH_TIMEOUT)); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to setup child timeout " + "[%d]: %s\n", ret, sss_strerror(ret)); + krb5_child_terminate(state->child_pid); + goto fail; + } + } else { + /* Continue talking to an existing child. */ + io_key = talloc_asprintf(state, "%d", kr->pd->child_pid); + if (io_key == NULL) { + ret = ENOMEM; + goto fail; + } + + state->io = sss_ptr_hash_lookup(kr->krb5_ctx->io_table, io_key, + struct child_io_fds); + if (state->io == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to locate pipe for child pid=%s\n", + io_key); + ret = ENOENT; + goto fail; + } + } + + state->io->in_use = true; + subreq = write_pipe_safe_send(state, ev, buf->data, buf->size, + state->io->write_to_child_fd); + if (!subreq) { + ret = ENOMEM; + goto fail; + } + tevent_req_set_callback(subreq, handle_child_step, req); + + return req; + +fail: + tevent_req_error(req, ret); + tevent_req_post(req, ev); + return req; +} + +static void handle_child_step(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, + struct tevent_req); + struct handle_child_state *state = tevent_req_data(req, + struct handle_child_state); + int ret; + + ret = write_pipe_safe_recv(subreq); + talloc_zfree(subreq); + if (ret != EOK) { + goto done; + } + + subreq = read_pipe_safe_send(state, state->ev, + state->io->read_from_child_fd); + if (!subreq) { + ret = ENOMEM; + goto done; + } + tevent_req_set_callback(subreq, handle_child_done, req); + +done: + if (ret != EOK) { + state->io->in_use = false; + if (state->io->child_exited) { + talloc_free(state->io); + } + + tevent_req_error(req, ret); + } +} + +static void handle_child_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, + struct tevent_req); + struct handle_child_state *state = tevent_req_data(req, + struct handle_child_state); + int ret; + + talloc_zfree(state->timeout_handler); + + ret = read_pipe_safe_recv(subreq, state, &state->buf, &state->len); + state->io->in_use = false; + talloc_zfree(subreq); + if (ret != EOK) { + goto done; + } + +done: + state->io->in_use = false; + if (state->io->child_exited) { + talloc_free(state->io); + } + + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +int handle_child_recv(struct tevent_req *req, TALLOC_CTX *mem_ctx, + uint8_t **buf, ssize_t *len) +{ + struct handle_child_state *state = tevent_req_data(req, + struct handle_child_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + *buf = talloc_move(mem_ctx, &state->buf); + *len = state->len; + + return EOK; +} + +static const char *krb5_child_response_type_to_str(int32_t type) +{ + switch (type) { + case SSS_PAM_ENV_ITEM: + return "Env variable to be set with pam_putenv(3)"; + case SSS_PAM_USER_INFO: + return "Message to be displayed to the user"; + case SSS_OTP: + return "Authtok was a OTP"; + case SSS_PAM_TEXT_MSG: + return "Plain text message to be displayed to the user"; + case SSS_PAM_OTP_INFO: + return "OTP info"; + case SSS_PASSWORD_PROMPTING: + return "Password prompting is possible"; + case SSS_CERT_AUTH_PROMPTING: + return "Certificate based authentication is available"; + case SSS_KRB5_INFO_TGT_LIFETIME: + return "TGT lifetime info"; + case SSS_KRB5_INFO_UPN: + return "UPN info"; + case SSS_CHILD_KEEP_ALIVE: + return "Keep alive"; + case SSS_PAM_OAUTH2_INFO: + return "OAuth2 info"; + case SSS_PAM_PASSKEY_INFO: + return "Passkey info"; + case SSS_PAM_PASSKEY_KRB_INFO: + return "Passkey kerberos info"; + } + + DEBUG(SSSDBG_MINOR_FAILURE, "Unexpected response type %d\n", type); + return "-unexpected-"; +} + +errno_t +parse_krb5_child_response(TALLOC_CTX *mem_ctx, uint8_t *buf, ssize_t len, + struct pam_data *pd, int pwd_exp_warning, + struct krb5_child_response **_res) +{ + ssize_t pref_len; + size_t p; + errno_t ret; + bool skip; + char *ccname = NULL; + size_t ccname_len = 0; + int32_t msg_status; + int32_t msg_type; + int32_t msg_len; + int64_t time_data; + struct tgt_times tgtt; + uint32_t expiration; + uint32_t msg_subtype; + struct krb5_child_response *res; + const char *upn = NULL; + size_t upn_len = 0; + bool otp = false; + + if ((size_t) len < sizeof(int32_t)) { + DEBUG(SSSDBG_CRIT_FAILURE, "message too short.\n"); + return EINVAL; + } + + memset(&tgtt, 0, sizeof(struct tgt_times)); + + if (pwd_exp_warning < 0) { + pwd_exp_warning = KERBEROS_PWEXPIRE_WARNING_TIME; + } + + /* A buffer with the following structure is expected. + * int32_t status of the request (required) + * message (zero or more) + * + * A message consists of: + * int32_t type of the message + * int32_t length of the following data + * uint8_t[len] data + */ + + p=0; + SAFEALIGN_COPY_INT32(&msg_status, buf+p, &p); + + while (p < len) { + skip = false; + SAFEALIGN_COPY_INT32(&msg_type, buf+p, &p); + SAFEALIGN_COPY_INT32(&msg_len, buf+p, &p); + + DEBUG(SSSDBG_TRACE_LIBS, "child response: " + "status code: %d (%s), msg type: %d (%s), len: %d\n", + msg_status, sss_strerror(msg_status), + msg_type, krb5_child_response_type_to_str(msg_type), + msg_len); + + if (msg_len > len - p) { + DEBUG(SSSDBG_CRIT_FAILURE, "message format error [%d] > [%zu].\n", + msg_len, len - p); + return EINVAL; + } + + /* We need to save the name of the credential cache file. To find it + * we check if the data part of a message starts with + * CCACHE_ENV_NAME"=". pref_len also counts the trailing '=' because + * sizeof() counts the trailing '\0' of a string. */ + pref_len = sizeof(CCACHE_ENV_NAME); + if ((msg_type == SSS_PAM_ENV_ITEM) && + (msg_len > pref_len) && + (strncmp((const char *) &buf[p], CCACHE_ENV_NAME"=", pref_len) == 0)) { + ccname = (char *) &buf[p+pref_len]; + ccname_len = msg_len-pref_len; + } + + if (msg_type == SSS_KRB5_INFO_TGT_LIFETIME && + msg_len == 4*sizeof(int64_t)) { + SAFEALIGN_COPY_INT64(&time_data, buf+p, NULL); + tgtt.authtime = int64_to_time_t(time_data); + SAFEALIGN_COPY_INT64(&time_data, buf+p+sizeof(int64_t), NULL); + tgtt.starttime = int64_to_time_t(time_data); + SAFEALIGN_COPY_INT64(&time_data, buf+p+2*sizeof(int64_t), NULL); + tgtt.endtime = int64_to_time_t(time_data); + SAFEALIGN_COPY_INT64(&time_data, buf+p+3*sizeof(int64_t), NULL); + tgtt.renew_till = int64_to_time_t(time_data); + DEBUG(SSSDBG_TRACE_LIBS, + "TGT times are [%"SPRItime"][%"SPRItime"][%"SPRItime"][%"SPRItime"].\n", + tgtt.authtime, tgtt.starttime, tgtt.endtime, tgtt.renew_till); + } + + if (msg_type == SSS_KRB5_INFO_UPN) { + upn = (char *) buf + p; + upn_len = msg_len; + } + + if (msg_type == SSS_PAM_USER_INFO) { + SAFEALIGN_COPY_UINT32(&msg_subtype, buf + p, NULL); + if (msg_subtype == SSS_PAM_USER_INFO_EXPIRE_WARN) { + SAFEALIGN_COPY_UINT32(&expiration, + buf + p + sizeof(uint32_t), NULL); + if (pwd_exp_warning > 0 && + difftime(pwd_exp_warning, expiration) < 0.0) { + skip = true; + } + } + } + + if (msg_type == SSS_OTP) { + otp = true; + skip = true; + } + + if (!skip) { + ret = pam_add_response(pd, msg_type, msg_len, &buf[p]); + if (ret != EOK) { + /* This is not a fatal error */ + DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); + } + } + + p += msg_len; + + if ((p < len) && (p + 2*sizeof(int32_t) > len)) { + DEBUG(SSSDBG_CRIT_FAILURE, + "The remainder of the message is too short.\n"); + return EINVAL; + } + } + + res = talloc_zero(mem_ctx, struct krb5_child_response); + if (!res) return ENOMEM; + + res->otp = otp; + res->msg_status = msg_status; + memcpy(&res->tgtt, &tgtt, sizeof(tgtt)); + + if (ccname) { + res->ccname = talloc_strndup(res, ccname, ccname_len); + if (res->ccname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strndup failed.\n"); + talloc_free(res); + return ENOMEM; + } + } + + if (upn != NULL) { + res->correct_upn = talloc_strndup(res, upn, upn_len); + if (res->correct_upn == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strndup failed.\n"); + talloc_free(res); + return ENOMEM; + } + } + + *_res = res; + return EOK; +} diff --git a/src/providers/krb5/krb5_common.c b/src/providers/krb5/krb5_common.c new file mode 100644 index 0000000..6498258 --- /dev/null +++ b/src/providers/krb5/krb5_common.c @@ -0,0 +1,1261 @@ +/* + SSSD + + Kerberos Provider Common Functions + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2008-2009 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <netdb.h> +#include <arpa/inet.h> +#include <ctype.h> + +#include "providers/backend.h" +#include "providers/krb5/krb5_common.h" +#include "providers/krb5/krb5_opts.h" +#include "providers/krb5/krb5_utils.h" +#include "providers/fail_over.h" + +#ifdef HAVE_KRB5_CC_COLLECTION +/* krb5 profile functions */ +#include <profile.h> +#endif + +static errno_t check_lifetime(TALLOC_CTX *mem_ctx, struct dp_option *opts, + const int opt_id, char **lifetime_str) +{ + int ret; + char *str = NULL; + krb5_deltat lifetime; + + str = dp_opt_get_string(opts, opt_id); + if (str == NULL || *str == '\0') { + DEBUG(SSSDBG_FUNC_DATA, "No lifetime configured.\n"); + *lifetime_str = NULL; + return EOK; + } + + if (isdigit(str[strlen(str)-1])) { + str = talloc_asprintf(mem_ctx, "%ss", str); + if (str == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + + ret = dp_opt_set_string(opts, opt_id, str); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "dp_opt_set_string failed.\n"); + goto done; + } + } else { + str = talloc_strdup(mem_ctx, str); + if (str == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n"); + ret = ENOMEM; + goto done; + } + } + + ret = krb5_string_to_deltat(str, &lifetime); + if (ret != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Invalid value [%s] for a lifetime.\n", str); + ret = EINVAL; + goto done; + } + + *lifetime_str = str; + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(str); + } + + return ret; +} + +#ifdef HAVE_KRB5_CC_COLLECTION +/* source default_ccache_name from krb5.conf */ +static errno_t sss_get_system_ccname_template(TALLOC_CTX *mem_ctx, + char **ccname) +{ + krb5_context ctx; + profile_t p; + char *value = NULL; + long ret; + + *ccname = NULL; + + ret = sss_krb5_init_context(&ctx); + if (ret) return ret; + + ret = krb5_get_profile(ctx, &p); + if (ret) goto done; + + ret = profile_get_string(p, "libdefaults", "default_ccache_name", + NULL, NULL, &value); + profile_release(p); + if (ret) goto done; + + if (!value) { + ret = ERR_NOT_FOUND; + goto done; + } + + *ccname = talloc_strdup(mem_ctx, value); + if (*ccname == NULL) { + ret = ENOMEM; + goto done; + } + + ret = EOK; + +done: + krb5_free_context(ctx); + free(value); + return ret; +} +#else +static errno_t sss_get_system_ccname_template(TALLOC_CTX *mem_ctx, + char **ccname) +{ + DEBUG(SSSDBG_CONF_SETTINGS, + "Your kerberos library does not support the default_ccache_name " + "option or the profile library. Please use krb5_ccname_template " + "in sssd.conf if you want to change the default\n"); + *ccname = NULL; + return ERR_NOT_FOUND; +} +#endif + +static void sss_check_cc_template(const char *cc_template) +{ + size_t template_len; + + template_len = strlen(cc_template); + if (template_len >= 6 && + strcmp(cc_template + (template_len - 6), "XXXXXX") != 0) { + DEBUG(SSSDBG_CONF_SETTINGS, "ccache file name template [%s] doesn't " + "contain randomizing characters (XXXXXX), file might not " + "be rewritable\n", cc_template); + } +} + +errno_t sss_krb5_check_options(struct dp_option *opts, + struct sss_domain_info *dom, + struct krb5_ctx *krb5_ctx) +{ + TALLOC_CTX *tmp_ctx = NULL; + int ret; + const char *realm; + const char *dummy; + char *ccname; + + if (opts == NULL || dom == NULL || krb5_ctx == NULL) { + return EINVAL; + } + + tmp_ctx = talloc_new(NULL); + if (!tmp_ctx) { + ret = ENOMEM; + goto done; + } + + realm = dp_opt_get_cstring(opts, KRB5_REALM); + if (realm == NULL) { + ret = dp_opt_set_string(opts, KRB5_REALM, dom->name); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "dp_opt_set_string failed.\n"); + goto done; + } + realm = dom->name; + } + + krb5_ctx->realm = talloc_strdup(krb5_ctx, realm); + if (krb5_ctx->realm == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to set realm, krb5_child might not work as expected.\n"); + } + + ret = check_lifetime(krb5_ctx, opts, KRB5_RENEWABLE_LIFETIME, + &krb5_ctx->rlife_str); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to check value of krb5_renewable_lifetime. [%d][%s]\n", + ret, strerror(ret)); + goto done; + } + + ret = check_lifetime(krb5_ctx, opts, KRB5_LIFETIME, + &krb5_ctx->lifetime_str); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to check value of krb5_lifetime. [%d][%s]\n", + ret, strerror(ret)); + goto done; + } + + krb5_ctx->use_fast_str = dp_opt_get_cstring(opts, KRB5_USE_FAST); + if (krb5_ctx->use_fast_str != NULL) { + ret = check_fast(krb5_ctx->use_fast_str, &krb5_ctx->use_fast); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "check_fast failed.\n"); + goto done; + } + + if (krb5_ctx->use_fast) { + krb5_ctx->fast_principal = dp_opt_get_cstring(opts, + KRB5_FAST_PRINCIPAL); + krb5_ctx->fast_use_anonymous_pkinit = dp_opt_get_bool(opts, + KRB5_FAST_USE_ANONYMOUS_PKINIT); + } + } + + /* In contrast to MIT KDCs AD does not automatically canonicalize the + * enterprise principal in an AS request but requires the canonicalize + * flags to be set. To be on the safe side we always enable + * canonicalization if enterprise principals are used. */ + krb5_ctx->canonicalize = false; + if (dp_opt_get_bool(opts, KRB5_CANONICALIZE) + || dp_opt_get_bool(opts, KRB5_USE_ENTERPRISE_PRINCIPAL)) { + krb5_ctx->canonicalize = true; + } + + dummy = dp_opt_get_cstring(opts, KRB5_KDC); + if (dummy == NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, "No KDC explicitly configured, using defaults.\n"); + } + + dummy = dp_opt_get_cstring(opts, KRB5_KPASSWD); + if (dummy == NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, "No kpasswd server explicitly configured, " + "using the KDC or defaults.\n"); + } + + ccname = dp_opt_get_string(opts, KRB5_CCNAME_TMPL); + if (ccname != NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, + "The credential ccache name template has been explicitly set " + "in sssd.conf, it is recommended to set default_ccache_name " + "in krb5.conf instead so that a system default is used\n"); + ccname = talloc_strdup(tmp_ctx, ccname); + if (!ccname) { + ret = ENOMEM; + goto done; + } + } else { + ret = sss_get_system_ccname_template(tmp_ctx, &ccname); + if (ret && ret != ERR_NOT_FOUND) { + goto done; + } + if (ret == ERR_NOT_FOUND) { + /* Use fallback default */ + ccname = talloc_strdup(tmp_ctx, DEFAULT_CCNAME_TEMPLATE); + if (!ccname) { + ret = ENOMEM; + goto done; + } + } + + /* set back in opts */ + ret = dp_opt_set_string(opts, KRB5_CCNAME_TMPL, ccname); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "dp_opt_set_string failed.\n"); + goto done; + } + } + + if ((ccname[0] == '/') || (strncmp(ccname, "FILE:", 5) == 0)) { + DEBUG(SSSDBG_CONF_SETTINGS, "ccache is of type FILE\n"); + /* warn if the file type (which is usually created in a sticky bit + * laden directory) does not have randomizing characters */ + sss_check_cc_template(ccname); + + if (ccname[0] == '/') { + /* /path/to/cc prepend FILE: */ + DEBUG(SSSDBG_CONF_SETTINGS, "The ccname template was " + "missing an explicit type, but is an absolute " + "path specifier. Assuming FILE:\n"); + + ccname = talloc_asprintf(tmp_ctx, "FILE:%s", ccname); + if (!ccname) { + ret = ENOMEM; + goto done; + } + + ret = dp_opt_set_string(opts, KRB5_CCNAME_TMPL, ccname); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "dp_opt_set_string failed.\n"); + goto done; + } + } + } + + ret = EOK; + +done: + talloc_free(tmp_ctx); + return ret; +} + +errno_t krb5_try_kdcip(struct confdb_ctx *cdb, const char *conf_path, + struct dp_option *opts, int opt_id) +{ + char *krb5_servers = NULL; + errno_t ret; + + krb5_servers = dp_opt_get_string(opts, opt_id); + if (krb5_servers == NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, + "No KDC found in configuration, trying legacy option\n"); + ret = confdb_get_string(cdb, NULL, conf_path, + "krb5_kdcip", NULL, &krb5_servers); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "confdb_get_string failed.\n"); + return ret; + } + + if (krb5_servers != NULL) + { + ret = dp_opt_set_string(opts, opt_id, krb5_servers); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "dp_opt_set_string failed.\n"); + talloc_free(krb5_servers); + return ret; + } + + DEBUG(SSSDBG_CONF_SETTINGS, + "Set krb5 server [%s] based on legacy krb5_kdcip option\n", + krb5_servers); + DEBUG(SSSDBG_FATAL_FAILURE, + "Your configuration uses the deprecated option " + "'krb5_kdcip' to specify the KDC. Please change the " + "configuration to use the 'krb5_server' option " + "instead.\n"); + talloc_free(krb5_servers); + } + } + + return EOK; +} + +errno_t sss_krb5_get_options(TALLOC_CTX *memctx, struct confdb_ctx *cdb, + const char *conf_path, struct dp_option **_opts) +{ + int ret; + struct dp_option *opts; + + ret = dp_get_options(memctx, cdb, conf_path, default_krb5_opts, + KRB5_OPTS, &opts); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "dp_get_options failed.\n"); + goto done; + } + + /* If there is no KDC, try the deprecated krb5_kdcip option, too */ + /* FIXME - this can be removed in a future version */ + ret = krb5_try_kdcip(cdb, conf_path, opts, KRB5_KDC); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_krb5_try_kdcip failed.\n"); + goto done; + } + + *_opts = opts; + ret = EOK; + +done: + if (ret != EOK) { + talloc_zfree(opts); + } + + return ret; +} + +void sss_krb5_parse_lookahead(const char *param, size_t *primary, size_t *backup) +{ + int ret; + + if (primary == NULL || backup == NULL) { + return; + } + + *primary = SSS_KRB5_LOOKAHEAD_PRIMARY_DEFAULT; + *backup = SSS_KRB5_LOOKAHEAD_BACKUP_DEFAULT; + + if (param == NULL) { + return; + } + + if (strchr(param, ':')) { + ret = sscanf(param, "%zu:%zu", primary, backup); + if (ret != 2) { + DEBUG(SSSDBG_MINOR_FAILURE, "Could not parse krb5_kdcinfo_lookahead!\n"); + } + } else { + ret = sscanf(param, "%zu", primary); + if (ret != 1) { + DEBUG(SSSDBG_MINOR_FAILURE, "Could not parse krb5_kdcinfo_lookahead!\n"); + } + } + + DEBUG(SSSDBG_CONF_SETTINGS, + "Option krb5_kdcinfo_lookahead set to %zu:%zu", + *primary, *backup); +} + + +static int remove_info_files_destructor(void *p) +{ + int ret; + struct remove_info_files_ctx *ctx = talloc_get_type(p, + struct remove_info_files_ctx); + + ret = remove_krb5_info_files(ctx, ctx->realm); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "remove_krb5_info_files failed.\n"); + } + ctx->krb5_service->removal_callback_available = false; + + return 0; +} + +static errno_t +krb5_add_krb5info_offline_callback(struct krb5_service *krb5_service) +{ + int ret; + struct remove_info_files_ctx *ctx = NULL; + + if (krb5_service == NULL || krb5_service->name == NULL + || krb5_service->realm == NULL + || krb5_service->be_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing KDC service name or realm!\n"); + return EINVAL; + } + + if (krb5_service->removal_callback_available) { + DEBUG(SSSDBG_TRACE_ALL, + "Removal callback already available for service [%s].\n", + krb5_service->name); + return EOK; + } + + ctx = talloc_zero(krb5_service->be_ctx, struct remove_info_files_ctx); + if (ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zfree failed.\n"); + return ENOMEM; + } + + ctx->realm = talloc_strdup(ctx, krb5_service->realm); + if (ctx->realm == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed!\n"); + ret = ENOMEM; + goto done; + } + + ctx->be_ctx = krb5_service->be_ctx; + ctx->krb5_service = krb5_service; + ctx->kdc_service_name = talloc_strdup(ctx, krb5_service->name); + if (ctx->kdc_service_name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed!\n"); + ret = ENOMEM; + goto done; + } + + ret = be_add_offline_cb(ctx, krb5_service->be_ctx, + remove_krb5_info_files_callback, ctx, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "be_add_offline_cb failed.\n"); + goto done; + } + + talloc_set_destructor((TALLOC_CTX *) ctx, remove_info_files_destructor); + krb5_service->removal_callback_available = true; + + ret = EOK; + +done: + if (ret != EOK) { + talloc_zfree(ctx); + } + + return ret; +} + +static errno_t write_krb5info_file_contents(struct krb5_service *krb5_service, + const char *contents, + const char *service) +{ + int ret; + int fd = -1; + char *tmp_name = NULL; + char *krb5info_name = NULL; + TALLOC_CTX *tmp_ctx = NULL; + const char *name_tmpl = NULL; + size_t server_len; + ssize_t written; + + if (krb5_service == NULL || krb5_service->realm == NULL + || *krb5_service->realm == '\0' + || contents == NULL || *contents == '\0' + || service == NULL || *service == '\0') { + DEBUG(SSSDBG_CRIT_FAILURE, + "Missing or empty realm, server or service.\n"); + return EINVAL; + } + + if (sss_krb5_realm_has_proxy(krb5_service->realm)) { + DEBUG(SSSDBG_CONF_SETTINGS, + "KDC Proxy available for realm [%s], no kdcinfo file created.\n", + krb5_service->realm); + return EOK; + } + + if (strcmp(service, SSS_KRB5KDC_FO_SRV) == 0) { + name_tmpl = KDCINFO_TMPL; + } else if (strcmp(service, SSS_KRB5KPASSWD_FO_SRV) == 0) { + name_tmpl = KPASSWDINFO_TMPL; + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "Unsupported service [%s].\n", service); + return EINVAL; + } + + server_len = strlen(contents); + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed.\n"); + return ENOMEM; + } + + tmp_name = talloc_asprintf(tmp_ctx, PUBCONF_PATH"/.krb5info_dummy_XXXXXX"); + if (tmp_name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + + krb5info_name = talloc_asprintf(tmp_ctx, name_tmpl, krb5_service->realm); + if (krb5info_name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + + fd = sss_unique_file(tmp_ctx, tmp_name, &ret); + if (fd == -1) { + DEBUG(SSSDBG_CRIT_FAILURE, + "sss_unique_file failed [%d][%s].\n", ret, strerror(ret)); + goto done; + } + + errno = 0; + written = sss_atomic_write_s(fd, discard_const(contents), server_len); + if (written == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "write failed [%d][%s].\n", ret, strerror(ret)); + goto done; + } + + if (written != server_len) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Write error, wrote [%zd] bytes, expected [%zu]\n", + written, server_len); + ret = EIO; + goto done; + } + + ret = fchmod(fd, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "fchmod failed [%d][%s].\n", ret, strerror(ret)); + goto done; + } + + ret = close(fd); + fd = -1; + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "close failed [%d][%s].\n", ret, strerror(ret)); + goto done; + } + + ret = rename(tmp_name, krb5info_name); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "rename failed [%d][%s].\n", ret, strerror(ret)); + goto done; + } + + ret = krb5_add_krb5info_offline_callback(krb5_service); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to add offline callback, krb5info " + "file might not be removed properly.\n"); + } + + ret = EOK; +done: + if (fd != -1) { + close(fd); + } + + talloc_free(tmp_ctx); + return ret; +} + +errno_t write_krb5info_file(struct krb5_service *krb5_service, + const char **server_list, + const char *service) +{ + int i; + errno_t ret; + TALLOC_CTX *tmp_ctx = NULL; + char *contents = NULL; + + if (krb5_service == NULL || server_list == NULL || service == NULL) { + return EINVAL; + } + + if (server_list[0] == NULL) { + return EOK; + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + return ENOMEM; + } + + contents = talloc_strdup(tmp_ctx, ""); + if (contents == NULL) { + ret = ENOMEM; + goto done; + } + + i = 0; + do { + contents = talloc_asprintf_append(contents, "%s\n", server_list[i]); + if (contents == NULL) { + ret = ENOMEM; + goto done; + } + i++; + } while (server_list[i] != NULL); + + ret = write_krb5info_file_contents(krb5_service, contents, service); +done: + talloc_free(tmp_ctx); + return ret; +} + +static const char* fo_server_address_or_name(TALLOC_CTX *tmp_ctx, struct fo_server *server) +{ + struct resolv_hostent *srvaddr; + char *address; + + if (!server) return NULL; + + srvaddr = fo_get_server_hostent(server); + if (srvaddr) { + address = resolv_get_string_address(tmp_ctx, srvaddr); + if (address) { + return sss_escape_ip_address(tmp_ctx, + srvaddr->family, + address); + } + } + + address = discard_const(fo_get_server_name(server)); + if (address != NULL && fo_get_use_search_list(server) == false) { + if (address[strlen(address)-1] != '.') { + address = talloc_asprintf(tmp_ctx, "%s.", address); + if (address == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "talloc_asprintf failed.\n"); + } + } + } + return address; +} + +errno_t write_krb5info_file_from_fo_server(struct krb5_service *krb5_service, + struct fo_server *server, + bool force_default_port, + const char *service, + bool (*filter)(struct fo_server *)) +{ + TALLOC_CTX *tmp_ctx = NULL; + const char **server_list; + size_t server_idx; + struct fo_server *item; + int primary; + int port; + const char *address; + errno_t ret; + size_t n_lookahead_primary; + size_t n_lookahead_backup; + + if (krb5_service == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "The krb5_service must not be NULL!\n"); + return EINVAL; + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed\n"); + return ENOMEM; + } + + n_lookahead_primary = krb5_service->lookahead_primary; + n_lookahead_backup = krb5_service->lookahead_backup; + + server_idx = 0; + server_list = talloc_zero_array(tmp_ctx, + const char *, + fo_server_count(server) + 1); + if (server_list == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero_array failed\n"); + talloc_free(tmp_ctx); + return ENOMEM; + } + + if (filter == NULL || filter(server) == false) { + address = fo_server_address_or_name(tmp_ctx, server); + if (address) { + if (!force_default_port) { + port = fo_get_server_port(server); + if (port != 0) { + address = talloc_asprintf(tmp_ctx, "%s:%d", address, port); + if (address == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + talloc_free(tmp_ctx); + return ENOMEM; + } + } + } + + server_list[server_idx++] = address; + if (fo_is_server_primary(server)) { + if (n_lookahead_primary > 0) { + n_lookahead_primary--; + } + } else { + if (n_lookahead_backup > 0) { + n_lookahead_backup--; + } + } + } else { + DEBUG(SSSDBG_CRIT_FAILURE, + "Server without name and address found in list.\n"); + } + } + + for (primary = 1; primary >= 0; --primary) { + for (item = fo_server_next(server) ? fo_server_next(server) : fo_server_first(server); + item != server; + item = fo_server_next(item) ? fo_server_next(item) : fo_server_first(item)) { + + if (primary && n_lookahead_primary == 0) break; + if (!primary && n_lookahead_backup == 0) break; + if (primary && !fo_is_server_primary(item)) continue; + if (!primary && fo_is_server_primary(item)) continue; + if (filter != NULL && filter(item)) continue; + + address = fo_server_address_or_name(tmp_ctx, item); + if (address == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Server without name and address found in list.\n"); + continue; + } + + if (!force_default_port) { + port = fo_get_server_port(item); + if (port != 0) { + address = talloc_asprintf(tmp_ctx, "%s:%d", address, port); + if (address == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + talloc_free(tmp_ctx); + return ENOMEM; + } + } + } + + server_list[server_idx++] = address; + if (primary) { + n_lookahead_primary--; + } else { + n_lookahead_backup--; + } + } + } + if (server_list[0] == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "There is no server that can be written into kdc info file.\n"); + ret = EINVAL; + } else { + ret = write_krb5info_file(krb5_service, + server_list, + service); + } + talloc_free(tmp_ctx); + return ret; +} + + +static void krb5_resolve_callback(void *private_data, struct fo_server *server) +{ + struct krb5_service *krb5_service; + int ret; + + krb5_service = talloc_get_type(private_data, struct krb5_service); + if (!krb5_service) { + DEBUG(SSSDBG_CRIT_FAILURE, "Bad private_data\n"); + return; + } + + if (krb5_service->write_kdcinfo) { + ret = write_krb5info_file_from_fo_server(krb5_service, + server, + false, + krb5_service->name, + NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "write to %s/kdcinfo.%s failed, authentication might fail.\n", + PUBCONF_PATH, krb5_service->realm); + } + } +} + +static errno_t _krb5_servers_init(struct be_ctx *ctx, + struct krb5_service *service, + const char *service_name, + const char *servers, + bool primary) +{ + TALLOC_CTX *tmp_ctx; + char **list = NULL; + errno_t ret = 0; + int i; + char *port_str; + long port; + char *server_spec; + char *endptr; + struct servent *servent; + + tmp_ctx = talloc_new(NULL); + if (!tmp_ctx) { + return ENOMEM; + } + + ret = split_on_separator(tmp_ctx, servers, ',', true, true, &list, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to parse server list!\n"); + goto done; + } + + for (i = 0; list[i]; i++) { + talloc_steal(service, list[i]); + server_spec = talloc_strdup(service, list[i]); + if (!server_spec) { + ret = ENOMEM; + goto done; + } + + if (be_fo_is_srv_identifier(server_spec)) { + if (!primary) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to add server [%s] to failover service: " + "SRV resolution only allowed for primary servers!\n", + list[i]); + continue; + } + + ret = be_fo_add_srv_server(ctx, service_name, service_name, NULL, + BE_FO_PROTO_UDP, true, NULL); + if (ret) { + DEBUG(SSSDBG_FATAL_FAILURE, "Failed to add server\n"); + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Added service lookup\n"); + continue; + } + + /* Do not try to get port number if last character is ']' */ + if (server_spec[strlen(server_spec) - 1] != ']') { + port_str = strrchr(server_spec, ':'); + } else { + port_str = NULL; + } + + if (port_str == NULL) { + port = 0; + } else { + *port_str = '\0'; + ++port_str; + if (isdigit(*port_str)) { + errno = 0; + port = strtol(port_str, &endptr, 10); + if (errno != 0) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, "strtol failed on [%s]: [%d][%s].\n", port_str, + ret, strerror(ret)); + goto done; + } + if (*endptr != '\0') { + DEBUG(SSSDBG_CRIT_FAILURE, "Found additional characters [%s] in port number " + "[%s].\n", endptr, port_str); + ret = EINVAL; + goto done; + } + + if (port < 1 || port > 65535) { + DEBUG(SSSDBG_CRIT_FAILURE, "Illegal port number [%ld].\n", port); + ret = EINVAL; + goto done; + } + } else if (isalpha(*port_str)) { + servent = getservbyname(port_str, NULL); + if (servent == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "getservbyname cannot find service [%s].\n", + port_str); + ret = EINVAL; + goto done; + } + + port = servent->s_port; + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "Unsupported port specifier in [%s].\n", list[i]); + ret = EINVAL; + goto done; + } + } + + /* It could be ipv6 address in square brackets. Remove + * the brackets if needed. */ + ret = remove_ipv6_brackets(server_spec); + if (ret != EOK) { + goto done; + } + + ret = be_fo_add_server(ctx, service_name, server_spec, (int) port, + list[i], primary); + if (ret && ret != EEXIST) { + DEBUG(SSSDBG_FATAL_FAILURE, "Failed to add server\n"); + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Added Server %s\n", list[i]); + } + +done: + talloc_free(tmp_ctx); + return ret; +} + +static inline errno_t +krb5_primary_servers_init(struct be_ctx *ctx, struct krb5_service *service, + const char *service_name, const char *servers) +{ + return _krb5_servers_init(ctx, service, service_name, servers, true); +} + +static inline errno_t +krb5_backup_servers_init(struct be_ctx *ctx, struct krb5_service *service, + const char *service_name, const char *servers) +{ + return _krb5_servers_init(ctx, service, service_name, servers, false); +} + +static int krb5_user_data_cmp(void *ud1, void *ud2) +{ + return strcasecmp((char*) ud1, (char*) ud2); +} + +struct krb5_service *krb5_service_new(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + const char *service_name, + const char *realm, + bool use_kdcinfo, + size_t n_lookahead_primary, + size_t n_lookahead_backup) +{ + struct krb5_service *service; + + service = talloc_zero(mem_ctx, struct krb5_service); + if (service == NULL) { + return NULL; + } + + service->name = talloc_strdup(service, service_name); + if (service->name == NULL) { + talloc_free(service); + return NULL; + } + + service->realm = talloc_strdup(service, realm); + if (service->realm == NULL) { + talloc_free(service); + return NULL; + } + + DEBUG(SSSDBG_CONF_SETTINGS, + "write_kdcinfo for realm %s set to %s\n", + realm, + use_kdcinfo ? "true" : "false"); + service->write_kdcinfo = use_kdcinfo; + service->lookahead_primary = n_lookahead_primary; + service->lookahead_backup = n_lookahead_backup; + + service->be_ctx = be_ctx; + return service; +} + +int krb5_service_init(TALLOC_CTX *memctx, struct be_ctx *ctx, + const char *service_name, + const char *primary_servers, + const char *backup_servers, + const char *realm, + bool use_kdcinfo, + size_t n_lookahead_primary, + size_t n_lookahead_backup, + struct krb5_service **_service) +{ + TALLOC_CTX *tmp_ctx; + struct krb5_service *service; + int ret; + + tmp_ctx = talloc_new(memctx); + if (!tmp_ctx) { + return ENOMEM; + } + + service = krb5_service_new(tmp_ctx, ctx, service_name, realm, use_kdcinfo, + n_lookahead_primary, n_lookahead_backup); + if (!service) { + ret = ENOMEM; + goto done; + } + + ret = be_fo_add_service(ctx, service_name, krb5_user_data_cmp); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to create failover service!\n"); + goto done; + } + + if (!primary_servers) { + DEBUG(SSSDBG_CONF_SETTINGS, + "No primary servers defined, using service discovery\n"); + primary_servers = BE_SRV_IDENTIFIER; + } + + ret = krb5_primary_servers_init(ctx, service, service_name, primary_servers); + if (ret != EOK) { + goto done; + } + + if (backup_servers) { + ret = krb5_backup_servers_init(ctx, service, service_name, + backup_servers); + if (ret != EOK) { + goto done; + } + } + + ret = be_fo_service_add_callback(memctx, ctx, service_name, + krb5_resolve_callback, service); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to add failover callback!\n"); + goto done; + } + + ret = EOK; + +done: + if (ret == EOK) { + *_service = talloc_steal(memctx, service); + } + talloc_zfree(tmp_ctx); + return ret; +} + + +errno_t remove_krb5_info_files(TALLOC_CTX *mem_ctx, const char *realm) +{ + int ret; + errno_t err; + char *file; + + file = talloc_asprintf(mem_ctx, KDCINFO_TMPL, realm); + if(file == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + return ENOMEM; + } + + errno = 0; + ret = unlink(file); + if (ret == -1) { + err = errno; + DEBUG(SSSDBG_FUNC_DATA, "Could not remove [%s], [%d][%s]\n", file, + err, strerror(err)); + } + + file = talloc_asprintf(mem_ctx, KPASSWDINFO_TMPL, realm); + if(file == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + return ENOMEM; + } + + errno = 0; + ret = unlink(file); + if (ret == -1) { + err = errno; + DEBUG(SSSDBG_FUNC_DATA, "Could not remove [%s], [%d][%s]\n", file, + err, strerror(err)); + } + + return EOK; +} + +void remove_krb5_info_files_callback(void *pvt) +{ + int ret; + struct remove_info_files_ctx *ctx = talloc_get_type(pvt, + struct remove_info_files_ctx); + + ret = be_fo_run_callbacks_at_next_request(ctx->be_ctx, + ctx->kdc_service_name); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "be_fo_run_callbacks_at_next_request(kdc_service_name) failed, " + "krb5 info files will not be removed, because " + "it is unclear if they will be recreated properly.\n"); + return; + } + if (ctx->kpasswd_service_name != NULL) { + ret = be_fo_run_callbacks_at_next_request(ctx->be_ctx, + ctx->kpasswd_service_name); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "be_fo_run_callbacks_at_next_request(kpasswd_service_name) failed, " + "krb5 info files will not be removed, because " + "it is unclear if they will be recreated properly.\n"); + return; + } + } + + /* Freeing the remove_info_files_ctx will remove the related krb5info + * file. Additionally the callback from the list of callbacks is removed, + * it will be added again when a new krb5info file is created. */ + talloc_free(ctx); +} + +errno_t krb5_get_simple_upn(TALLOC_CTX *mem_ctx, struct krb5_ctx *krb5_ctx, + struct sss_domain_info *dom, const char *username, + const char *user_dom, char **_upn) +{ + const char *realm = NULL; + char *uc_dom = NULL; + char *upn; + char *name; + TALLOC_CTX *tmp_ctx = NULL; + errno_t ret; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed.\n"); + return ENOMEM; + } + + if (user_dom != NULL && dom->name != NULL && + strcasecmp(dom->name, user_dom) != 0) { + uc_dom = get_uppercase_realm(tmp_ctx, user_dom); + if (uc_dom == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "get_uppercase_realm failed.\n"); + ret = ENOMEM; + goto done; + } + } else { + realm = dp_opt_get_cstring(krb5_ctx->opts, KRB5_REALM); + if (realm == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Missing Kerberos realm.\n"); + ret = ENOMEM; + goto done; + } + } + + /* The internal username is qualified, but we are only interested in + * the name part + */ + ret = sss_parse_internal_fqname(tmp_ctx, username, &name, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Could not parse [%s] into name and " + "domain components, login might fail\n", username); + upn = talloc_strdup(tmp_ctx, username); + } else { + /* NOTE: this is a hack, works only in some environments */ + upn = talloc_asprintf(tmp_ctx, "%s@%s", + name, realm != NULL ? realm : uc_dom); + } + + if (upn == NULL) { + ret = ENOMEM; + goto done; + } + + DEBUG(SSSDBG_TRACE_ALL, "Using simple UPN [%s].\n", upn); + *_upn = talloc_steal(mem_ctx, upn); + ret = EOK; +done: + talloc_free(tmp_ctx); + return ret; +} + +errno_t compare_principal_realm(const char *upn, const char *realm, + bool *different_realm) +{ + char *at_sign; + + if (upn == NULL || realm == NULL || different_realm == NULL || + *upn == '\0' || *realm == '\0') { + return EINVAL; + } + + at_sign = strchr(upn, '@'); + + if (at_sign == NULL) { + return EINVAL; + } + + if (strcmp(realm, at_sign + 1) == 0) { + *different_realm = false; + } else { + *different_realm = true; + } + + return EOK; +} diff --git a/src/providers/krb5/krb5_common.h b/src/providers/krb5/krb5_common.h new file mode 100644 index 0000000..80552ab --- /dev/null +++ b/src/providers/krb5/krb5_common.h @@ -0,0 +1,249 @@ +/* + SSSD + + Kerberos Backend, common header file + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2009 Red Hat + + + 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 <http://www.gnu.org/licenses/>. +*/ + +#ifndef __KRB5_COMMON_H__ +#define __KRB5_COMMON_H__ + +#include "config.h" +#include <stdbool.h> + +#include "providers/backend.h" +#include "util/util.h" +#include "util/sss_krb5.h" + +#define KDCINFO_TMPL PUBCONF_PATH"/kdcinfo.%s" +#define KPASSWDINFO_TMPL PUBCONF_PATH"/kpasswdinfo.%s" + +#define SSS_KRB5KDC_FO_SRV "KERBEROS" +#define SSS_KRB5KPASSWD_FO_SRV "KPASSWD" +#define SSS_KRB5_LOOKAHEAD_PRIMARY_DEFAULT 3 +#define SSS_KRB5_LOOKAHEAD_BACKUP_DEFAULT 1 + +enum krb5_opts { + KRB5_KDC = 0, + KRB5_BACKUP_KDC, + KRB5_REALM, + KRB5_CCACHEDIR, + KRB5_CCNAME_TMPL, + KRB5_AUTH_TIMEOUT, + KRB5_KEYTAB, + KRB5_VALIDATE, + KRB5_KPASSWD, + KRB5_BACKUP_KPASSWD, + KRB5_STORE_PASSWORD_IF_OFFLINE, + KRB5_RENEWABLE_LIFETIME, + KRB5_LIFETIME, + KRB5_RENEW_INTERVAL, + KRB5_USE_FAST, + KRB5_FAST_PRINCIPAL, + KRB5_FAST_USE_ANONYMOUS_PKINIT, + KRB5_CANONICALIZE, + KRB5_USE_ENTERPRISE_PRINCIPAL, + KRB5_USE_KDCINFO, + KRB5_KDCINFO_LOOKAHEAD, + KRB5_MAP_USER, + KRB5_USE_SUBDOMAIN_REALM, + + KRB5_OPTS +}; + +typedef enum { INIT_PW, INIT_KT, RENEW, VALIDATE } action_type; + +struct krb5_service { + struct be_ctx *be_ctx; + char *name; + char *realm; + bool write_kdcinfo; + size_t lookahead_primary; + size_t lookahead_backup; + bool removal_callback_available; +}; + +struct fo_service; +struct deferred_auth_ctx; +struct renew_tgt_ctx; + +enum krb5_config_type { + K5C_GENERIC, + K5C_IPA_CLIENT, + K5C_IPA_SERVER +}; + +struct map_id_name_to_krb_primary { + const char *id_name; + const char* krb_primary; +}; + +struct krb5_ctx { + /* opts taken from kinit */ + /* in seconds */ + krb5_deltat starttime; + krb5_deltat lifetime; + char *lifetime_str; + krb5_deltat rlife; + char *rlife_str; + + int forwardable; + int proxiable; + int addresses; + + int not_forwardable; + int not_proxiable; + int no_addresses; + + int verbose; + + char* principal_name; + char* service_name; + char* keytab_name; + char* k5_cache_name; + char* k4_cache_name; + + action_type action; + + struct dp_option *opts; + struct krb5_service *service; + struct krb5_service *kpasswd_service; + + sss_regexp_t *illegal_path_re; + + struct deferred_auth_ctx *deferred_auth_ctx; + struct renew_tgt_ctx *renew_tgt_ctx; + struct kcm_renew_tgt_ctx *kcm_renew_tgt_ctx; + bool use_fast; + bool sss_creds_password; + + hash_table_t *wait_queue_hash; + hash_table_t *io_table; + + enum krb5_config_type config_type; + + struct map_id_name_to_krb_primary *name_to_primary; + + char *realm; + + const char *use_fast_str; + const char *fast_principal; + bool fast_use_anonymous_pkinit; + uint32_t check_pac_flags; + + bool canonicalize; +}; + +struct remove_info_files_ctx { + char *realm; + struct be_ctx *be_ctx; + const char *kdc_service_name; + const char *kpasswd_service_name; + struct krb5_service *krb5_service; +}; + +errno_t sss_krb5_check_options(struct dp_option *opts, + struct sss_domain_info *dom, + struct krb5_ctx *krb5_ctx); + +errno_t krb5_try_kdcip(struct confdb_ctx *cdb, const char *conf_path, + struct dp_option *opts, int opt_id); + +errno_t sss_krb5_get_options(TALLOC_CTX *memctx, struct confdb_ctx *cdb, + const char *conf_path, struct dp_option **_opts); + +void sss_krb5_parse_lookahead(const char *param, size_t *primary, size_t *backup); + +errno_t write_krb5info_file(struct krb5_service *krb5_service, + const char **server_list, + const char *service); + +errno_t write_krb5info_file_from_fo_server(struct krb5_service *krb5_service, + struct fo_server *server, + bool force_default_port, + const char *service, + bool (*filter)(struct fo_server *)); + +struct krb5_service *krb5_service_new(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + const char *service_name, + const char *realm, + bool use_kdcinfo, + size_t n_lookahead_primary, + size_t n_lookahead_backup); + +int krb5_service_init(TALLOC_CTX *memctx, struct be_ctx *ctx, + const char *service_name, + const char *primary_servers, + const char *backup_servers, + const char *realm, + bool use_kdcinfo, + size_t n_lookahead_primary, + size_t n_lookahead_backup, + struct krb5_service **_service); + +void remove_krb5_info_files_callback(void *pvt); + +errno_t remove_krb5_info_files(TALLOC_CTX *mem_ctx, const char *realm); + +errno_t krb5_get_simple_upn(TALLOC_CTX *mem_ctx, struct krb5_ctx *krb5_ctx, + struct sss_domain_info *dom, const char *username, + const char *user_dom, char **_upn); + +errno_t compare_principal_realm(const char *upn, const char *realm, + bool *different_realm); + +/* from krb5_keytab.c */ + +/** + * @brief Copy given keytab into a MEMORY keytab + * + * @param[in] mem_ctx Talloc memory context the new keytab name should be + * allocated on + * @param[in] kctx Kerberos context + * @param[in] inp_keytab_file Existing keytab, if set to NULL the default + * keytab will be used + * @param[out] _mem_name Name of the new MEMORY keytab + * @param[out] _mem_keytab Krb5 keytab handle for the new MEMORY keytab, NULL + * may be passed here if the caller has no use for the + * handle + * + * The memory for the MEMORY keytab is handled by libkrb5 internally and + * a reference counter is used. If the reference counter of the specific + * MEMORY keytab reaches 0, i.e. no open ones are left, the memory is free. + * This means we cannot call krb5_kt_close() for the new MEMORY keytab in + * copy_keytab_into_memory() because this would destroy it immediately. Hence + * we have to return the handle so that the caller can safely remove the + * MEMORY keytab if the is not needed anymore. Since libkrb5 frees the + * internal memory when the library is unloaded short running processes can + * safely pass NULL as the 5th argument because on exit all memory is freed. + * Long running processes which need more control over the memory consumption + * should close the handle for free the memory at runtime. + */ +krb5_error_code copy_keytab_into_memory(TALLOC_CTX *mem_ctx, krb5_context kctx, + const char *inp_keytab_file, + char **_mem_name, + krb5_keytab *_mem_keytab); + +errno_t set_extra_args(TALLOC_CTX *mem_ctx, struct krb5_ctx *krb5_ctx, + struct sss_domain_info *domain, + const char ***krb5_child_extra_args); +#endif /* __KRB5_COMMON_H__ */ diff --git a/src/providers/krb5/krb5_delayed_online_authentication.c b/src/providers/krb5/krb5_delayed_online_authentication.c new file mode 100644 index 0000000..f88d8ab --- /dev/null +++ b/src/providers/krb5/krb5_delayed_online_authentication.c @@ -0,0 +1,386 @@ +/* + SSSD + + Kerberos 5 Backend Module -- Request a TGT when the system gets online + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2010 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <security/pam_modules.h> +#ifdef USE_KEYRING +#include <sys/types.h> +#include <keyutils.h> +#endif +#include <dhash.h> + +#include "providers/krb5/krb5_auth.h" +#include "util/util.h" +#include "util/find_uid.h" + +struct deferred_auth_ctx { + hash_table_t *user_table; + struct be_ctx *be_ctx; + struct tevent_context *ev; + struct krb5_ctx *krb5_ctx; +}; + +struct auth_data { + struct be_ctx *be_ctx; + struct krb5_ctx *krb5_ctx; + struct pam_data *pd; +}; + +static void *hash_talloc(const size_t size, void *pvt) +{ + return talloc_size(pvt, size); +} + +static void hash_talloc_free(void *ptr, void *pvt) +{ + talloc_free(ptr); +} + +static void authenticate_user_done(struct tevent_req *req); +static void authenticate_user(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval current_time, + void *private_data) +{ + struct auth_data *auth_data = talloc_get_type(private_data, + struct auth_data); + struct pam_data *pd = auth_data->pd; + struct tevent_req *req; + + DEBUG_PAM_DATA(SSSDBG_TRACE_ALL, pd); + +#ifdef USE_KEYRING + char *password; + long keysize; + long keyrevoke; + errno_t ret; + + keysize = keyctl_read_alloc(pd->key_serial, (void **)&password); + if (keysize == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "keyctl_read failed [%d][%s].\n", ret, strerror(ret)); + return; + } + + ret = sss_authtok_set_password(pd->authtok, password, keysize); + sss_erase_mem_securely(password, keysize); + free(password); + if (ret) { + DEBUG(SSSDBG_CRIT_FAILURE, + "failed to set password in auth token [%d][%s].\n", + ret, strerror(ret)); + return; + } + + keyrevoke = keyctl_revoke(pd->key_serial); + if (keyrevoke == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "keyctl_revoke failed [%d][%s].\n", ret, strerror(ret)); + } +#endif + + req = krb5_auth_queue_send(auth_data, ev, auth_data->be_ctx, + auth_data->pd, auth_data->krb5_ctx); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_auth_send failed.\n"); + talloc_free(auth_data); + return; + } + + tevent_req_set_callback(req, authenticate_user_done, auth_data); +} + +static void authenticate_user_done(struct tevent_req *req) +{ + struct auth_data *auth_data = tevent_req_callback_data(req, + struct auth_data); + int ret; + int pam_status = PAM_SYSTEM_ERR; + int dp_err = DP_ERR_OK; + + ret = krb5_auth_queue_recv(req, &pam_status, &dp_err); + talloc_free(req); + if (ret) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_auth request failed.\n"); + } else { + if (pam_status == PAM_SUCCESS) { + DEBUG(SSSDBG_CONF_SETTINGS, + "Successfully authenticated user [%s].\n", + auth_data->pd->user); + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to authenticate user [%s].\n", + auth_data->pd->user); + } + } + + talloc_free(auth_data); +} + +static errno_t authenticate_stored_users( + struct deferred_auth_ctx *deferred_auth_ctx) +{ + int ret; + hash_table_t *uid_table; + struct hash_iter_context_t *iter; + hash_entry_t *entry; + hash_key_t key; + hash_value_t value; + struct pam_data *pd; + struct auth_data *auth_data; + struct tevent_timer *te; + + ret = get_uid_table(deferred_auth_ctx, &uid_table); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "get_uid_table failed.\n"); + return ret; + } + + iter = new_hash_iter_context(deferred_auth_ctx->user_table); + if (iter == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "new_hash_iter_context failed.\n"); + return EINVAL; + } + + while ((entry = iter->next(iter)) != NULL) { + key.type = HASH_KEY_ULONG; + key.ul = entry->key.ul; + pd = talloc_get_type(entry->value.ptr, struct pam_data); + + ret = hash_lookup(uid_table, &key, &value); + + if (ret == HASH_SUCCESS) { + DEBUG(SSSDBG_FUNC_DATA, "User [%s] is still logged in, " + "trying online authentication.\n", pd->user); + + auth_data = talloc_zero(deferred_auth_ctx->be_ctx, + struct auth_data); + if (auth_data == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + } else { + auth_data->pd = talloc_steal(auth_data, pd); + auth_data->krb5_ctx = deferred_auth_ctx->krb5_ctx; + auth_data->be_ctx = deferred_auth_ctx->be_ctx; + + te = tevent_add_timer(deferred_auth_ctx->ev, + auth_data, tevent_timeval_current(), + authenticate_user, auth_data); + if (te == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_add_timer failed.\n"); + } + } + } else { + DEBUG(SSSDBG_FUNC_DATA, "User [%s] is not logged in anymore, " + "discarding online authentication.\n", pd->user); + talloc_free(pd); + } + + ret = hash_delete(deferred_auth_ctx->user_table, + &entry->key); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "hash_delete failed [%s].\n", + hash_error_string(ret)); + } + } + + talloc_free(iter); + + return EOK; +} + +static void delayed_online_authentication_callback(void *private_data) +{ + struct deferred_auth_ctx *deferred_auth_ctx = + talloc_get_type(private_data, struct deferred_auth_ctx); + int ret; + + if (deferred_auth_ctx->user_table == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Delayed online authentication activated, " + "but user table does not exists.\n"); + return; + } + + DEBUG(SSSDBG_FUNC_DATA, + "Backend is online, starting delayed online authentication.\n"); + ret = authenticate_stored_users(deferred_auth_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "authenticate_stored_users failed.\n"); + } + + return; +} + +errno_t add_user_to_delayed_online_authentication(struct krb5_ctx *krb5_ctx, + struct sss_domain_info *domain, + struct pam_data *pd, + uid_t uid) +{ + int ret; + hash_key_t key; + hash_value_t value; + struct pam_data *new_pd; + + if (domain->type != DOM_TYPE_POSIX) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Domain type does not support delayed authentication\n"); + return ENOTSUP; + } + + if (krb5_ctx->deferred_auth_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Missing context for delayed online authentication.\n"); + return EINVAL; + } + + if (krb5_ctx->deferred_auth_ctx->user_table == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "user_table not available.\n"); + return EINVAL; + } + + if (sss_authtok_get_type(pd->authtok) != SSS_AUTHTOK_TYPE_PASSWORD) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Invalid authtok for user [%s].\n", pd->user); + return EINVAL; + } + + ret = copy_pam_data(krb5_ctx->deferred_auth_ctx, pd, &new_pd); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "copy_pam_data failed\n"); + return ENOMEM; + } + + +#ifdef USE_KEYRING + const char *password; + size_t len; + + ret = sss_authtok_get_password(new_pd->authtok, &password, &len); + if (ret) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to get password [%d][%s].\n", ret, strerror(ret)); + sss_authtok_set_empty(new_pd->authtok); + talloc_free(new_pd); + return ret; + } + + new_pd->key_serial = add_key("user", new_pd->user, password, len, + KEY_SPEC_SESSION_KEYRING); + if (new_pd->key_serial == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "add_key failed [%d][%s].\n", ret, strerror(ret)); + sss_authtok_set_empty(new_pd->authtok); + talloc_free(new_pd); + return ret; + } + DEBUG(SSSDBG_TRACE_ALL, + "Saved authtok of user [%s] with serial [%"SPRIkey_ser"].\n", + new_pd->user, new_pd->key_serial); + sss_authtok_set_empty(new_pd->authtok); +#endif + + key.type = HASH_KEY_ULONG; + key.ul = uid; + value.type = HASH_VALUE_PTR; + value.ptr = new_pd; + + ret = hash_enter(krb5_ctx->deferred_auth_ctx->user_table, + &key, &value); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot add user [%s] to table [%s], " + "delayed online authentication not possible.\n", + pd->user, hash_error_string(ret)); + talloc_free(new_pd); + return ENOMEM; + } + + DEBUG(SSSDBG_TRACE_ALL, "Added user [%s] successfully to " + "delayed online authentication.\n", pd->user); + + return EOK; +} + +errno_t init_delayed_online_authentication(struct krb5_ctx *krb5_ctx, + struct be_ctx *be_ctx, + struct tevent_context *ev) +{ + int ret; + hash_table_t *tmp_table; + + ret = get_uid_table(krb5_ctx, &tmp_table); + if (ret != EOK) { + if (ret == ENOSYS) { + DEBUG(SSSDBG_FATAL_FAILURE, "Delayed online auth was requested " + "on an unsupported system.\n"); + } else { + DEBUG(SSSDBG_FATAL_FAILURE, "Delayed online auth was requested " + "but initialisation failed.\n"); + } + return ret; + } + ret = hash_destroy(tmp_table); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, + "hash_destroy failed [%s].\n", hash_error_string(ret)); + return EFAULT; + } + + krb5_ctx->deferred_auth_ctx = talloc_zero(krb5_ctx, + struct deferred_auth_ctx); + if (krb5_ctx->deferred_auth_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + return ENOMEM; + } + + ret = hash_create_ex(0, + &krb5_ctx->deferred_auth_ctx->user_table, + 0, 0, 0, 0, hash_talloc, hash_talloc_free, + krb5_ctx->deferred_auth_ctx, + NULL, NULL); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, + "hash_create_ex failed [%s]\n", hash_error_string(ret)); + ret = ENOMEM; + goto fail; + } + + krb5_ctx->deferred_auth_ctx->be_ctx = be_ctx; + krb5_ctx->deferred_auth_ctx->krb5_ctx = krb5_ctx; + krb5_ctx->deferred_auth_ctx->ev = ev; + + ret = be_add_online_cb(krb5_ctx, be_ctx, + delayed_online_authentication_callback, + krb5_ctx->deferred_auth_ctx, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "be_add_online_cb failed.\n"); + goto fail; + } + + /* TODO: add destructor */ + + return EOK; +fail: + talloc_zfree(krb5_ctx->deferred_auth_ctx); + return ret; +} diff --git a/src/providers/krb5/krb5_init.c b/src/providers/krb5/krb5_init.c new file mode 100644 index 0000000..d2c927e --- /dev/null +++ b/src/providers/krb5/krb5_init.c @@ -0,0 +1,229 @@ +/* + SSSD + + Kerberos 5 Backend Module + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2009 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <sys/types.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/stat.h> +#include "util/child_common.h" +#include "providers/krb5/krb5_auth.h" +#include "providers/krb5/krb5_common.h" +#include "providers/krb5/krb5_init_shared.h" +#include "providers/data_provider.h" + +static errno_t krb5_init_kpasswd(struct krb5_ctx *ctx, + struct be_ctx *be_ctx) +{ + const char *realm; + const char *primary_servers; + const char *backup_servers; + const char *kdc_servers; + bool use_kdcinfo; + size_t n_lookahead_primary; + size_t n_lookahead_backup; + errno_t ret; + + realm = dp_opt_get_string(ctx->opts, KRB5_REALM); + if (realm == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Missing krb5_realm option!\n"); + return EINVAL; + } + + kdc_servers = dp_opt_get_string(ctx->opts, KRB5_KDC); + primary_servers = dp_opt_get_string(ctx->opts, KRB5_KPASSWD); + backup_servers = dp_opt_get_string(ctx->opts, KRB5_BACKUP_KPASSWD); + use_kdcinfo = dp_opt_get_bool(ctx->opts, KRB5_USE_KDCINFO); + sss_krb5_parse_lookahead(dp_opt_get_string(ctx->opts, KRB5_KDCINFO_LOOKAHEAD), + &n_lookahead_primary, &n_lookahead_backup); + + + if (primary_servers == NULL && backup_servers != NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, "kpasswd server wasn't specified but " + "backup_servers kpasswd given. Using it as primary_servers\n"); + primary_servers = backup_servers; + backup_servers = NULL; + } + + if (primary_servers == NULL && kdc_servers != NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Missing krb5_kpasswd option and KDC set " + "explicitly, will use KDC for password change operations!\n"); + ctx->kpasswd_service = NULL; + } else { + ret = krb5_service_init(ctx, be_ctx, SSS_KRB5KPASSWD_FO_SRV, + primary_servers, backup_servers, realm, + use_kdcinfo, + n_lookahead_primary, + n_lookahead_backup, + &ctx->kpasswd_service); + if (ret != EOK) { + DEBUG(SSSDBG_FATAL_FAILURE, + "Failed to init KRB5KPASSWD failover service!\n"); + return ret; + } + } + + return EOK; +} + +static errno_t krb5_init_kdc(struct krb5_ctx *ctx, struct be_ctx *be_ctx) +{ + const char *primary_servers; + const char *backup_servers; + const char *realm; + bool use_kdcinfo; + size_t n_lookahead_primary; + size_t n_lookahead_backup; + errno_t ret; + + realm = dp_opt_get_string(ctx->opts, KRB5_REALM); + if (realm == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Missing krb5_realm option!\n"); + return EINVAL; + } + + primary_servers = dp_opt_get_string(ctx->opts, KRB5_KDC); + backup_servers = dp_opt_get_string(ctx->opts, KRB5_BACKUP_KDC); + + use_kdcinfo = dp_opt_get_bool(ctx->opts, KRB5_USE_KDCINFO); + sss_krb5_parse_lookahead(dp_opt_get_string(ctx->opts, KRB5_KDCINFO_LOOKAHEAD), + &n_lookahead_primary, &n_lookahead_backup); + + ret = krb5_service_init(ctx, be_ctx, SSS_KRB5KDC_FO_SRV, + primary_servers, backup_servers, realm, + use_kdcinfo, + n_lookahead_primary, + n_lookahead_backup, + &ctx->service); + if (ret != EOK) { + DEBUG(SSSDBG_FATAL_FAILURE, "Failed to init KRB5 failover service!\n"); + return ret; + } + + return EOK; +} + +errno_t sssm_krb5_init(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + struct data_provider *provider, + const char *module_name, + void **_module_data) +{ + struct krb5_ctx *ctx; + errno_t ret; + + ctx = talloc_zero(mem_ctx, struct krb5_ctx); + if (ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero() failed\n"); + return ENOMEM; + } + + ret = sss_krb5_get_options(ctx, be_ctx->cdb, be_ctx->conf_path, &ctx->opts); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to get krb5 options [%d]: %s\n", + ret, sss_strerror(ret)); + goto done; + } + + ctx->action = INIT_PW; + ctx->config_type = K5C_GENERIC; + + ret = krb5_init_kdc(ctx, be_ctx); + if (ret != EOK) { + goto done; + } + + ret = krb5_init_kpasswd(ctx, be_ctx); + if (ret != EOK) { + goto done; + } + + ret = krb5_child_init(ctx, be_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_FATAL_FAILURE, "Could not initialize krb5_child settings " + "[%d]: %s\n", ret, sss_strerror(ret)); + goto done; + } + + ret = sss_regexp_new(ctx, ILLEGAL_PATH_PATTERN, 0, &(ctx->illegal_path_re)); + if (ret != EOK) { + ret = EFAULT; + goto done; + } + + ret = be_fo_set_dns_srv_lookup_plugin(be_ctx, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to set SRV lookup plugin " + "[%d]: %s\n", ret, sss_strerror(ret)); + goto done; + } + + *_module_data = ctx; + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(ctx); + } + + return ret; +} + +errno_t sssm_krb5_auth_init(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + void *module_data, + struct dp_method *dp_methods) +{ + struct krb5_ctx *ctx; + + ctx = talloc_get_type(module_data, struct krb5_ctx); + dp_set_method(dp_methods, DPM_AUTH_HANDLER, + krb5_pam_handler_send, krb5_pam_handler_recv, ctx, + struct krb5_ctx, struct pam_data, struct pam_data *); + + return EOK; +} + +errno_t sssm_krb5_chpass_init(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + void *module_data, + struct dp_method *dp_methods) +{ + return sssm_krb5_auth_init(mem_ctx, be_ctx, module_data, dp_methods); +} + +errno_t sssm_krb5_access_init(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + void *module_data, + struct dp_method *dp_methods) +{ + struct krb5_ctx *ctx; + + ctx = talloc_get_type(module_data, struct krb5_ctx); + dp_set_method(dp_methods, DPM_ACCESS_HANDLER, + krb5_pam_handler_send, krb5_pam_handler_recv, ctx, + struct krb5_ctx, struct pam_data, struct pam_data *); + + return EOK; +} diff --git a/src/providers/krb5/krb5_init_shared.c b/src/providers/krb5/krb5_init_shared.c new file mode 100644 index 0000000..3e6ebe2 --- /dev/null +++ b/src/providers/krb5/krb5_init_shared.c @@ -0,0 +1,105 @@ +/* + SSSD + + Authors: + Stephen Gallagher <sgallagh@redhat.com> + + Copyright (C) 2012 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <fcntl.h> + +#include "providers/krb5/krb5_common.h" +#include "providers/krb5/krb5_auth.h" +#include "providers/krb5/krb5_utils.h" +#include "providers/krb5/krb5_init_shared.h" + +errno_t krb5_child_init(struct krb5_ctx *krb5_auth_ctx, + struct be_ctx *bectx) +{ + errno_t ret; + time_t renew_intv = 0; + krb5_deltat renew_interval_delta; + char *renew_interval_str; + + if (dp_opt_get_bool(krb5_auth_ctx->opts, KRB5_STORE_PASSWORD_IF_OFFLINE)) { + ret = init_delayed_online_authentication(krb5_auth_ctx, bectx, + bectx->ev); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "init_delayed_online_authentication failed.\n"); + goto done; + } + } + renew_interval_str = dp_opt_get_string(krb5_auth_ctx->opts, + KRB5_RENEW_INTERVAL); + if (renew_interval_str != NULL) { + ret = krb5_string_to_deltat(renew_interval_str, &renew_interval_delta); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Reading krb5_renew_interval failed.\n"); + renew_interval_delta = 0; + } + renew_intv = renew_interval_delta; + } + + if (renew_intv > 0) { + ret = init_renew_tgt(krb5_auth_ctx, bectx, bectx->ev, renew_intv); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "init_renew_tgt failed.\n"); + goto done; + } + } + + ret = sss_krb5_check_options(krb5_auth_ctx->opts, bectx->domain, + krb5_auth_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_krb5_check_options failed.\n"); + goto done; + } + + ret = get_pac_check_config(bectx->cdb, &krb5_auth_ctx->check_pac_flags); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to get pac_check option.\n"); + goto done; + } + + if (krb5_auth_ctx->check_pac_flags != 0 + && !dp_opt_get_bool(krb5_auth_ctx->opts, KRB5_VALIDATE)) { + DEBUG(SSSDBG_IMPORTANT_INFO, + "PAC check is requested but krb5_validate is set to false. " + "PAC checks will be skipped.\n"); + sss_log(SSS_LOG_WARNING, + "PAC check is requested but krb5_validate is set to false. " + "PAC checks will be skipped."); + } + + ret = parse_krb5_map_user(krb5_auth_ctx, + dp_opt_get_cstring(krb5_auth_ctx->opts, + KRB5_MAP_USER), + bectx->domain->name, + &krb5_auth_ctx->name_to_primary); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "parse_krb5_map_user failed: %s:[%d]\n", + sss_strerror(ret), ret); + goto done; + } + + ret = EOK; + +done: + return ret; +} diff --git a/src/providers/krb5/krb5_init_shared.h b/src/providers/krb5/krb5_init_shared.h new file mode 100644 index 0000000..883b84f --- /dev/null +++ b/src/providers/krb5/krb5_init_shared.h @@ -0,0 +1,29 @@ +/* + SSSD + + Authors: + Stephen Gallagher <sgallagh@redhat.com> + + Copyright (C) 2012 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#ifndef KRB5_INIT_SHARED_H_ +#define KRB5_INIT_SHARED_H_ + +errno_t krb5_child_init(struct krb5_ctx *krb5_auth_ctx, + struct be_ctx *bectx); + +#endif /* KRB5_INIT_SHARED_H_ */ diff --git a/src/providers/krb5/krb5_keytab.c b/src/providers/krb5/krb5_keytab.c new file mode 100644 index 0000000..db383d4 --- /dev/null +++ b/src/providers/krb5/krb5_keytab.c @@ -0,0 +1,231 @@ +/* + SSSD + + Kerberos 5 Backend Module -- keytab related utilities + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2014 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include "util/util.h" +#include "util/sss_krb5.h" +#include "providers/krb5/krb5_common.h" + +static krb5_error_code do_keytab_copy(krb5_context kctx, krb5_keytab s_keytab, + krb5_keytab d_keytab) +{ + krb5_error_code kerr; + krb5_error_code kt_err; + krb5_kt_cursor cursor; + krb5_keytab_entry entry; + + memset(&cursor, 0, sizeof(cursor)); + kerr = krb5_kt_start_seq_get(kctx, s_keytab, &cursor); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "error reading keytab.\n"); + return kerr; + } + + memset(&entry, 0, sizeof(entry)); + while ((kt_err = krb5_kt_next_entry(kctx, s_keytab, &entry, + &cursor)) == 0) { + kerr = krb5_kt_add_entry(kctx, d_keytab, &entry); + if (kerr != 0) { + DEBUG(SSSDBG_OP_FAILURE, "krb5_kt_add_entry failed.\n"); + kt_err = krb5_kt_end_seq_get(kctx, s_keytab, &cursor); + if (kt_err != 0) { + DEBUG(SSSDBG_TRACE_ALL, + "krb5_kt_end_seq_get failed with [%d], ignored.\n", + kt_err); + } + return kerr; + } + + kerr = sss_krb5_free_keytab_entry_contents(kctx, &entry); + if (kerr != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, "Failed to free keytab entry.\n"); + kt_err = krb5_kt_end_seq_get(kctx, s_keytab, &cursor); + if (kt_err != 0) { + DEBUG(SSSDBG_TRACE_ALL, + "krb5_kt_end_seq_get failed with [%d], ignored.\n", + kt_err); + } + return kerr; + } + memset(&entry, 0, sizeof(entry)); + } + + kerr = krb5_kt_end_seq_get(kctx, s_keytab, &cursor); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_kt_end_seq_get failed.\n"); + return kerr; + } + + /* check if we got any errors from krb5_kt_next_entry */ + if (kt_err != 0 && kt_err != KRB5_KT_END) { + DEBUG(SSSDBG_CRIT_FAILURE, "error reading keytab.\n"); + return kt_err; + } + + return 0; +} + +krb5_error_code copy_keytab_into_memory(TALLOC_CTX *mem_ctx, krb5_context kctx, + const char *inp_keytab_file, + char **_mem_name, + krb5_keytab *_mem_keytab) +{ + krb5_error_code kerr; + krb5_keytab keytab = NULL; + krb5_keytab mem_keytab = NULL; + krb5_keytab tmp_mem_keytab = NULL; + char keytab_name[MAX_KEYTAB_NAME_LEN]; + char *sep; + char *mem_name = NULL; + char *tmp_mem_name = NULL; + const char *keytab_file; + char default_keytab_name[MAX_KEYTAB_NAME_LEN]; + + keytab_file = inp_keytab_file; + if (keytab_file == NULL) { + kerr = krb5_kt_default_name(kctx, default_keytab_name, + sizeof(default_keytab_name)); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_kt_default_name failed.\n"); + return kerr; + } + + keytab_file = default_keytab_name; + } + + kerr = krb5_kt_resolve(kctx, keytab_file, &keytab); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "error resolving keytab [%s].\n", + keytab_file); + return kerr; + } + + kerr = sss_krb5_kt_have_content(kctx, keytab); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "keytab [%s] has not entries.\n", + keytab_file); + goto done; + } + + kerr = krb5_kt_get_name(kctx, keytab, keytab_name, sizeof(keytab_name)); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to read name for keytab [%s].\n", + keytab_file); + goto done; + } + + sep = strchr(keytab_name, ':'); + if (sep == NULL || sep[1] == '\0') { + DEBUG(SSSDBG_CRIT_FAILURE, + "Keytab name [%s] does not have delimiter[:] .\n", keytab_name); + kerr = KRB5KRB_ERR_GENERIC; + goto done; + } + + if (strncmp(keytab_name, "MEMORY:", sizeof("MEMORY:") -1) == 0) { + DEBUG(SSSDBG_TRACE_FUNC, "Keytab [%s] is already memory keytab.\n", + keytab_name); + *_mem_name = talloc_strdup(mem_ctx, keytab_name); + if(*_mem_name == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + kerr = KRB5KRB_ERR_GENERIC; + goto done; + } + kerr = 0; + goto done; + } + + mem_name = talloc_asprintf(mem_ctx, "MEMORY:%s", sep + 1); + if (mem_name == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + kerr = KRB5KRB_ERR_GENERIC; + goto done; + } + + tmp_mem_name = talloc_asprintf(mem_ctx, "MEMORY:%s.tmp", sep + 1); + if (tmp_mem_name == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + kerr = KRB5KRB_ERR_GENERIC; + goto done; + } + + kerr = krb5_kt_resolve(kctx, mem_name, &mem_keytab); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "error resolving keytab [%s].\n", + mem_name); + goto done; + } + + kerr = krb5_kt_resolve(kctx, tmp_mem_name, &tmp_mem_keytab); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "error resolving keytab [%s].\n", + tmp_mem_name); + goto done; + } + + kerr = do_keytab_copy(kctx, keytab, tmp_mem_keytab); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to copy keytab [%s] into [%s].\n", + keytab_file, tmp_mem_name); + goto done; + } + + /* krb5_kt_add_entry() adds new entries into MEMORY keytabs at the + * beginning and not at the end as for FILE keytabs. Since we want to keep + * the processing order we have to copy the MEMORY keytab again to retain + * the order from the FILE keytab. */ + + kerr = do_keytab_copy(kctx, tmp_mem_keytab, mem_keytab); + if (kerr != 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to copy keytab [%s] into [%s].\n", + tmp_mem_name, mem_name); + goto done; + } + + *_mem_name = mem_name; + if (_mem_keytab != NULL) { + *_mem_keytab = mem_keytab; + } + + kerr = 0; +done: + + talloc_free(tmp_mem_name); + + if (kerr != 0) { + talloc_free(mem_name); + if ((mem_keytab != NULL) && krb5_kt_close(kctx, mem_keytab) != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, "krb5_kt_close failed.\n"); + } + } + + if (tmp_mem_keytab != NULL && krb5_kt_close(kctx, tmp_mem_keytab) != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, "krb5_kt_close failed.\n"); + } + + if (keytab != NULL && krb5_kt_close(kctx, keytab) != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, "krb5_kt_close failed.\n"); + } + + return kerr; +} diff --git a/src/providers/krb5/krb5_opts.c b/src/providers/krb5/krb5_opts.c new file mode 100644 index 0000000..20dcd58 --- /dev/null +++ b/src/providers/krb5/krb5_opts.c @@ -0,0 +1,50 @@ +/* + SSSD + + Authors: + Stephen Gallagher <sgallagh@redhat.com> + + Copyright (C) 2012 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include "src/providers/data_provider.h" + +struct dp_option default_krb5_opts[] = { + { "krb5_server", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_backup_server", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_realm", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_ccachedir", DP_OPT_STRING, { DEFAULT_CCACHE_DIR }, NULL_STRING }, + { "krb5_ccname_template", DP_OPT_STRING, NULL_STRING, NULL_STRING}, + { "krb5_auth_timeout", DP_OPT_NUMBER, { .number = 6 }, NULL_NUMBER }, + { "krb5_keytab", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_validate", DP_OPT_BOOL, BOOL_FALSE, BOOL_FALSE }, + { "krb5_kpasswd", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_backup_kpasswd", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_store_password_if_offline", DP_OPT_BOOL, BOOL_FALSE, BOOL_FALSE }, + { "krb5_renewable_lifetime", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_lifetime", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_renew_interval", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_use_fast", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_fast_principal", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_fast_use_anonymous_pkinit", DP_OPT_BOOL, BOOL_FALSE, BOOL_FALSE }, + { "krb5_canonicalize", DP_OPT_BOOL, BOOL_FALSE, BOOL_FALSE }, + { "krb5_use_enterprise_principal", DP_OPT_BOOL, BOOL_FALSE, BOOL_FALSE }, + { "krb5_use_kdcinfo", DP_OPT_BOOL, BOOL_TRUE, BOOL_TRUE }, + { "krb5_kdcinfo_lookahead", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_map_user", DP_OPT_STRING, NULL_STRING, NULL_STRING }, + { "krb5_use_subdomain_realm", DP_OPT_BOOL, BOOL_FALSE, BOOL_FALSE }, + DP_OPTION_TERMINATOR +}; diff --git a/src/providers/krb5/krb5_opts.h b/src/providers/krb5/krb5_opts.h new file mode 100644 index 0000000..798008d --- /dev/null +++ b/src/providers/krb5/krb5_opts.h @@ -0,0 +1,30 @@ +/* + SSSD + + Authors: + Stephen Gallagher <sgallagh@redhat.com> + + Copyright (C) 2012 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#ifndef KRB5_OPTS_H_ +#define KRB5_OPTS_H_ + +#include "src/providers/data_provider.h" + +extern struct dp_option default_krb5_opts[]; + +#endif /* KRB5_OPTS_H_ */ diff --git a/src/providers/krb5/krb5_renew_tgt.c b/src/providers/krb5/krb5_renew_tgt.c new file mode 100644 index 0000000..9435555 --- /dev/null +++ b/src/providers/krb5/krb5_renew_tgt.c @@ -0,0 +1,631 @@ +/* + SSSD + + Kerberos 5 Backend Module -- Renew a TGT automatically + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2010 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ +#include <security/pam_modules.h> + +#include "util/util.h" +#include "providers/krb5/krb5_common.h" +#include "providers/krb5/krb5_auth.h" +#include "providers/krb5/krb5_utils.h" +#include "providers/krb5/krb5_ccache.h" + +struct renew_tgt_ctx { + hash_table_t *tgt_table; + struct be_ctx *be_ctx; + struct tevent_context *ev; + struct krb5_ctx *krb5_ctx; + time_t timer_interval; + struct tevent_timer *te; +}; + +struct renew_data { + const char *ccfile; + time_t start_time; + time_t lifetime; + time_t start_renew_at; + struct pam_data *pd; +}; + +struct auth_data { + struct be_ctx *be_ctx; + struct krb5_ctx *krb5_ctx; + struct pam_data *pd; + struct renew_data *renew_data; + hash_table_t *table; + hash_key_t key; +}; + + +static void renew_tgt_done(struct tevent_req *req); +static void renew_tgt(struct tevent_context *ev, struct tevent_timer *te, + struct timeval current_time, void *private_data) +{ + struct auth_data *auth_data = talloc_get_type(private_data, + struct auth_data); + struct tevent_req *req; + + req = krb5_auth_queue_send(auth_data, ev, auth_data->be_ctx, auth_data->pd, + auth_data->krb5_ctx); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_auth_send failed.\n"); +/* Give back the pam data to the renewal item to be able to retry at the next + * time the renewals re run. */ + auth_data->renew_data->pd = talloc_steal(auth_data->renew_data, + auth_data->pd); + talloc_free(auth_data); + return; + } + + tevent_req_set_callback(req, renew_tgt_done, auth_data); +} + +static void renew_tgt_done(struct tevent_req *req) +{ + struct auth_data *auth_data = tevent_req_callback_data(req, + struct auth_data); + int ret; + int pam_status = PAM_SYSTEM_ERR; + int dp_err; + hash_value_t value; + + ret = krb5_auth_queue_recv(req, &pam_status, &dp_err); + talloc_free(req); + if (ret) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_auth request failed.\n"); + if (auth_data->renew_data != NULL) { + DEBUG(SSSDBG_FUNC_DATA, "Giving back pam data.\n"); + auth_data->renew_data->pd = talloc_steal(auth_data->renew_data, + auth_data->pd); + } + } else { + switch (pam_status) { + case PAM_SUCCESS: + DEBUG(SSSDBG_CONF_SETTINGS, + "Successfully renewed TGT for user [%s].\n", + auth_data->pd->user); +/* In general a successful renewal will update the renewal item and free the + * old data. But if the TGT has reached the end of his renewable lifetime it + * will not be put into the list of renewable tickets again. In this case the + * renewal item is not updated and the value from the hash and the one we have + * stored are the same. Since the TGT cannot be renewed anymore we want to + * remove it from the list of renewable tickets. */ + ret = hash_lookup(auth_data->table, &auth_data->key, &value); + if (ret == HASH_SUCCESS) { + if (value.type == HASH_VALUE_PTR && + auth_data->renew_data == talloc_get_type(value.ptr, + struct renew_data)) { + DEBUG(SSSDBG_FUNC_DATA, + "New TGT was not added for renewal, " + "removing list entry for user [%s].\n", + auth_data->pd->user); + ret = hash_delete(auth_data->table, &auth_data->key); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "hash_delete failed.\n"); + } + } + } + break; + case PAM_AUTHINFO_UNAVAIL: + case PAM_AUTHTOK_LOCK_BUSY: + DEBUG(SSSDBG_CONF_SETTINGS, + "Cannot renewed TGT for user [%s] while offline, " + "will retry later.\n", + auth_data->pd->user); + if (auth_data->renew_data != NULL) { + DEBUG(SSSDBG_FUNC_DATA, "Giving back pam data.\n"); + auth_data->renew_data->pd = talloc_steal(auth_data->renew_data, + auth_data->pd); + } + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to renew TGT for user [%s].\n", + auth_data->pd->user); + ret = hash_delete(auth_data->table, &auth_data->key); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "hash_delete failed.\n"); + } + } + } + + talloc_zfree(auth_data); +} + +static errno_t renew_all_tgts(struct renew_tgt_ctx *renew_tgt_ctx) +{ + int ret; + hash_entry_t *entries; + unsigned long count; + size_t c; + time_t now; + struct auth_data *auth_data; + struct renew_data *renew_data; + struct tevent_timer *te = NULL; + + ret = hash_entries(renew_tgt_ctx->tgt_table, &count, &entries); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "hash_entries failed.\n"); + return ENOMEM; + } + + now = time(NULL); + + for (c = 0; c < count; c++) { + renew_data = talloc_get_type(entries[c].value.ptr, struct renew_data); + DEBUG(SSSDBG_TRACE_ALL, + "Checking [%s] for renewal at [%.24s].\n", renew_data->ccfile, + ctime(&renew_data->start_renew_at)); + /* If renew_data->pd == NULL a renewal request for this data is + * currently running so we skip it. */ + if (renew_data->start_renew_at < now && renew_data->pd != NULL) { + auth_data = talloc_zero(renew_tgt_ctx, struct auth_data); + if (auth_data == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + } else { +/* We need to steal the pam_data here, because a successful renewal of the + * ticket might add a new renewal item to the list with the same key (upn). + * This would delete renew_data and all its children. But we cannot be sure + * that adding the new renewal item is the last operation of the renewal + * process with access the pam_data. To be on the safe side we steal the + * pam_data and make it a child of auth_data which is only freed after the + * renewal process is finished. In the case of an error during renewal we + * might want to steal the pam_data back to renew_data before freeing + * auth_data to allow a new renewal attempt. */ + auth_data->pd = talloc_move(auth_data, &renew_data->pd); + auth_data->krb5_ctx = renew_tgt_ctx->krb5_ctx; + auth_data->be_ctx = renew_tgt_ctx->be_ctx; + auth_data->table = renew_tgt_ctx->tgt_table; + auth_data->renew_data = renew_data; + auth_data->key.type = entries[c].key.type; + auth_data->key.str = talloc_strdup(auth_data, + entries[c].key.str); + if (auth_data->key.str == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n"); + } else { + te = tevent_add_timer(renew_tgt_ctx->ev, + auth_data, tevent_timeval_current(), + renew_tgt, auth_data); + if (te == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "tevent_add_timer failed.\n"); + } + } + } + + if (auth_data == NULL || te == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to renew TGT in [%s].\n", renew_data->ccfile); + ret = hash_delete(renew_tgt_ctx->tgt_table, &entries[c].key); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "hash_delete failed.\n"); + } + } + } + } + + talloc_free(entries); + + return EOK; +} + +static void renew_handler(struct renew_tgt_ctx *renew_tgt_ctx); + +static void renew_tgt_offline_callback(void *private_data) +{ + struct renew_tgt_ctx *renew_tgt_ctx = talloc_get_type(private_data, + struct renew_tgt_ctx); + + talloc_zfree(renew_tgt_ctx->te); +} + +static void renew_tgt_online_callback(void *private_data) +{ + struct renew_tgt_ctx *renew_tgt_ctx = talloc_get_type(private_data, + struct renew_tgt_ctx); + + renew_handler(renew_tgt_ctx); +} + +static void renew_tgt_timer_handler(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval current_time, void *data) +{ + struct renew_tgt_ctx *renew_tgt_ctx = talloc_get_type(data, + struct renew_tgt_ctx); + + /* forget the timer event, it will be freed by the tevent timer loop */ + renew_tgt_ctx->te = NULL; + + renew_handler(renew_tgt_ctx); +} + +static void renew_handler(struct renew_tgt_ctx *renew_tgt_ctx) +{ + struct timeval next; + int ret; + + if (be_is_offline(renew_tgt_ctx->be_ctx)) { + DEBUG(SSSDBG_CONF_SETTINGS, "Offline, disable renew timer.\n"); + return; + } + + ret = renew_all_tgts(renew_tgt_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "renew_all_tgts failed. " + "Disabling automatic TGT renewal\n"); + sss_log(SSS_LOG_ERR, "Disabling automatic TGT renewal."); + talloc_zfree(renew_tgt_ctx); + return; + } + + if (renew_tgt_ctx->te != NULL) { + DEBUG(SSSDBG_TRACE_LIBS, + "There is an active renewal timer, doing nothing.\n"); + return; + } + + DEBUG(SSSDBG_TRACE_LIBS, "Adding new renew timer.\n"); + + next = sss_tevent_timeval_current_ofs_time_t(renew_tgt_ctx->timer_interval); + renew_tgt_ctx->te = tevent_add_timer(renew_tgt_ctx->ev, renew_tgt_ctx, + next, renew_tgt_timer_handler, + renew_tgt_ctx); + if (renew_tgt_ctx->te == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_add_timer failed.\n"); + sss_log(SSS_LOG_ERR, "Disabling automatic TGT renewal."); + talloc_zfree(renew_tgt_ctx); + } + + return; +} + +static void renew_del_cb(hash_entry_t *entry, hash_destroy_enum type, void *pvt) +{ + struct renew_data *renew_data; + + if (entry->value.type == HASH_VALUE_PTR) { + renew_data = talloc_get_type(entry->value.ptr, struct renew_data); + talloc_zfree(renew_data); + return; + } + + DEBUG(SSSDBG_CRIT_FAILURE, + "Unexpected value type [%d].\n", entry->value.type); +} + +static errno_t check_ccache_file(struct renew_tgt_ctx *renew_tgt_ctx, + const char *ccache_file, const char *upn, + const char *user_name) +{ + int ret; + struct stat stat_buf; + struct tgt_times tgtt; + struct pam_data pd; + time_t now; + const char *filename; + + if (ccache_file == NULL || upn == NULL || user_name == NULL) { + DEBUG(SSSDBG_TRACE_FUNC, + "Missing one of the needed attributes: [%s][%s][%s].\n", + ccache_file == NULL ? "cache file missing" : ccache_file, + upn == NULL ? "principal missing" : upn, + user_name == NULL ? "user name missing" : user_name); + return EINVAL; + } + + if (strncmp(ccache_file, "FILE:", 5) == 0) { + filename = ccache_file + 5; + } else { + filename = ccache_file; + } + + ret = stat(filename, &stat_buf); + if (ret != EOK) { + if (ret == ENOENT) { + return EOK; + } + return ret; + } + + DEBUG(SSSDBG_TRACE_ALL, "Found ccache file [%s].\n", ccache_file); + + memset(&tgtt, 0, sizeof(tgtt)); + ret = get_ccache_file_data(ccache_file, upn, &tgtt); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "get_ccache_file_data failed.\n"); + return ret; + } + + memset(&pd, 0, sizeof(pd)); + pd.cmd = SSS_CMD_RENEW; + pd.user = discard_const_p(char, user_name); + now = time(NULL); + if (tgtt.renew_till > tgtt.endtime && tgtt.renew_till > now && + tgtt.endtime > now) { + DEBUG(SSSDBG_TRACE_LIBS, + "Adding [%s] for automatic renewal.\n", ccache_file); + ret = add_tgt_to_renew_table(renew_tgt_ctx->krb5_ctx, ccache_file, + &tgtt, &pd, upn); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "add_tgt_to_renew_table failed, " + "automatic renewal not possible.\n"); + } + } else { + DEBUG(SSSDBG_TRACE_ALL, + "TGT in [%s] for [%s] is too old.\n", ccache_file, upn); + } + + return EOK; +} + +static errno_t check_ccache_files(struct renew_tgt_ctx *renew_tgt_ctx) +{ + TALLOC_CTX *tmp_ctx; + int ret; + const char *ccache_filter = SYSDB_CCACHE_FILE"=*"; + const char *ccache_attrs[] = { SYSDB_CCACHE_FILE, SYSDB_UPN, SYSDB_NAME, + SYSDB_CANONICAL_UPN, NULL }; + size_t msgs_count = 0; + struct ldb_message **msgs = NULL; + size_t c; + const char *ccache_file; + char *upn; + const char *user_name; + struct ldb_dn *base_dn; + char *user_dom; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed.\n"); + return ENOMEM; + } + + base_dn = sysdb_user_base_dn(tmp_ctx, renew_tgt_ctx->be_ctx->domain); + if (base_dn == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "sysdb_base_dn failed.\n"); + ret = ENOMEM; + goto done; + } + + ret = sysdb_search_entry(tmp_ctx, renew_tgt_ctx->be_ctx->domain->sysdb, base_dn, + LDB_SCOPE_SUBTREE, ccache_filter, ccache_attrs, + &msgs_count, &msgs); + if (ret == ENOENT) { + msgs_count = 0; /* Fall through */ + } else if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "sysdb_search_entry failed.\n"); + goto done; + } + + if (msgs_count == 0) { + DEBUG(SSSDBG_TRACE_ALL, + "No entries with ccache file found in cache.\n"); + ret = EOK; + goto done; + } + DEBUG(SSSDBG_TRACE_ALL, + "Found [%zu] entries with ccache file in cache.\n", msgs_count); + + for (c = 0; c < msgs_count; c++) { + user_name = ldb_msg_find_attr_as_string(msgs[c], SYSDB_NAME, NULL); + if (user_name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "No user name found, this is a severe error, " + "but we ignore it here.\n"); + continue; + } + + ret = sss_parse_internal_fqname(tmp_ctx, user_name, NULL, &user_dom); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot parse internal fqname [%d]: %s\n", + ret, sss_strerror(ret)); + goto done; + } + + ret = find_or_guess_upn(tmp_ctx, msgs[c], renew_tgt_ctx->krb5_ctx, + renew_tgt_ctx->be_ctx->domain, + user_name, user_dom, &upn); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "find_or_guess_upn failed.\n"); + goto done; + } + + ccache_file = ldb_msg_find_attr_as_string(msgs[c], SYSDB_CCACHE_FILE, + NULL); + + ret = check_ccache_file(renew_tgt_ctx, ccache_file, upn, user_name); + if (ret != EOK) { + DEBUG(SSSDBG_FUNC_DATA, + "Failed to check ccache file [%s].\n", ccache_file); + } + } + + ret = EOK; + +done: + talloc_free(tmp_ctx); + + return ret; +} + +errno_t init_renew_tgt(struct krb5_ctx *krb5_ctx, struct be_ctx *be_ctx, + struct tevent_context *ev, time_t renew_intv) +{ + int ret; + struct timeval next; + + krb5_ctx->renew_tgt_ctx = talloc_zero(krb5_ctx, struct renew_tgt_ctx); + if (krb5_ctx->renew_tgt_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + return ENOMEM; + } + + ret = sss_hash_create_ex(krb5_ctx->renew_tgt_ctx, 0, + &krb5_ctx->renew_tgt_ctx->tgt_table, 0, 0, 0, 0, + renew_del_cb, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_hash_create failed.\n"); + goto fail; + } + + krb5_ctx->renew_tgt_ctx->be_ctx = be_ctx; + krb5_ctx->renew_tgt_ctx->krb5_ctx = krb5_ctx; + krb5_ctx->renew_tgt_ctx->ev = ev; + krb5_ctx->renew_tgt_ctx->timer_interval = renew_intv; + + ret = check_ccache_files(krb5_ctx->renew_tgt_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to read ccache files, continuing ...\n"); + } + + next = sss_tevent_timeval_current_ofs_time_t(krb5_ctx->renew_tgt_ctx->timer_interval); + krb5_ctx->renew_tgt_ctx->te = tevent_add_timer(ev, krb5_ctx->renew_tgt_ctx, + next, renew_tgt_timer_handler, + krb5_ctx->renew_tgt_ctx); + if (krb5_ctx->renew_tgt_ctx->te == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_add_timer failed.\n"); + ret = ENOMEM; + goto fail; + } + + DEBUG(SSSDBG_TRACE_LIBS, + "Adding offline callback to remove renewal timer.\n"); + ret = be_add_offline_cb(krb5_ctx->renew_tgt_ctx, be_ctx, + renew_tgt_offline_callback, krb5_ctx->renew_tgt_ctx, + NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to add offline callback.\n"); + goto fail; + } + + DEBUG(SSSDBG_TRACE_LIBS, "Adding renewal task to online callbacks.\n"); + ret = be_add_online_cb(krb5_ctx->renew_tgt_ctx, be_ctx, + renew_tgt_online_callback, krb5_ctx->renew_tgt_ctx, + NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to add renewal task to online callbacks.\n"); + goto fail; + } + + return EOK; + +fail: + talloc_zfree(krb5_ctx->renew_tgt_ctx); + return ret; +} + +errno_t add_tgt_to_renew_table(struct krb5_ctx *krb5_ctx, const char *ccfile, + struct tgt_times *tgtt, struct pam_data *pd, + const char *upn) +{ + int ret; + hash_key_t key; + hash_value_t value; + struct renew_data *renew_data = NULL; + + if (krb5_ctx->renew_tgt_ctx == NULL) { + DEBUG(SSSDBG_TRACE_LIBS ,"Renew context not initialized, " + "automatic renewal not available.\n"); + return EOK; + } + + if (pd->cmd != SSS_PAM_AUTHENTICATE && pd->cmd != SSS_CMD_RENEW && + pd->cmd != SSS_PAM_CHAUTHTOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unexpected pam task [%d].\n", pd->cmd); + return EINVAL; + } + + if (upn == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing user principal name.\n"); + return EINVAL; + } + + /* hash_enter copies the content of the hash string, so it is safe to use + * discard_const_p here. */ + key.type = HASH_KEY_STRING; + key.str = discard_const_p(char, upn); + + renew_data = talloc_zero(krb5_ctx->renew_tgt_ctx, struct renew_data); + if (renew_data == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + ret = ENOMEM; + goto done; + } + + if (ccfile[0] == '/') { + renew_data->ccfile = talloc_asprintf(renew_data, "FILE:%s", ccfile); + if (renew_data->ccfile == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + } else { + renew_data->ccfile = talloc_strdup(renew_data, ccfile); + } + + renew_data->start_time = tgtt->starttime; + renew_data->lifetime = tgtt->endtime; + renew_data->start_renew_at = (time_t) (tgtt->starttime + + 0.5 *(tgtt->endtime - tgtt->starttime)); + + ret = copy_pam_data(renew_data, pd, &renew_data->pd); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "copy_pam_data failed.\n"); + goto done; + } + + sss_authtok_set_empty(renew_data->pd->newauthtok); + + ret = sss_authtok_set_ccfile(renew_data->pd->authtok, renew_data->ccfile, 0); + if (ret) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to store ccfile in auth token.\n"); + goto done; + } + + renew_data->pd->cmd = SSS_CMD_RENEW; + + value.type = HASH_VALUE_PTR; + value.ptr = renew_data; + + ret = hash_enter(krb5_ctx->renew_tgt_ctx->tgt_table, &key, &value); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "hash_enter failed.\n"); + ret = EFAULT; + goto done; + } + + DEBUG(SSSDBG_TRACE_LIBS, + "Added [%s] for renewal at [%.24s].\n", renew_data->ccfile, + ctime(&renew_data->start_renew_at)); + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(renew_data); + } + return ret; +} diff --git a/src/providers/krb5/krb5_utils.c b/src/providers/krb5/krb5_utils.c new file mode 100644 index 0000000..b890745 --- /dev/null +++ b/src/providers/krb5/krb5_utils.c @@ -0,0 +1,605 @@ +/* + SSSD + + Kerberos 5 Backend Module -- Utilities + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2009 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ +#include <string.h> +#include <stdlib.h> +#include <libgen.h> + +#include "providers/krb5/krb5_utils.h" +#include "providers/krb5/krb5_ccache.h" +#include "providers/krb5/krb5_auth.h" +#include "src/util/find_uid.h" +#include "util/util.h" + +errno_t find_or_guess_upn(TALLOC_CTX *mem_ctx, struct ldb_message *msg, + struct krb5_ctx *krb5_ctx, + struct sss_domain_info *dom, const char *user, + const char *user_dom, char **_upn) +{ + const char *upn = NULL; + int ret; + + if (krb5_ctx == NULL || dom == NULL || user == NULL || _upn == NULL) { + return EINVAL; + } + + if (msg != NULL) { + upn = ldb_msg_find_attr_as_string(msg, SYSDB_CANONICAL_UPN, NULL); + if (upn != NULL) { + ret = EOK; + goto done; + } + + upn = ldb_msg_find_attr_as_string(msg, SYSDB_UPN, NULL); + if (upn != NULL) { + ret = EOK; + goto done; + } + } + + ret = krb5_get_simple_upn(mem_ctx, krb5_ctx, dom, user, + user_dom, _upn); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "krb5_get_simple_upn failed.\n"); + return ret; + } + +done: + if (ret == EOK && upn != NULL) { + *_upn = talloc_strdup(mem_ctx, upn); + if (*_upn == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + return ENOMEM; + } + } + + return ret; +} + +errno_t check_if_cached_upn_needs_update(struct sysdb_ctx *sysdb, + struct sss_domain_info *domain, + const char *user, + const char *upn) +{ + TALLOC_CTX *tmp_ctx; + int ret; + int sret; + const char *attrs[] = {SYSDB_UPN, SYSDB_CANONICAL_UPN, NULL}; + struct sysdb_attrs *new_attrs; + struct ldb_result *res; + bool in_transaction = false; + const char *cached_upn; + const char *cached_canonical_upn; + + if (sysdb == NULL || user == NULL || upn == NULL) { + return EINVAL; + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_new failed.\n"); + return ENOMEM; + } + + ret = sysdb_get_user_attr(tmp_ctx, domain, user, attrs, &res); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sysdb_get_user_attr failed.\n"); + goto done; + } + + if (res->count != 1) { + DEBUG(SSSDBG_OP_FAILURE, "[%d] user objects for name [%s] found, " \ + "expected 1.\n", res->count, user); + ret = EINVAL; + goto done; + } + + cached_upn = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_UPN, NULL); + + if (cached_upn != NULL && strcmp(cached_upn, upn) == 0) { + DEBUG(SSSDBG_TRACE_ALL, "Cached UPN and new one match, " + "nothing to do.\n"); + ret = EOK; + goto done; + } + + cached_canonical_upn = ldb_msg_find_attr_as_string(res->msgs[0], + SYSDB_CANONICAL_UPN, + NULL); + + if (cached_canonical_upn != NULL + && strcmp(cached_canonical_upn, upn) == 0) { + DEBUG(SSSDBG_TRACE_ALL, "Cached canonical UPN and new one match, " + "nothing to do.\n"); + ret = EOK; + goto done; + } + + DEBUG(SSSDBG_TRACE_LIBS, "Replacing canonical UPN [%s] with [%s] " \ + "for user [%s].\n", + cached_canonical_upn == NULL ? + "empty" : cached_canonical_upn, + upn, user); + + new_attrs = sysdb_new_attrs(tmp_ctx); + if (new_attrs == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "sysdb_new_attrs failed.\n"); + ret = ENOMEM; + goto done; + } + + ret = sysdb_attrs_add_string(new_attrs, SYSDB_CANONICAL_UPN, upn); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sysdb_attrs_add_string failed.\n"); + goto done; + } + + ret = sysdb_transaction_start(sysdb); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Error %d starting transaction (%s)\n", ret, strerror(ret)); + goto done; + } + in_transaction = true; + + ret = sysdb_set_entry_attr(sysdb, res->msgs[0]->dn, new_attrs, + cached_canonical_upn == NULL ? SYSDB_MOD_ADD : + SYSDB_MOD_REP); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sysdb_set_entry_attr failed [%d][%s].\n", + ret, strerror(ret)); + goto done; + } + + ret = sysdb_transaction_commit(sysdb); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to commit transaction!\n"); + goto done; + } + in_transaction = false; + + ret = EOK; + +done: + if (in_transaction) { + sret = sysdb_transaction_cancel(sysdb); + if (sret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to cancel transaction\n"); + } + } + + talloc_free(tmp_ctx); + + return ret; +} + +#define S_EXP_UID "{uid}" +#define L_EXP_UID (sizeof(S_EXP_UID) - 1) +#define S_EXP_USERID "{USERID}" +#define L_EXP_USERID (sizeof(S_EXP_USERID) - 1) +#define S_EXP_EUID "{euid}" +#define L_EXP_EUID (sizeof(S_EXP_EUID) - 1) +#define S_EXP_USERNAME "{username}" +#define L_EXP_USERNAME (sizeof(S_EXP_USERNAME) - 1) + +static errno_t +check_ccache_re(const char *filename, sss_regexp_t *illegal_re) +{ + errno_t ret; + + ret = sss_regexp_match(illegal_re, filename, 0, 0); + + if (ret == 0) { + DEBUG(SSSDBG_OP_FAILURE, + "Illegal pattern in ccache directory name [%s].\n", filename); + return EINVAL; + } else if (ret == SSS_REGEXP_ERROR_NOMATCH) { + DEBUG(SSSDBG_TRACE_LIBS, + "Ccache directory name [%s] does not contain " + "illegal patterns.\n", filename); + return EOK; + } + + DEBUG(SSSDBG_CRIT_FAILURE, "regexp match failed [%d].\n", ret); + return EFAULT; +} + +char *expand_ccname_template(TALLOC_CTX *mem_ctx, struct krb5child_req *kr, + const char *template, sss_regexp_t *illegal_re, + bool file_mode, bool case_sensitive) +{ + char *copy; + char *p; + char *n; + char *result = NULL; + char *dummy; + char *name; + char *res = NULL; + const char *cache_dir_tmpl; + TALLOC_CTX *tmp_ctx = NULL; + char action; + bool rerun; + int ret; + + if (template == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing template.\n"); + return NULL; + } + + tmp_ctx = talloc_new(NULL); + if (!tmp_ctx) return NULL; + + copy = talloc_strdup(tmp_ctx, template); + if (copy == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n"); + goto done; + } + + result = talloc_strdup(tmp_ctx, ""); + if (result == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n"); + goto done; + } + + p = copy; + while ( (n = strchr(p, '%')) != NULL) { + *n = '\0'; + n++; + if ( *n == '\0' ) { + DEBUG(SSSDBG_CRIT_FAILURE, + "format error, single %% at the end of the template.\n"); + goto done; + } + + rerun = true; + action = *n; + while (rerun) { + rerun = false; + switch (action) { + case 'u': + if (kr->pd->user == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot expand user name template " + "because user name is empty.\n"); + goto done; + } + + name = sss_output_name(tmp_ctx, kr->pd->user, case_sensitive, 0); + if (name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "sss_output_name failed\n"); + goto done; + } + + result = talloc_asprintf_append(result, "%s%s", p, + name); + break; + case 'U': + if (kr->uid <= 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot expand uid template " + "because uid is invalid.\n"); + goto done; + } + result = talloc_asprintf_append(result, "%s%"SPRIuid, p, + kr->uid); + break; + case 'p': + if (kr->upn == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot expand user principal name template " + "because upn is empty.\n"); + goto done; + } + result = talloc_asprintf_append(result, "%s%s", p, kr->upn); + break; + case '%': + result = talloc_asprintf_append(result, "%s%%", p); + break; + case 'r': + dummy = dp_opt_get_string(kr->krb5_ctx->opts, KRB5_REALM); + if (dummy == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing kerberos realm.\n"); + goto done; + } + result = talloc_asprintf_append(result, "%s%s", p, dummy); + break; + case 'h': + if (kr->homedir == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot expand home directory template " + "because the path is not available.\n"); + goto done; + } + result = talloc_asprintf_append(result, "%s%s", p, kr->homedir); + break; + case 'd': + if (file_mode) { + cache_dir_tmpl = dp_opt_get_string(kr->krb5_ctx->opts, + KRB5_CCACHEDIR); + if (cache_dir_tmpl == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Missing credential cache directory.\n"); + goto done; + } + + dummy = expand_ccname_template(tmp_ctx, kr, cache_dir_tmpl, + illegal_re, false, case_sensitive); + if (dummy == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Expanding credential cache directory " + "template failed.\n"); + goto done; + } + result = talloc_asprintf_append(result, "%s%s", p, dummy); + talloc_zfree(dummy); + } else { + DEBUG(SSSDBG_CRIT_FAILURE, + "'%%d' is not allowed in this template.\n"); + goto done; + } + break; + case 'P': + if (!file_mode) { + DEBUG(SSSDBG_CRIT_FAILURE, + "'%%P' is not allowed in this template.\n"); + goto done; + } + if (kr->pd->cli_pid == 0) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot expand PID template " + "because PID is not available.\n"); + goto done; + } + result = talloc_asprintf_append(result, "%s%d", p, + kr->pd->cli_pid); + break; + + /* Additional syntax from krb5.conf default_ccache_name */ + case '{': + if (strncmp(n , S_EXP_UID, L_EXP_UID) == 0) { + action = 'U'; + n += L_EXP_UID - 1; + rerun = true; + continue; + } else if (strncmp(n , S_EXP_USERID, L_EXP_USERID) == 0) { + action = 'U'; + n += L_EXP_USERID - 1; + rerun = true; + continue; + } else if (strncmp(n , S_EXP_EUID, L_EXP_EUID) == 0) { + /* SSSD does not distinguish between uid and euid, + * so we treat both the same way */ + action = 'U'; + n += L_EXP_EUID - 1; + rerun = true; + continue; + } else if (strncmp(n , S_EXP_USERNAME, L_EXP_USERNAME) == 0) { + action = 'u'; + n += L_EXP_USERNAME - 1; + rerun = true; + continue; + } else { + /* ignore any expansion variable we do not understand and + * let libkrb5 hndle it or fail */ + name = n; + n = strchr(name, '}'); + if (!n) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Invalid substitution sequence in cache " + "template. Missing closing '}' in [%s].\n", + template); + goto done; + } + result = talloc_asprintf_append(result, "%s%%%.*s", p, + (int)(n - name + 1), name); + } + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "format error, unknown template [%%%c].\n", *n); + goto done; + } + } + + if (result == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf_append failed.\n"); + goto done; + } + + p = n + 1; + } + + result = talloc_asprintf_append(result, "%s", p); + if (result == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf_append failed.\n"); + goto done; + } + + if (illegal_re != NULL) { + ret = check_ccache_re(result, illegal_re); + if (ret != EOK) { + goto done; + } + } + + res = talloc_move(mem_ctx, &result); +done: + talloc_zfree(tmp_ctx); + return res; +} + +errno_t get_domain_or_subdomain(struct be_ctx *be_ctx, + char *domain_name, + struct sss_domain_info **dom) +{ + + if (domain_name != NULL && + strcasecmp(domain_name, be_ctx->domain->name) != 0) { + *dom = find_domain_by_name(be_ctx->domain, domain_name, true); + if (*dom == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "find_domain_by_name failed.\n"); + return ENOMEM; + } + } else { + *dom = be_ctx->domain; + } + + return EOK; +} + +static errno_t split_tuple(TALLOC_CTX *mem_ctx, const char *tuple, + const char **_first, const char **_second) +{ + errno_t ret; + char **list; + int n; + + ret = split_on_separator(mem_ctx, tuple, ':', true, true, &list, &n); + + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, + "split_on_separator failed - %s:[%d]\n", + sss_strerror(ret), ret); + goto done; + } else if (n != 2) { + DEBUG(SSSDBG_MINOR_FAILURE, + "split_on_separator failed - Expected format is: " + "'username:primary' but got: '%s'.\n", tuple); + ret = EINVAL; + goto done; + } + + *_first = list[0]; + *_second = list[1]; + +done: + return ret; +} + +static errno_t +fill_name_to_primary_map(TALLOC_CTX *mem_ctx, char **map, + struct map_id_name_to_krb_primary *name_to_primary, + size_t size) +{ + int i; + errno_t ret; + + for (i = 0; i < size; i++) { + ret = split_tuple(mem_ctx, map[i], + &name_to_primary[i].id_name, + &name_to_primary[i].krb_primary); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, + "split_tuple failed - %s:[%d]\n", sss_strerror(ret), ret); + goto done; + } + } + + ret = EOK; + +done: + return ret; +} + +errno_t +parse_krb5_map_user(TALLOC_CTX *mem_ctx, + const char *krb5_map_user, + const char *dom_name, + struct map_id_name_to_krb_primary **_name_to_primary) +{ + int size; + char **map = NULL; + errno_t ret; + TALLOC_CTX *tmp_ctx; + struct map_id_name_to_krb_primary *name_to_primary; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + ret = ENOMEM; + goto done; + } + + if (krb5_map_user == NULL || strlen(krb5_map_user) == 0) { + DEBUG(SSSDBG_CONF_SETTINGS, "krb5_map_user is empty!\n"); + size = 0; + } else { + ret = split_on_separator(tmp_ctx, krb5_map_user, ',', true, true, + &map, &size); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to parse krb5_map_user!\n"); + goto done; + } + } + + name_to_primary = talloc_zero_array(tmp_ctx, + struct map_id_name_to_krb_primary, + size + 1); + if (name_to_primary == NULL) { + ret = ENOMEM; + goto done; + } + /* sentinel */ + name_to_primary[size].id_name = NULL; + name_to_primary[size].krb_primary = NULL; + + if (size > 0) { + ret = fill_name_to_primary_map(name_to_primary, map, name_to_primary, + size); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, + "fill_name_to_primary_map failed: %s:[%d]\n", + sss_strerror(ret), ret); + goto done; + } + } + + /* conversion names to fully-qualified names */ + for (int i = 0; i < size; i++) { + name_to_primary[i].id_name = sss_create_internal_fqname( + name_to_primary, + name_to_primary[i].id_name, + dom_name); + if (name_to_primary[i].id_name == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "sss_create_internal_fqname failed\n"); + ret = ENOMEM; + goto done; + } + + name_to_primary[i].krb_primary = sss_create_internal_fqname( + name_to_primary, + name_to_primary[i].krb_primary, + dom_name); + if (name_to_primary[i].krb_primary == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "sss_create_internal_fqname failed\n"); + ret = ENOMEM; + goto done; + } + } + ret = EOK; + +done: + if (ret == EOK) { + *_name_to_primary = talloc_steal(mem_ctx, name_to_primary); + } + talloc_free(tmp_ctx); + return ret; +} diff --git a/src/providers/krb5/krb5_utils.h b/src/providers/krb5/krb5_utils.h new file mode 100644 index 0000000..473006b --- /dev/null +++ b/src/providers/krb5/krb5_utils.h @@ -0,0 +1,59 @@ +/* + SSSD + + Kerberos Backend, header file for utilities + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2009 Red Hat + + + 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 <http://www.gnu.org/licenses/>. +*/ + +#ifndef __KRB5_UTILS_H__ +#define __KRB5_UTILS_H__ + +#include <talloc.h> +#include "config.h" + +#include "providers/krb5/krb5_auth.h" +#include "providers/data_provider.h" + +errno_t find_or_guess_upn(TALLOC_CTX *mem_ctx, struct ldb_message *msg, + struct krb5_ctx *krb5_ctx, + struct sss_domain_info *dom, const char *user, + const char *user_dom, char **_upn); + +errno_t check_if_cached_upn_needs_update(struct sysdb_ctx *sysdb, + struct sss_domain_info *domain, + const char *user, + const char *upn); + +char *expand_ccname_template(TALLOC_CTX *mem_ctx, struct krb5child_req *kr, + const char *template, sss_regexp_t *illegal_re, + bool file_mode, bool case_sensitive); + +errno_t get_domain_or_subdomain(struct be_ctx *be_ctx, + char *domain_name, + struct sss_domain_info **dom); + +errno_t +parse_krb5_map_user(TALLOC_CTX *mem_ctx, + const char *krb5_map_user, + const char *dom_name, + struct map_id_name_to_krb_primary **_name_to_primary); + +#endif /* __KRB5_UTILS_H__ */ diff --git a/src/providers/krb5/krb5_wait_queue.c b/src/providers/krb5/krb5_wait_queue.c new file mode 100644 index 0000000..06d7a98 --- /dev/null +++ b/src/providers/krb5/krb5_wait_queue.c @@ -0,0 +1,373 @@ +/* + SSSD + + Kerberos 5 Backend Module - Serialize the request of a user + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2010 Red Hat + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <tevent.h> +#include <dhash.h> + +#include <security/pam_modules.h> + +#include "src/providers/krb5/krb5_auth.h" + +struct queue_entry { + struct queue_entry *prev; + struct queue_entry *next; + + struct be_ctx *be_ctx; + struct be_req *be_req; + struct tevent_req *parent_req; + struct pam_data *pd; + struct krb5_ctx *krb5_ctx; +}; + +static void wait_queue_auth_done(struct tevent_req *req); + +static void krb5_auth_queue_finish(struct tevent_req *req, errno_t ret, + int pam_status, int dp_err); + +static void wait_queue_auth(struct tevent_context *ev, struct tevent_timer *te, + struct timeval current_time, void *private_data) +{ + struct queue_entry *qe = talloc_get_type(private_data, struct queue_entry); + struct tevent_req *req; + + req = krb5_auth_send(qe->parent_req, qe->be_ctx->ev, + qe->be_ctx, qe->pd, qe->krb5_ctx); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_auth_send failed.\n"); + } else { + tevent_req_set_callback(req, wait_queue_auth_done, + qe->parent_req); + } + + talloc_zfree(qe); +} + +static void wait_queue_auth_done(struct tevent_req *req) +{ + struct tevent_req *parent_req = \ + tevent_req_callback_data(req, struct tevent_req); + int pam_status; + int dp_err; + errno_t ret; + + ret = krb5_auth_recv(req, &pam_status, &dp_err); + talloc_zfree(req); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "krb5_auth_recv failed: %d\n", ret); + } + + krb5_auth_queue_finish(parent_req, ret, pam_status, dp_err); +} + +static void wait_queue_del_cb(hash_entry_t *entry, hash_destroy_enum type, + void *pvt) +{ + struct queue_entry *head; + + if (entry->value.type == HASH_VALUE_PTR) { + head = talloc_get_type(entry->value.ptr, struct queue_entry); + talloc_zfree(head); + return; + } + + DEBUG(SSSDBG_CRIT_FAILURE, + "Unexpected value type [%d].\n", entry->value.type); +} + +static errno_t add_to_wait_queue(struct be_ctx *be_ctx, + struct tevent_req *parent_req, + struct pam_data *pd, + struct krb5_ctx *krb5_ctx) +{ + int ret; + hash_key_t key; + hash_value_t value; + struct queue_entry *head; + struct queue_entry *queue_entry; + + if (krb5_ctx->wait_queue_hash == NULL) { + ret = sss_hash_create_ex(krb5_ctx, 0, + &krb5_ctx->wait_queue_hash, 0, 0, 0, 0, + wait_queue_del_cb, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_hash_create failed\n"); + return ret; + } + } + + key.type = HASH_KEY_STRING; + key.str = pd->user; + + ret = hash_lookup(krb5_ctx->wait_queue_hash, &key, &value); + switch (ret) { + case HASH_SUCCESS: + if (value.type != HASH_VALUE_PTR) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unexpected hash value type.\n"); + return EINVAL; + } + + head = talloc_get_type(value.ptr, struct queue_entry); + + queue_entry = talloc_zero(head, struct queue_entry); + if (queue_entry == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + return ENOMEM; + } + + queue_entry->be_ctx = be_ctx; + queue_entry->parent_req = parent_req; + queue_entry->pd = pd; + queue_entry->krb5_ctx = krb5_ctx; + + DLIST_ADD_END(head, queue_entry, struct queue_entry *); + + break; + case HASH_ERROR_KEY_NOT_FOUND: + value.type = HASH_VALUE_PTR; + head = talloc_zero(krb5_ctx->wait_queue_hash, struct queue_entry); + if (head == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n"); + return ENOMEM; + } + value.ptr = head; + + ret = hash_enter(krb5_ctx->wait_queue_hash, &key, &value); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "hash_enter failed.\n"); + talloc_free(head); + return EIO; + } + + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, "hash_lookup failed.\n"); + return EIO; + } + + if (head->next == NULL) { + return ENOENT; + } else { + return EOK; + } +} + +static void check_wait_queue(struct krb5_ctx *krb5_ctx, char *username) +{ + int ret; + hash_key_t key; + hash_value_t value; + struct queue_entry *head; + struct queue_entry *queue_entry; + struct tevent_timer *te; + + if (krb5_ctx->wait_queue_hash == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "No wait queue available.\n"); + return; + } + + key.type = HASH_KEY_STRING; + key.str = username; + + ret = hash_lookup(krb5_ctx->wait_queue_hash, &key, &value); + + switch (ret) { + case HASH_SUCCESS: + if (value.type != HASH_VALUE_PTR) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unexpected hash value type.\n"); + return; + } + + head = talloc_get_type(value.ptr, struct queue_entry); + + if (head->next == NULL) { + DEBUG(SSSDBG_TRACE_LIBS, + "Wait queue for user [%s] is empty.\n", username); + } else { + queue_entry = head->next; + + DLIST_REMOVE(head, queue_entry); + + te = tevent_add_timer(queue_entry->be_ctx->ev, krb5_ctx, + tevent_timeval_current(), wait_queue_auth, + queue_entry); + if (te == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_add_timer failed.\n"); + } else { + return; + } + } + + ret = hash_delete(krb5_ctx->wait_queue_hash, &key); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to remove wait queue for user [%s].\n", + username); + } + + break; + case HASH_ERROR_KEY_NOT_FOUND: + DEBUG(SSSDBG_CRIT_FAILURE, + "No wait queue for user [%s] found.\n", username); + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, "hash_lookup failed.\n"); + } + + return; +} + +struct krb5_auth_queue_state { + struct krb5_ctx *krb5_ctx; + struct pam_data *pd; + + int pam_status; + int dp_err; +}; + +static void krb5_auth_queue_done(struct tevent_req *subreq); + +struct tevent_req *krb5_auth_queue_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct be_ctx *be_ctx, + struct pam_data *pd, + struct krb5_ctx *krb5_ctx) +{ + errno_t ret; + struct tevent_req *req; + struct tevent_req *subreq; + struct krb5_auth_queue_state *state; + + req = tevent_req_create(mem_ctx, &state, struct krb5_auth_queue_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create failed.\n"); + return NULL; + } + state->krb5_ctx = krb5_ctx; + state->pd = pd; + + ret = add_to_wait_queue(be_ctx, req, pd, krb5_ctx); + if (ret == EOK) { + DEBUG(SSSDBG_TRACE_LIBS, + "Request [%p] successfully added to wait queue " + "of user [%s].\n", req, pd->user); + ret = EOK; + goto immediate; + } else if (ret == ENOENT) { + DEBUG(SSSDBG_TRACE_LIBS, "Wait queue of user [%s] is empty, " + "running request [%p] immediately.\n", pd->user, req); + } else { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to add request to wait queue of user [%s], " + "running request [%p] immediately.\n", pd->user, req); + } + + subreq = krb5_auth_send(req, ev, be_ctx, pd, krb5_ctx); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "krb5_auth_send failed.\n"); + ret = ENOMEM; + goto immediate; + } + + tevent_req_set_callback(subreq, krb5_auth_queue_done, req); + + ret = EOK; + +immediate: + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + return req; +} + +static void krb5_auth_queue_done(struct tevent_req *subreq) +{ + struct tevent_req *req = \ + tevent_req_callback_data(subreq, struct tevent_req); + struct krb5_auth_queue_state *state = \ + tevent_req_data(req, struct krb5_auth_queue_state); + errno_t ret; + + ret = krb5_auth_recv(subreq, &state->pam_status, &state->dp_err); + talloc_zfree(subreq); + + check_wait_queue(state->krb5_ctx, state->pd->user); + + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "krb5_auth_recv failed with: %d\n", ret); + tevent_req_error(req, ret); + return; + } + + DEBUG(SSSDBG_TRACE_LIBS, "krb5_auth_queue request [%p] done.\n", req); + tevent_req_done(req); +} + +/* This is a violation of the tevent_req style. Ideally, the wait queue would + * be rewritten to the tevent_req style in the future, expose per-request recv + * and not hide the request underneath. But this function allows us to expose + * a tevent_req API for users of this module + */ +static void krb5_auth_queue_finish(struct tevent_req *req, + errno_t ret, + int pam_status, + int dp_err) +{ + struct krb5_auth_queue_state *state = \ + tevent_req_data(req, struct krb5_auth_queue_state); + + check_wait_queue(state->krb5_ctx, state->pd->user); + + state->pam_status = pam_status; + state->dp_err = dp_err; + if (ret != EOK) { + tevent_req_error(req, ret); + } else { + DEBUG(SSSDBG_TRACE_LIBS, "krb5_auth_queue request [%p] done.\n", req); + tevent_req_done(req); + } +} + +int krb5_auth_queue_recv(struct tevent_req *req, + int *_pam_status, + int *_dp_err) +{ + struct krb5_auth_queue_state *state = \ + tevent_req_data(req, struct krb5_auth_queue_state); + + /* Returning values even on failure is not typical, but IPA password migration + * relies on receiving PAM_CRED_ERR even if the request fails.. + */ + if (_pam_status) { + *_pam_status = state->pam_status; + } + + if (_dp_err) { + *_dp_err = state->dp_err; + } + + TEVENT_REQ_RETURN_ON_ERROR(req); + + return EOK; +} |