/* SSSD PAM Responder - passkey related requests Copyright (C) Justin Stephenson 2022 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 . */ #include "util/child_common.h" #include "util/authtok.h" #include "db/sysdb.h" #include "db/sysdb_passkey_user_verification.h" #include "responder/pam/pamsrv.h" #include "responder/pam/pamsrv_passkey.h" struct pam_passkey_verification_enum_str { enum passkey_user_verification verification; const char *option; }; struct pam_passkey_table_data { hash_table_t *table; char *key; struct pk_child_user_data *data; }; struct pam_passkey_verification_enum_str pam_passkey_verification_enum_str[] = { { PAM_PASSKEY_VERIFICATION_ON, "on" }, { PAM_PASSKEY_VERIFICATION_OFF, "off" }, { PAM_PASSKEY_VERIFICATION_OMIT, "unset" }, { PAM_PASSKEY_VERIFICATION_INVALID, NULL } }; #define PASSKEY_PREFIX "passkey:" #define USER_VERIFICATION "user_verification=" #define USER_VERIFICATION_LEN (sizeof(USER_VERIFICATION) -1) const char *pam_passkey_verification_enum_to_string(enum passkey_user_verification verification) { size_t c; for (c = 0 ; pam_passkey_verification_enum_str[c].option != NULL; c++) { if (pam_passkey_verification_enum_str[c].verification == verification) { return pam_passkey_verification_enum_str[c].option; } } return "(NULL)"; } struct passkey_ctx { struct pam_ctx *pam_ctx; struct tevent_context *ev; struct pam_data *pd; struct pam_auth_req *preq; }; void pam_forwarder_passkey_cb(struct tevent_req *req); errno_t pam_passkey_concatenate_keys(TALLOC_CTX *mem_ctx, struct pk_child_user_data *pk_data, bool kerberos_pa, char **_result_kh, char **_result_ph); struct tevent_req *pam_passkey_get_mapping_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev, struct passkey_ctx *pctx); void pam_passkey_get_user_done(struct tevent_req *req); void pam_passkey_get_mapping_done(struct tevent_req *req); errno_t pam_passkey_get_mapping_recv(TALLOC_CTX *mem_ctx, struct tevent_req *req, struct cache_req_result **_result); struct passkey_get_mapping_state { struct pam_data *pd; struct cache_req_result *result; }; void passkey_kerberos_cb(struct tevent_req *req) { struct pam_auth_req *preq = tevent_req_callback_data(req, struct pam_auth_req); errno_t ret = EOK; int child_status; ret = pam_passkey_auth_recv(req, &child_status); talloc_free(req); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "PAM passkey auth failed [%d]: %s\n", ret, sss_strerror(ret)); goto done; } DEBUG(SSSDBG_TRACE_FUNC, "passkey child finished with status [%d]\n", child_status); pam_check_user_search(preq); done: pam_check_user_done(preq, ret); } errno_t passkey_kerberos(struct pam_ctx *pctx, struct pam_data *pd, struct pam_auth_req *preq) { errno_t ret; const char *prompt; const char *key; const char *pin; size_t pin_len; struct pk_child_user_data *data; struct tevent_req *req; int timeout; char *verify_opts; bool debug_libfido2; enum passkey_user_verification verification; ret = sss_authtok_get_passkey(preq, preq->pd->authtok, &prompt, &key, &pin, &pin_len); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failure to get passkey authtok\n"); return EIO; } if (prompt == NULL || key == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Passkey prompt and key are missing or invalid.\n"); return EIO; } data = sss_ptr_hash_lookup(pctx->pk_table_data->table, key, struct pk_child_user_data); if (data == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to lookup passkey authtok\n"); return EIO; } ret = confdb_get_int(pctx->rctx->cdb, CONFDB_PAM_CONF_ENTRY, CONFDB_PAM_PASSKEY_CHILD_TIMEOUT, PASSKEY_CHILD_TIMEOUT_DEFAULT, &timeout); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to read passkey_child_timeout from confdb: [%d]: %s\n", ret, sss_strerror(ret)); goto done; } ret = confdb_get_string(pctx->rctx->cdb, preq, CONFDB_MONITOR_CONF_ENTRY, CONFDB_MONITOR_PASSKEY_VERIFICATION, NULL, &verify_opts); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to read '"CONFDB_MONITOR_PASSKEY_VERIFICATION"' from confdb: [%d]: %s\n", ret, sss_strerror(ret)); goto done; } /* Always use verification sent from passkey krb5 plugin */ if (strcasecmp(data->user_verification, "false") == 0) { verification = PAM_PASSKEY_VERIFICATION_OFF; } else { verification = PAM_PASSKEY_VERIFICATION_ON; } ret = confdb_get_bool(pctx->rctx->cdb, CONFDB_PAM_CONF_ENTRY, CONFDB_PAM_PASSKEY_DEBUG_LIBFIDO2, false, &debug_libfido2); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to read '"CONFDB_PAM_PASSKEY_DEBUG_LIBFIDO2"' from confdb: [%d]: %s\n", ret, sss_strerror(ret)); goto done; } req = pam_passkey_auth_send(preq->cctx, preq->cctx->ev, timeout, debug_libfido2, verification, pd, data, true); if (req == NULL) { DEBUG(SSSDBG_OP_FAILURE, "passkey auth send failed [%d]: [%s]\n", ret, sss_strerror(ret)); goto done; } tevent_req_set_callback(req, passkey_kerberos_cb, preq); ret = EAGAIN; done: return ret; } errno_t passkey_local(TALLOC_CTX *mem_ctx, struct tevent_context *ev, struct pam_ctx *pam_ctx, struct pam_auth_req *preq, struct pam_data *pd) { struct tevent_req *req; struct passkey_ctx *pctx; errno_t ret; pctx = talloc_zero(mem_ctx, struct passkey_ctx); if (pctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "pctx == NULL\n"); return ENOMEM; } pctx->pd = pd; pctx->pam_ctx = pam_ctx; pctx->ev = ev; pctx->preq = preq; DEBUG(SSSDBG_TRACE_FUNC, "Checking for passkey authentication data\n"); req = pam_passkey_get_mapping_send(mem_ctx, pctx->ev, pctx); if (req == NULL) { DEBUG(SSSDBG_OP_FAILURE, "pam_passkey_get_mapping_send failed.\n"); ret = ENOMEM; goto done; } tevent_req_set_callback(req, pam_passkey_get_user_done, pctx); ret = EAGAIN; done: if (ret != EAGAIN) { talloc_free(pctx); } return ret; } struct tevent_req *pam_passkey_get_mapping_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev, struct passkey_ctx *pk_ctx) { struct passkey_get_mapping_state *state; struct tevent_req *req; struct tevent_req *subreq; int ret; static const char *attrs[] = { SYSDB_NAME, SYSDB_USER_PASSKEY, NULL }; req = tevent_req_create(mem_ctx, &state, struct passkey_get_mapping_state); if (req == NULL) { ret = ENOMEM; goto done; } subreq = cache_req_user_by_name_attrs_send(state, pk_ctx->ev, pk_ctx->pam_ctx->rctx, pk_ctx->pam_ctx->rctx->ncache, 0, pk_ctx->pd->domain, pk_ctx->pd->user, attrs); if (subreq == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Unable to create tevent request!\n"); ret = ENOMEM; goto done; } tevent_req_set_callback(subreq, pam_passkey_get_mapping_done, req); return req; done: tevent_req_error(req, ret); tevent_req_post(req, ev); return req; } void pam_passkey_get_mapping_done(struct tevent_req *subreq) { struct cache_req_result *result; struct tevent_req *req; struct passkey_get_mapping_state *state; errno_t ret; req = tevent_req_callback_data(subreq, struct tevent_req); state = tevent_req_data(req, struct passkey_get_mapping_state); ret = cache_req_user_by_name_attrs_recv(state, subreq, &result); state->result = result; talloc_zfree(subreq); if (ret != EOK) { tevent_req_error(req, ret); return; } tevent_req_done(req); return; } errno_t pam_passkey_get_mapping_recv(TALLOC_CTX *mem_ctx, struct tevent_req *req, struct cache_req_result **_result) { struct passkey_get_mapping_state *state = NULL; state = tevent_req_data(req, struct passkey_get_mapping_state); TEVENT_REQ_RETURN_ON_ERROR(req); *_result = talloc_steal(mem_ctx, state->result); return EOK; } errno_t read_passkey_conf_verification(TALLOC_CTX *mem_ctx, const char *verify_opts, enum passkey_user_verification *_user_verification) { int ret; TALLOC_CTX *tmp_ctx; char **opts; size_t c; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_new failed.\n"); return ENOMEM; } if (verify_opts == NULL) { ret = EOK; goto done; } ret = split_on_separator(tmp_ctx, verify_opts, ',', true, true, &opts, NULL); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "split_on_separator failed [%d], %s.\n", ret, sss_strerror(ret)); goto done; } for (c = 0; opts[c] != NULL; c++) { if (strncasecmp(opts[c], USER_VERIFICATION, USER_VERIFICATION_LEN) == 0) { if (strcasecmp("true", &opts[c][USER_VERIFICATION_LEN]) == 0) { DEBUG(SSSDBG_TRACE_FUNC, "user_verification set to true.\n"); *_user_verification = PAM_PASSKEY_VERIFICATION_ON; } else if (strcasecmp("false", &opts[c][USER_VERIFICATION_LEN]) == 0) { DEBUG(SSSDBG_TRACE_FUNC, "user_verification set to false.\n"); *_user_verification = PAM_PASSKEY_VERIFICATION_OFF; } } else { DEBUG(SSSDBG_MINOR_FAILURE, "Unsupported passkey verification option [%s], " \ "skipping.\n", opts[c]); } } ret = EOK; done: talloc_free(tmp_ctx); return ret; } static errno_t passkey_local_verification(TALLOC_CTX *mem_ctx, struct passkey_ctx *pctx, struct confdb_ctx *cdb, struct sysdb_ctx *sysdb, const char *domain_name, struct pk_child_user_data *pk_data, enum passkey_user_verification *_user_verification, bool *_debug_libfido2) { TALLOC_CTX *tmp_ctx; errno_t ret; const char *verification_from_ldap; char *verify_opts = NULL; bool debug_libfido2 = false; enum passkey_user_verification verification = PAM_PASSKEY_VERIFICATION_OMIT; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } ret = sysdb_domain_get_passkey_user_verification(tmp_ctx, sysdb, domain_name, &verification_from_ldap); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to read passkeyUserVerification from sysdb: [%d]: %s\n", ret, sss_strerror(ret)); /* This is expected for AD and LDAP */ ret = EOK; goto done; } ret = confdb_get_bool(pctx->pam_ctx->rctx->cdb, CONFDB_PAM_CONF_ENTRY, CONFDB_PAM_PASSKEY_DEBUG_LIBFIDO2, false, &debug_libfido2); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to read '"CONFDB_PAM_PASSKEY_DEBUG_LIBFIDO2"' from confdb: [%d]: %s\n", ret, sss_strerror(ret)); goto done; } /* If require user verification setting is set in LDAP, use it */ if (verification_from_ldap != NULL) { if (strcasecmp(verification_from_ldap, "true") == 0) { verification = PAM_PASSKEY_VERIFICATION_ON; } else if (strcasecmp(verification_from_ldap, "false") == 0) { verification = PAM_PASSKEY_VERIFICATION_OFF; } DEBUG(SSSDBG_TRACE_FUNC, "Passkey verification is being enforced from LDAP\n"); } else { /* No verification set in LDAP, fallback to local sssd.conf setting */ ret = confdb_get_string(pctx->pam_ctx->rctx->cdb, tmp_ctx, CONFDB_MONITOR_CONF_ENTRY, CONFDB_MONITOR_PASSKEY_VERIFICATION, NULL, &verify_opts); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to read '"CONFDB_MONITOR_PASSKEY_VERIFICATION"' from confdb: [%d]: %s\n", ret, sss_strerror(ret)); goto done; } ret = read_passkey_conf_verification(tmp_ctx, verify_opts, &verification); if (ret != EOK) { DEBUG(SSSDBG_MINOR_FAILURE, "Unable to parse passkey verificaton options.\n"); /* Continue anyway */ } DEBUG(SSSDBG_TRACE_FUNC, "Passkey verification is being enforced from local configuration\n"); } DEBUG(SSSDBG_TRACE_FUNC, "Passkey verification setting [%s]\n", pam_passkey_verification_enum_to_string(verification)); *_user_verification = verification; *_debug_libfido2 = debug_libfido2; ret = EOK; done: talloc_free(tmp_ctx); return ret; } errno_t process_passkey_data(TALLOC_CTX *mem_ctx, struct ldb_message *user_mesg, const char *domain, struct pk_child_user_data *_data) { struct ldb_message_element *el; TALLOC_CTX *tmp_ctx; int ret; char **mappings; const char **kh_mappings; const char **public_keys; const char *domain_name; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { ERROR("talloc_new() failed\n"); return ENOMEM; } el = ldb_msg_find_element(user_mesg, SYSDB_USER_PASSKEY); if (el == NULL) { DEBUG(SSSDBG_TRACE_FUNC, "No passkey data found\n"); ret = ENOENT; goto done; } /* This attribute may contain other mapping data unrelated to passkey. In that case * let's omit it. For example, AD user altSecurityIdentities may store ssh public key * or smart card mapping data */ ret = split_on_separator(tmp_ctx, (const char *) el->values[0].data, ':', true, true, &mappings, NULL); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Incorrectly formatted passkey data [%d]: %s\n", ret, sss_strerror(ret)); ret = ENOENT; goto done; } else if (strcasecmp(mappings[0], "passkey") != 0) { DEBUG(SSSDBG_TRACE_FUNC, "Mapping data found is not passkey related\n"); ret = ENOENT; goto done; } kh_mappings = talloc_zero_array(tmp_ctx, const char *, el->num_values + 1); if (kh_mappings == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_zero_array failed.\n"); ret = ENOMEM; goto done; } public_keys = talloc_zero_array(tmp_ctx, const char *, el->num_values + 1); if (public_keys == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_zero_array failed.\n"); ret = ENOMEM; goto done; } for (int i = 0; i < el->num_values; i++) { ret = split_on_separator(tmp_ctx, (const char *) el->values[i].data, ',', true, true, &mappings, NULL); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Incorrectly formatted passkey data [%d]: %s\n", ret, sss_strerror(ret)); goto done; } kh_mappings[i] = talloc_strdup(kh_mappings, mappings[0] + strlen(PASSKEY_PREFIX)); if (kh_mappings[i] == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup key handle failed.\n"); ret = ENOMEM; goto done; } public_keys[i] = talloc_strdup(public_keys, mappings[1]); if (public_keys[i] == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup public key failed.\n"); ret = ENOMEM; goto done; } } domain_name = talloc_strdup(tmp_ctx, domain); if (domain_name == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup domain failed.\n"); ret = ENOMEM; goto done; } _data->domain = talloc_steal(mem_ctx, domain_name); _data->key_handles = talloc_steal(mem_ctx, kh_mappings); _data->public_keys = talloc_steal(mem_ctx, public_keys); _data->num_credentials = el->num_values; ret = EOK; done: talloc_free(tmp_ctx); return ret; } void pam_forwarder_passkey_cb(struct tevent_req *req) { struct pam_auth_req *preq = tevent_req_callback_data(req, struct pam_auth_req); errno_t ret = EOK; int child_status; ret = pam_passkey_auth_recv(req, &child_status); talloc_free(req); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "PAM passkey auth failed [%d]: %s\n", ret, sss_strerror(ret)); goto done; } preq->pd->passkey_local_done = true; DEBUG(SSSDBG_TRACE_FUNC, "passkey child finished with status [%d]\n", child_status); preq->pd->pam_status = PAM_SUCCESS; pam_reply(preq); return; done: pam_check_user_done(preq, ret); } void pam_passkey_get_user_done(struct tevent_req *req) { int ret; struct passkey_ctx *pctx; bool debug_libfido2 = false; char *domain_name = NULL; int timeout; struct cache_req_result *result = NULL; struct pk_child_user_data *pk_data = NULL; enum passkey_user_verification verification = PAM_PASSKEY_VERIFICATION_OMIT; pctx = tevent_req_callback_data(req, struct passkey_ctx); ret = pam_passkey_get_mapping_recv(pctx, req, &result); talloc_zfree(req); if (ret != EOK && ret != ENOENT) { DEBUG(SSSDBG_OP_FAILURE, "cache_req_user_by_name_attrs_recv failed [%d]: %s.\n", ret, sss_strerror(ret)); goto done; } if (result == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "cache req result == NULL\n"); ret = ENOMEM; goto done; } pk_data = talloc_zero(pctx, struct pk_child_user_data); if (!pk_data) { DEBUG(SSSDBG_CRIT_FAILURE, "pk_data == NULL\n"); ret = ENOMEM; goto done; } /* Use dns_name for AD/IPA - for LDAP fallback to domain->name */ if (result->domain != NULL) { domain_name = result->domain->dns_name; if (domain_name == NULL) { domain_name = result->domain->name; } } if (domain_name == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Invalid or missing domain name\n"); ret = EIO; goto done; } /* Get passkey data */ DEBUG(SSSDBG_TRACE_ALL, "Processing passkey data\n"); ret = process_passkey_data(pk_data, result->msgs[0], domain_name, pk_data); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "process_passkey_data failed: [%d]: %s\n", ret, sss_strerror(ret)); goto done; } /* timeout */ ret = confdb_get_int(pctx->pam_ctx->rctx->cdb, CONFDB_PAM_CONF_ENTRY, CONFDB_PAM_PASSKEY_CHILD_TIMEOUT, PASSKEY_CHILD_TIMEOUT_DEFAULT, &timeout); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to read passkey_child_timeout from confdb: [%d]: %s\n", ret, sss_strerror(ret)); goto done; } ret = passkey_local_verification(pctx, pctx, pctx->pam_ctx->rctx->cdb, result->domain->sysdb, result->domain->dns_name, pk_data, &verification, &debug_libfido2); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to check passkey verification [%d]: %s\n", ret, sss_strerror(ret)); goto done; } /* Preauth respond with prompt_pin true or false based on user verification */ if (pctx->pd->cmd == SSS_PAM_PREAUTH) { const char *prompt_pin = verification == PAM_PASSKEY_VERIFICATION_OFF ? "false" : "true"; ret = pam_add_response(pctx->pd, SSS_PAM_PASSKEY_INFO, strlen(prompt_pin) + 1, (const uint8_t *) prompt_pin); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed. [%d]: %s\n", ret, sss_strerror(ret)); goto done; } pctx->pd->pam_status = PAM_SUCCESS; pam_reply(pctx->preq); talloc_free(pk_data); return; } req = pam_passkey_auth_send(pctx, pctx->ev, timeout, debug_libfido2, verification, pctx->pd, pk_data, false); if (req == NULL) { DEBUG(SSSDBG_OP_FAILURE, "pam_passkey_auth_send failed [%d]: %s\n", ret, sss_strerror(ret)); goto done; } tevent_req_set_callback(req, pam_forwarder_passkey_cb, pctx->preq); done: if (pk_data != NULL) { talloc_free(pk_data); } if (ret == ENOENT) { /* No passkey data, continue through to typical auth flow */ DEBUG(SSSDBG_TRACE_FUNC, "No passkey data found, skipping passkey auth\n"); pctx->preq->passkey_data_exists = false; pam_check_user_search(pctx->preq); } else if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Unexpected passkey error [%d]: %s." " Skipping passkey auth\n", ret, sss_strerror(ret)); pam_check_user_search(pctx->preq); } return; } struct pam_passkey_auth_send_state { struct pam_data *pd; struct tevent_context *ev; struct tevent_timer *timeout_handler; struct sss_child_ctx_old *child_ctx; struct child_io_fds *io; const char *logfile; const char **extra_args; char *verify_opts; int timeout; int child_status; bool kerberos_pa; }; static errno_t passkey_child_exec(struct tevent_req *req); static void pam_passkey_auth_done(int child_status, struct tevent_signal *sige, void *pvt); static int pin_destructor(void *ptr) { uint8_t *pin = talloc_get_type(ptr, uint8_t); if (pin == NULL) return EOK; sss_erase_talloc_mem_securely(pin); return EOK; } errno_t get_passkey_child_write_buffer(TALLOC_CTX *mem_ctx, struct pam_data *pd, uint8_t **_buf, size_t *_len) { int ret; uint8_t *buf; size_t len; const char *pin = NULL; if (pd == NULL || pd->authtok == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Missing authtok.\n"); return EINVAL; } if (sss_authtok_get_type(pd->authtok) == SSS_AUTHTOK_TYPE_PASSKEY || sss_authtok_get_type(pd->authtok) == SSS_AUTHTOK_TYPE_PASSKEY_KRB) { ret = sss_authtok_get_passkey_pin(pd->authtok, &pin, &len); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "sss_authtok_get_passkey_pin failed [%d]: %s\n", ret, sss_strerror(ret)); return ret; } if (pin == NULL || len == 0) { DEBUG(SSSDBG_OP_FAILURE, "Missing PIN.\n"); return EINVAL; } buf = talloc_size(mem_ctx, len); if (buf == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_size failed.\n"); return ENOMEM; } talloc_set_destructor((void *) buf, pin_destructor); safealign_memcpy(buf, pin, len, NULL); } else { DEBUG(SSSDBG_CRIT_FAILURE, "Unsupported authtok type [%d].\n", sss_authtok_get_type(pd->authtok)); return EINVAL; } *_len = len; *_buf = buf; return EOK; } static void pam_passkey_child_read_data(struct tevent_req *subreq) { uint8_t *buf; ssize_t buf_len; char *str; struct tevent_req *req = tevent_req_callback_data(subreq, struct tevent_req); struct pam_passkey_auth_send_state *state = tevent_req_data(req, struct pam_passkey_auth_send_state); int ret; ret = read_pipe_recv(subreq, state, &buf, &buf_len); talloc_zfree(subreq); if (ret != EOK) { tevent_req_error(req, ret); return; } str = malloc(sizeof(char) * buf_len); if (str == NULL) { return; } snprintf(str, buf_len, "%s", buf); sss_authtok_set_passkey_reply(state->pd->authtok, str, 0); free(str); tevent_req_done(req); return; } static void passkey_child_write_done(struct tevent_req *subreq) { struct tevent_req *req = tevent_req_callback_data(subreq, struct tevent_req); struct pam_passkey_auth_send_state *state = tevent_req_data(req, struct pam_passkey_auth_send_state); int ret; DEBUG(SSSDBG_TRACE_LIBS, "Sending passkey data complete\n"); ret = write_pipe_recv(subreq); talloc_zfree(subreq); if (ret != EOK) { tevent_req_error(req, ret); return; } PIPE_FD_CLOSE(state->io->write_to_child_fd); if (state->kerberos_pa) { /* Read data back from passkey child */ subreq = read_pipe_send(state, state->ev, state->io->read_from_child_fd); if (subreq == NULL) { DEBUG(SSSDBG_OP_FAILURE, "read_pipe_send failed.\n"); return; } tevent_req_set_callback(subreq, pam_passkey_child_read_data, req); } } errno_t pam_passkey_concatenate_keys(TALLOC_CTX *mem_ctx, struct pk_child_user_data *pk_data, bool kerberos_pa, char **_result_kh, char **_result_pk) { errno_t ret; char *result_kh = NULL; char *result_pk = NULL; result_kh = talloc_strdup(mem_ctx, pk_data->key_handles[0]); if (!kerberos_pa) { result_pk = talloc_strdup(mem_ctx, pk_data->public_keys[0]); } for (int i = 1; i < pk_data->num_credentials; i++) { result_kh = talloc_strdup_append(result_kh, ","); if (result_kh == NULL) { ret = ENOMEM; goto done; } result_kh = talloc_strdup_append(result_kh, pk_data->key_handles[i]); if (result_kh == NULL) { ret = ENOMEM; goto done; } if (!kerberos_pa) { result_pk = talloc_strdup_append(result_pk, ","); if (result_pk == NULL) { ret = ENOMEM; goto done; } result_pk = talloc_strdup_append(result_pk, pk_data->public_keys[i]); if (result_kh == NULL || result_pk == NULL) { ret = ENOMEM; goto done; } } } *_result_kh = result_kh; *_result_pk = result_pk; ret = EOK; done: return ret; } struct tevent_req * pam_passkey_auth_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev, int timeout, bool debug_libfido2, enum passkey_user_verification verification, struct pam_data *pd, struct pk_child_user_data *pk_data, bool kerberos_pa) { struct tevent_req *req; struct pam_passkey_auth_send_state *state; size_t arg_c = 0; char *result_kh; char *result_pk; int num_args; int ret; req = tevent_req_create(mem_ctx, &state, struct pam_passkey_auth_send_state); if (req == NULL) { return NULL; } state->pd = pd; state->ev = ev; state->timeout = timeout; state->kerberos_pa = kerberos_pa; state->logfile = PASSKEY_CHILD_LOG_FILE; state->io = talloc(state, struct child_io_fds); if (state->io == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc child fds failed.\n"); ret = ENOMEM; goto done; } state->io->write_to_child_fd = -1; state->io->read_from_child_fd = -1; talloc_set_destructor((void *) state->io, child_io_destructor); num_args = 11; state->extra_args = talloc_zero_array(state, const char *, num_args + 1); if (state->extra_args == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_zero_array failed.\n"); ret = ENOMEM; goto done; } switch (verification) { case PAM_PASSKEY_VERIFICATION_ON: state->extra_args[arg_c++] = "--user-verification=true"; DEBUG(SSSDBG_TRACE_FUNC, "Calling child with user-verification true\n"); break; case PAM_PASSKEY_VERIFICATION_OFF: state->extra_args[arg_c++] = "--user-verification=false"; DEBUG(SSSDBG_TRACE_FUNC, "Calling child with user-verification false\n"); break; default: DEBUG(SSSDBG_TRACE_FUNC, "Calling child with user-verification unset\n"); break; } ret = pam_passkey_concatenate_keys(state, pk_data, state->kerberos_pa, &result_kh, &result_pk); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "pam_passkey_concatenate keys failed - [%d]: [%s]\n", ret, sss_strerror(ret)); goto done; } if (state->kerberos_pa) { state->extra_args[arg_c++] = pk_data->crypto_challenge; state->extra_args[arg_c++] = "--cryptographic-challenge"; state->extra_args[arg_c++] = result_kh; state->extra_args[arg_c++] = "--key-handle"; state->extra_args[arg_c++] = pk_data->domain; state->extra_args[arg_c++] = "--domain"; state->extra_args[arg_c++] = "--get-assert"; } else { state->extra_args[arg_c++] = result_pk; state->extra_args[arg_c++] = "--public-key"; state->extra_args[arg_c++] = result_kh; state->extra_args[arg_c++] = "--key-handle"; state->extra_args[arg_c++] = pk_data->domain; state->extra_args[arg_c++] = "--domain"; state->extra_args[arg_c++] = state->pd->user; state->extra_args[arg_c++] = "--username"; state->extra_args[arg_c++] = "--authenticate"; } ret = passkey_child_exec(req); done: if (ret == EOK) { tevent_req_done(req); tevent_req_post(req, ev); } else if (ret != EAGAIN) { tevent_req_error(req, ret); tevent_req_post(req, ev); } return req; } static void passkey_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 pam_passkey_auth_send_state *state = tevent_req_data(req, struct pam_passkey_auth_send_state); DEBUG(SSSDBG_CRIT_FAILURE, "Timeout reached for passkey child, " "consider increasing passkey_child_timeout\n"); child_handler_destroy(state->child_ctx); state->child_ctx = NULL; state->child_status = ETIMEDOUT; tevent_req_error(req, ERR_PASSKEY_CHILD_TIMEOUT); } static errno_t passkey_child_exec(struct tevent_req *req) { struct pam_passkey_auth_send_state *state; struct tevent_req *subreq; int pipefd_from_child[2] = PIPE_INIT; int pipefd_to_child[2] = PIPE_INIT; pid_t child_pid; uint8_t *write_buf = NULL; size_t write_buf_len = 0; struct timeval tv; int ret; state = tevent_req_data(req, struct pam_passkey_auth_send_state); ret = pipe(pipefd_from_child); if (ret == -1) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "pipe failed [%d][%s].\n", ret, strerror(ret)); goto done; } ret = pipe(pipefd_to_child); if (ret == -1) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "pipe failed [%d][%s].\n", ret, strerror(ret)); goto done; } child_pid = fork(); if (child_pid == 0) { /* child */ exec_child_ex(state, pipefd_to_child, pipefd_from_child, PASSKEY_CHILD_PATH, state->logfile, state->extra_args, false, STDIN_FILENO, STDOUT_FILENO); /* We should never get here */ ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "BUG: Could not exec passkey child\n"); goto done; } else if (child_pid > 0) { /* parent */ state->io->read_from_child_fd = pipefd_from_child[0]; PIPE_FD_CLOSE(pipefd_from_child[1]); sss_fd_nonblocking(state->io->read_from_child_fd); state->io->write_to_child_fd = pipefd_to_child[1]; PIPE_FD_CLOSE(pipefd_to_child[0]); sss_fd_nonblocking(state->io->write_to_child_fd); /* Set up SIGCHLD handler */ if (state->kerberos_pa) { ret = child_handler_setup(state->ev, child_pid, NULL, req, &state->child_ctx); } else { ret = child_handler_setup(state->ev, child_pid, pam_passkey_auth_done, req, &state->child_ctx); } if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Could not set up child handlers [%d]: %s\n", ret, sss_strerror(ret)); ret = ERR_PASSKEY_CHILD; goto done; } /* Set up timeout handler */ tv = tevent_timeval_current_ofs(state->timeout, 0); state->timeout_handler = tevent_add_timer(state->ev, req, tv, passkey_child_timeout, req); if (state->timeout_handler == NULL) { ret = ERR_PASSKEY_CHILD; goto done; } /* PIN is needed */ if (sss_authtok_get_type(state->pd->authtok) != SSS_AUTHTOK_TYPE_EMPTY) { ret = get_passkey_child_write_buffer(state, state->pd, &write_buf, &write_buf_len); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "get_passkey_child_write_buffer failed [%d]: %s.\n", ret, sss_strerror(ret)); goto done; } } if (write_buf_len != 0) { subreq = write_pipe_send(state, state->ev, write_buf, write_buf_len, state->io->write_to_child_fd); if (subreq == NULL) { DEBUG(SSSDBG_OP_FAILURE, "write_pipe_send failed.\n"); ret = ERR_PASSKEY_CHILD; goto done; } tevent_req_set_callback(subreq, passkey_child_write_done, req); } /* Now either wait for the timeout to fire or the child to finish */ } else { /* error */ ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "fork failed [%d][%s].\n", ret, sss_strerror(ret)); goto done; } return EAGAIN; done: if (ret != EOK) { PIPE_CLOSE(pipefd_from_child); PIPE_CLOSE(pipefd_to_child); } return ret; } errno_t pam_passkey_auth_recv(struct tevent_req *req, int *child_status) { struct pam_passkey_auth_send_state *state = tevent_req_data(req, struct pam_passkey_auth_send_state); *child_status = state->child_status; TEVENT_REQ_RETURN_ON_ERROR(req); return EOK; } errno_t decode_pam_passkey_msg(TALLOC_CTX *mem_ctx, uint8_t *buf, size_t len, struct pk_child_user_data **_data) { size_t p = 0; size_t pctr = 0; errno_t ret; size_t offset; struct pk_child_user_data *data = NULL; TALLOC_CTX *tmp_ctx; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } data = talloc_zero(tmp_ctx, struct pk_child_user_data); if (data == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to talloc passkey data.\n"); ret = ENOMEM; goto done; } data->user_verification = talloc_strdup(data, (char *) &buf[p]); if (data->user_verification == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to strdup passkey prompt.\n"); ret = ENOMEM; goto done; } offset = strlen(data->user_verification) + 1; if (offset >= len) { DEBUG(SSSDBG_OP_FAILURE, "passkey prompt offset failure.\n"); ret = EIO; goto done; } data->crypto_challenge = talloc_strdup(data, (char *) &buf[p + offset]); if (data->crypto_challenge == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to strdup passkey challenge.\n"); ret = ENOMEM; goto done; } offset += strlen(data->crypto_challenge) + 1; if (offset >= len) { DEBUG(SSSDBG_OP_FAILURE, "passkey challenge offset failure.\n"); ret = EIO; goto done; } data->domain = talloc_strdup(data, (char *) &buf[p] + offset); if (data->domain == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to strdup passkey domain.\n"); ret = ENOMEM; goto done; } offset += strlen(data->domain) + 1; if (offset >= len) { DEBUG(SSSDBG_OP_FAILURE, "passkey domain offset failure.\n"); ret = EIO; goto done; } SAFEALIGN_COPY_UINT32(&data->num_credentials, &buf[p + offset], &pctr); size_t list_sz = (size_t) data->num_credentials; offset += sizeof(uint32_t); data->key_handles = talloc_zero_array(data, const char *, list_sz); for (int i = 0; i < list_sz; i++) { data->key_handles[i] = talloc_strdup(data->key_handles, (char *) &buf[p + offset]); if (data->key_handles[i] == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to strdup passkey list.\n"); ret = ENOMEM; goto done; } offset += strlen(data->key_handles[i]) + 1; } *_data = talloc_steal(mem_ctx, data); ret = EOK; done: talloc_free(tmp_ctx); return ret; } errno_t save_passkey_data(TALLOC_CTX *mem_ctx, struct pam_ctx *pctx, struct pk_child_user_data *data, struct pam_auth_req *preq) { char *pk_key; errno_t ret; TALLOC_CTX *tmp_ctx; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } /* Passkey data (pk_table_data) is stolen onto client ctx, it will * be freed when the client closes, and the sss_ptr_hash interface * takes care of automatically removing it from the hash table then */ pctx->pk_table_data = talloc_zero(tmp_ctx, struct pam_passkey_table_data); if (pctx->pk_table_data == NULL) { return ENOMEM; } if (pctx->pk_table_data->table == NULL) { pctx->pk_table_data->table = sss_ptr_hash_create(pctx->pk_table_data, NULL, NULL); if (pctx->pk_table_data->table == NULL) { ret = ENOMEM; goto done; } } pk_key = talloc_asprintf(tmp_ctx, "%s", data->crypto_challenge); if (pk_key == NULL) { ret = ENOMEM; goto done; } pctx->pk_table_data->key = talloc_strdup(pctx->pk_table_data, pk_key); if (pctx->pk_table_data->key == NULL) { ret = ENOMEM; goto done; } ret = sss_ptr_hash_add(pctx->pk_table_data->table, pk_key, data, struct pk_child_user_data); if (ret == EEXIST) { DEBUG(SSSDBG_TRACE_FUNC, "pk_table key [%s] already exists\n", pk_key); goto done; } else if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Unable to add pk data to hash table " "[%d]: %s\n", ret, sss_strerror(ret)); goto done; } talloc_steal(mem_ctx, pctx->pk_table_data); pctx->pk_table_data->data = talloc_steal(mem_ctx, data); ret = EOK; done: talloc_free(tmp_ctx); return ret; } errno_t pam_eval_passkey_response(struct pam_ctx *pctx, struct pam_data *pd, struct pam_auth_req *preq, bool *_pk_preauth_done) { struct response_data *pk_resp; struct pk_child_user_data *pk_data; errno_t ret; TALLOC_CTX *tmp_ctx; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } pk_resp = pd->resp_list; while (pk_resp != NULL) { switch (pk_resp->type) { case SSS_PAM_PASSKEY_KRB_INFO: if (!pctx->passkey_auth) { /* Passkey auth is disabled. To avoid passkey prompts appearing, * don't send SSS_PAM_PASSKEY_KRB_INFO to the client and * add a dummy response to fallback to normal auth */ pk_resp->do_not_send_to_client = true; ret = pam_add_response(pd, SSS_OTP, 0, NULL); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "pam_add_response failed.\n"); goto done; } break; } ret = decode_pam_passkey_msg(tmp_ctx, pk_resp->data, pk_resp->len, &pk_data); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to decode passkey msg\n"); ret = EIO; goto done; } ret = save_passkey_data(preq->cctx, pctx, pk_data, preq); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to save passkey msg\n"); ret = EIO; goto done; } break; /* Passkey non-kerberos preauth has already run */ case SSS_PAM_PASSKEY_INFO: *_pk_preauth_done = true; default: break; } pk_resp = pk_resp->next; } ret = EOK; done: talloc_free(tmp_ctx); return ret; } static void pam_passkey_auth_done(int child_status, struct tevent_signal *sige, void *pvt) { struct tevent_req *req = talloc_get_type(pvt, struct tevent_req); struct pam_passkey_auth_send_state *state = tevent_req_data(req, struct pam_passkey_auth_send_state); state->child_status = WEXITSTATUS(child_status); if (WIFEXITED(child_status)) { if (WEXITSTATUS(child_status) != 0) { DEBUG(SSSDBG_OP_FAILURE, PASSKEY_CHILD_PATH " failed with status [%d]. Check passkey_child" " logs for more information.\n", WEXITSTATUS(child_status)); tevent_req_error(req, ERR_PASSKEY_CHILD); return; } } else if (WIFSIGNALED(child_status)) { DEBUG(SSSDBG_OP_FAILURE, PASSKEY_CHILD_PATH " was terminated by signal [%d]. Check passkey_child" " logs for more information.\n", WTERMSIG(child_status)); tevent_req_error(req, ECHILD); return; } DEBUG(SSSDBG_TRACE_FUNC, "passkey data is valid. Mark done\n"); tevent_req_done(req); return; } bool may_do_passkey_auth(struct pam_ctx *pctx, struct pam_data *pd) { #ifndef BUILD_PASSKEY DEBUG(SSSDBG_TRACE_FUNC, "Passkey auth not possible, SSSD built without passkey support!\n"); return false; #else if (!pctx->passkey_auth) { return false; } if (pd->cmd != SSS_PAM_PREAUTH && pd->cmd != SSS_PAM_AUTHENTICATE) { return false; } if (pd->service == NULL || *pd->service == '\0') { return false; } return true; #endif /* BUILD_PASSKEY */ }