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/krb5_plugin/passkey | |
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/krb5_plugin/passkey')
-rw-r--r-- | src/krb5_plugin/passkey/passkey.h | 110 | ||||
-rw-r--r-- | src/krb5_plugin/passkey/passkey_clpreauth.c | 395 | ||||
-rw-r--r-- | src/krb5_plugin/passkey/passkey_kdcpreauth.c | 438 | ||||
-rw-r--r-- | src/krb5_plugin/passkey/passkey_utils.c | 648 | ||||
-rw-r--r-- | src/krb5_plugin/passkey/sssd_enable_passkey | 14 |
5 files changed, 1605 insertions, 0 deletions
diff --git a/src/krb5_plugin/passkey/passkey.h b/src/krb5_plugin/passkey/passkey.h new file mode 100644 index 0000000..0f36d5d --- /dev/null +++ b/src/krb5_plugin/passkey/passkey.h @@ -0,0 +1,110 @@ +/* + Authors: + Pavel Březina <pbrezina@redhat.com> + + Copyright (C) 2023 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 _PASSKEY_H_ +#define _PASSKEY_H_ + +#include <stdlib.h> +#include <krb5/preauth_plugin.h> + +#ifndef discard_const +#define discard_const(ptr) ((void *)((uintptr_t)(ptr))) +#endif + +#define SSSD_PASSKEY_PLUGIN "passkey" +#define SSSD_PASSKEY_CONFIG "passkey" +#define SSSD_PASSKEY_PADATA 153 // PA-REDHAT-PASSKEY +#define SSSD_PASSKEY_QUESTION "passkey" +#define SSSD_PASSKEY_PREFIX "passkey " +#define SSSD_PASSKEY_REPLY_STATE "ipa_otpd state" +#define SSSD_PASSKEY_PROMPT "Insert your passkey device, then press ENTER" +#define SSSD_PASSKEY_PIN_PROMPT "Enter PIN" +#define SSSD_PASSKEY_CHILD SSSD_LIBEXEC_PATH"/passkey_child" + +struct sss_passkey_config { + char **indicators; +}; + +void +sss_passkey_config_free(struct sss_passkey_config *passkey); + +krb5_error_code +sss_passkey_config_init(const char *config, + struct sss_passkey_config **_passkey); + +enum sss_passkey_phase { + SSS_PASSKEY_PHASE_INIT, + SSS_PASSKEY_PHASE_CHALLENGE, + SSS_PASSKEY_PHASE_REPLY +}; + +struct sss_passkey_challenge { + char *domain; + char **credential_id_list; + int user_verification; + char *cryptographic_challenge; +}; + +struct sss_passkey_reply { + char *credential_id; + char *cryptographic_challenge; + char *authenticator_data; + char *assertion_signature; + char *user_id; +}; + +struct sss_passkey_message { + enum sss_passkey_phase phase; + char *state; + union { + struct sss_passkey_challenge *challenge; + struct sss_passkey_reply *reply; + void *ptr; + } data; +}; + +void +sss_passkey_message_free(struct sss_passkey_message *message); + +struct sss_passkey_message * +sss_passkey_message_from_reply_json(enum sss_passkey_phase phase, + const char *state, + const char *json_str); + +char * +sss_passkey_message_encode(const struct sss_passkey_message *data); + +struct sss_passkey_message * +sss_passkey_message_decode(const char *str); + +krb5_pa_data * +sss_passkey_message_encode_padata(const struct sss_passkey_message *data); + +struct sss_passkey_message * +sss_passkey_message_decode_padata(krb5_pa_data *padata); + +krb5_pa_data ** +sss_passkey_message_encode_padata_array(const struct sss_passkey_message *data); + +krb5_error_code +sss_passkey_concat_credentials(char **creds, + char **_creds_str); + +#endif /* _PASSKEY_H_ */ diff --git a/src/krb5_plugin/passkey/passkey_clpreauth.c b/src/krb5_plugin/passkey/passkey_clpreauth.c new file mode 100644 index 0000000..d2dfe6f --- /dev/null +++ b/src/krb5_plugin/passkey/passkey_clpreauth.c @@ -0,0 +1,395 @@ +/* + Authors: + Pavel Březina <pbrezina@redhat.com> + + Copyright (C) 2023 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 "config.h" + +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <sys/wait.h> +#include <unistd.h> +#include <krb5/clpreauth_plugin.h> + +#include "krb5_plugin/common/radius_clpreauth.h" +#include "passkey.h" + +#include "util/child_common.h" + +static krb5_error_code +sss_passkeycl_prompt(krb5_context context, + krb5_prompter_fct prompter, + void *prompter_data, + struct sss_passkey_message *message, + const char *prompt_txt, + char *prompt_answer, + int answer_len, + krb5_data *_reply) +{ + krb5_error_code ret; + krb5_prompt prompt; + char *prompt_str; + int aret; + + _reply->magic = 0; + _reply->length = answer_len; + _reply->data = prompt_answer; + + aret = asprintf(&prompt_str, "%s", prompt_txt); + if (aret < 0) { + return ENOMEM; + } + + prompt.reply = _reply; + prompt.prompt = prompt_str; + prompt.hidden = 1; + + ret = (*prompter)(context, prompter_data, NULL, NULL, 1, &prompt); + free(prompt_str); + + return ret; +} + +static krb5_error_code +sss_passkeycl_prep_questions(krb5_context context, + krb5_clpreauth_moddata moddata, + krb5_clpreauth_modreq modreq, + krb5_get_init_creds_opt *opt, + krb5_clpreauth_callbacks cb, + krb5_clpreauth_rock rock, + krb5_kdc_req *request, + krb5_data *encoded_request_body, + krb5_data *encoded_previous_request, + krb5_pa_data *pa_data) +{ + struct sss_passkey_message *message; + char *question = NULL; + char **realms = NULL; + krb5_error_code ret; + size_t r = 0; + + message = sss_passkey_message_decode_padata(pa_data); + if (message == NULL) { + ret = ENOMEM; + goto done; + } + + if (message->data.challenge->domain == NULL) { + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto done; + } + + /* Find a realm that matches the domain from the challenge */ + ret = krb5_get_host_realm(context, + message->data.challenge->domain, + &realms); + if (ret || ((realms == NULL) || (realms[0] == NULL))) { + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto done; + } + + /* Do explicit check for the challenge domain in case + * we've got back a referral (empty) realm */ + if (strlen(realms[0]) == strlen(KRB5_REFERRAL_REALM)) { + ret = strncasecmp(message->data.challenge->domain, + request->server->realm.data, + request->server->realm.length); + if (ret != 0) { + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto done; + } + + } else { + for(r = 0; realms[r] != NULL; r++) { + ret = strncasecmp(realms[r], + request->server->realm.data, + request->server->realm.length); + if (ret == 0) { + break; + } + } + + /* doesn't know the domain, reject the challenge */ + if (realms[r] == NULL) { + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto done; + } + } + + question = sss_passkey_message_encode(message); + if (question == NULL) { + ret = ENOMEM; + goto done; + } + + ret = cb->ask_responder_question(context, rock, SSSD_PASSKEY_QUESTION, + question); + +done: + if (realms) { + krb5_free_host_realm(context, realms); + } + sss_passkey_message_free(message); + free(question); + + return ret; +} + +static krb5_error_code +sss_passkeycl_exec_child(struct sss_passkey_challenge *data, + char *pin, + uint8_t **_reply) +{ + int pipe_to_child[2]; + int pipe_to_parent[2]; + pid_t cpid; + char *args[10] = {NULL}; + int arg_c = 0; + int size; + uint8_t *buf; + int ret = 0; + char *result_creds; + + buf = calloc(1, CHILD_MSG_CHUNK); + if (buf == NULL) { + ret = ENOMEM; + return ret; + } + + ret = sss_passkey_concat_credentials(data->credential_id_list, + &result_creds); + if (ret != 0) { + ret = ENOMEM; + goto done; + } + + args[arg_c++] = discard_const(SSSD_PASSKEY_CHILD); + args[arg_c++] = discard_const("--get-assert"); + args[arg_c++] = discard_const("--domain"); + args[arg_c++] = data->domain; + args[arg_c++] = discard_const("--key-handle"); + args[arg_c++] = discard_const(result_creds); + args[arg_c++] = discard_const("--cryptographic-challenge"); + args[arg_c++] = data->cryptographic_challenge; + args[arg_c++] = NULL; + + ret = pipe(pipe_to_child); + if (ret == -1) { + ret = ENOMEM; + goto done; + } + + ret = pipe(pipe_to_parent); + if (ret == -1) { + ret = ENOMEM; + goto done; + } + + cpid = fork(); + /* Child */ + if (cpid == 0) { + close(pipe_to_child[1]); + dup2(pipe_to_child[0], STDIN_FILENO); + + close(pipe_to_parent[0]); + dup2(pipe_to_parent[1], STDOUT_FILENO); + + execv(SSSD_PASSKEY_CHILD, args); + exit(EXIT_FAILURE); + /* Parent - write PIN to child and read output + * back from child */ + } else { + close(pipe_to_child[0]); + close(pipe_to_parent[1]); + + write(pipe_to_child[1], pin, strlen(pin)); + close(pipe_to_child[1]); + + size = read(pipe_to_parent[0], buf, CHILD_MSG_CHUNK); + if (size == -1) { + ret = ENOMEM; + goto done; + } + + close(pipe_to_parent[0]); + wait(NULL); + } + + *_reply = buf; + +done: + if (ret != 0) { + free(buf); + } + + free(result_creds); + return ret; +} + +static krb5_error_code +sss_passkeycl_process(krb5_context context, + krb5_clpreauth_moddata moddata, + krb5_clpreauth_modreq modreq, + krb5_get_init_creds_opt *opt, + krb5_clpreauth_callbacks cb, + krb5_clpreauth_rock rock, + krb5_kdc_req *request, + krb5_data *encoded_request_body, + krb5_data *encoded_previous_request, + krb5_pa_data *pa_data, + krb5_prompter_fct prompter, + void *prompter_data, + krb5_pa_data ***_pa_data_out) +{ + krb5_keyblock *as_key; + krb5_pa_data **padata; + krb5_error_code ret; + krb5_data user_reply; + struct sss_passkey_message *input_message = NULL; + struct sss_passkey_message *reply_message = NULL; + struct sss_passkey_message *reply_msg = NULL; + enum sss_passkey_phase phase; + const char *state; + char prompt_answer[255] = {0}; + int answer_len; + char *prompt_reply = NULL; + uint8_t *reply = NULL; + const char *answer; + + input_message = sss_passkey_message_decode_padata(pa_data); + if (input_message == NULL) { + ret = ENOMEM; + goto done; + } + + /* Get FAST armor key. */ + as_key = cb->fast_armor(context, rock); + if (as_key == NULL) { + ret = ENOENT; + goto done; + } + + answer = cb->get_responder_answer(context, rock, SSSD_PASSKEY_QUESTION); + /* Call prompter if we have no answer to present a prompt. */ + if (answer == NULL) { + /* Interactive prompt */ + answer_len = sizeof(prompt_answer) / sizeof(char); + + ret = sss_passkeycl_prompt(context, prompter, prompter_data, + input_message, SSSD_PASSKEY_PROMPT, + prompt_answer, answer_len, + &user_reply); + if (ret != 0) { + goto done; + } + + /* Prompt for PIN */ + if (input_message->data.challenge->user_verification == 1) { + ret = sss_passkeycl_prompt(context, prompter, prompter_data, + input_message, SSSD_PASSKEY_PIN_PROMPT, + prompt_answer, answer_len, + &user_reply); + if (ret != 0) { + goto done; + } + } + + ret = sss_passkeycl_exec_child(input_message->data.challenge, prompt_answer, &reply); + if (ret != 0) { + goto done; + } + + phase = SSS_PASSKEY_PHASE_REPLY; + state = SSSD_PASSKEY_REPLY_STATE; + reply_msg = sss_passkey_message_from_reply_json(phase, state, (char *)reply); + if (reply_msg == NULL) { + ret = ENOMEM; + goto done; + } + + prompt_reply = sss_passkey_message_encode(reply_msg); + if (prompt_reply == NULL) { + ret = ENOMEM; + goto done; + } + } + + /* Use FAST armor key as response key. */ + ret = cb->set_as_key(context, rock, as_key); + if (ret != 0) { + goto done; + } + + /* Encode the answer into the pa_data output. */ + if (prompt_reply != NULL) { + reply_message = sss_passkey_message_decode(prompt_reply); + } else { + reply_message = sss_passkey_message_decode(answer); + } + if (reply_message == NULL) { + ret = ENOMEM; + goto done; + } + + padata = sss_passkey_message_encode_padata_array(reply_message); + if (padata == NULL) { + ret = ENOMEM; + goto done; + } + + cb->disable_fallback(context, rock); + *_pa_data_out = padata; + + ret = 0; + +done: + sss_passkey_message_free(reply_message); + sss_passkey_message_free(reply_msg); + sss_passkey_message_free(input_message); + free(reply); + free(prompt_reply); + + return ret; +} + +krb5_error_code +clpreauth_passkey_initvt(krb5_context context, + int maj_ver, + int min_ver, + krb5_plugin_vtable vtable) +{ + static krb5_preauthtype pa_type_list[] = { SSSD_PASSKEY_PADATA, 0 }; + krb5_clpreauth_vtable vt; + + if (maj_ver != 1) { + return KRB5_PLUGIN_VER_NOTSUPP; + } + + vt = (krb5_clpreauth_vtable)vtable; + vt->name = discard_const(SSSD_PASSKEY_PLUGIN); + vt->pa_type_list = pa_type_list; + vt->request_init = sss_radiuscl_init; + vt->prep_questions = sss_passkeycl_prep_questions; + vt->process = sss_passkeycl_process; + vt->request_fini = sss_radiuscl_fini; + vt->gic_opts = NULL; + + return 0; +} diff --git a/src/krb5_plugin/passkey/passkey_kdcpreauth.c b/src/krb5_plugin/passkey/passkey_kdcpreauth.c new file mode 100644 index 0000000..903cb42 --- /dev/null +++ b/src/krb5_plugin/passkey/passkey_kdcpreauth.c @@ -0,0 +1,438 @@ +/* + Authors: + Pavel Březina <pbrezina@redhat.com> + + Copyright (C) 2023 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 "config.h" + +#include <errno.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include <krad.h> +#include <krb5/kdcpreauth_plugin.h> + +#include "shared/safealign.h" +#include "krb5_plugin/common/radius_kdcpreauth.h" +#include "krb5_plugin/common/utils.h" +#include "krb5_plugin/passkey/passkey.h" +#include "util/util.h" + +struct sss_passkeykdc_config { + struct sss_radiuskdc_config *radius; + struct sss_passkey_config *passkey; +}; + +static void +sss_passkeykdc_config_free(struct sss_passkeykdc_config *config) +{ + if (config == NULL) { + return; + } + + sss_radiuskdc_config_free(config->radius); + sss_passkey_config_free(config->passkey); + free(config); +} + +static krb5_error_code +sss_passkeykdc_config_init(struct sss_radiuskdc_state *state, + krb5_context kctx, + krb5_const_principal princ, + const char *configstr, + struct sss_passkeykdc_config **_config) +{ + struct sss_passkeykdc_config *config; + krb5_error_code ret; + + if (state == NULL) { + return EINVAL; + } + + config = malloc(sizeof(struct sss_passkeykdc_config)); + if (config == NULL) { + ret = ENOMEM; + goto done; + } + memset(config, 0, sizeof(struct sss_passkeykdc_config)); + + ret = sss_radiuskdc_config_init(state, kctx, princ, configstr, &config->radius); + if (ret != 0) { + goto done; + } + + ret = sss_passkey_config_init(configstr, &config->passkey); + if (ret != 0) { + goto done; + } + + + *_config = config; + ret = 0; + +done: + if (ret != 0) { + sss_passkeykdc_config_free(config); + } + + return ret; +} + +static void +sss_passkeykdc_challenge_done(krb5_error_code rret, + const krad_packet *rreq, + const krad_packet *rres, + void *data) +{ + struct sss_passkey_message *message = NULL; + struct sss_radiuskdc_challenge *state; + krb5_pa_data *padata = NULL; + krb5_data cookie = {0}; + char *reply = NULL; + krb5_error_code ret; + + state = (struct sss_radiuskdc_challenge *)data; + + if (rret != 0) { + ret = rret; + goto done; + } + + if (krad_packet_get_code(rres) != krad_code_name2num("Access-Challenge")) { + ret = ENOENT; + goto done; + } + + reply = sss_radiuskdc_get_attr_as_string(rres, "Proxy-State"); + if (reply == NULL) { + ret = EINVAL; + goto done; + } + + message = sss_passkey_message_decode(reply); + if (message == NULL) { + ret = ENOMEM; + goto done; + } + + if (message->phase != SSS_PASSKEY_PHASE_CHALLENGE) { + ret = EINVAL; + goto done; + } + + /* Remember the whole passkey challenge message in cookie so we can later + * verify that the client response is really associated with this request. + */ + cookie.data = reply; + cookie.length = strlen(reply) + 1; + ret = sss_radiuskdc_set_cookie(state->kctx, state->cb, state->rock, + SSSD_PASSKEY_PADATA, &cookie); + if (ret != 0) { + goto done; + } + + padata = sss_passkey_message_encode_padata(message); + if (padata == NULL) { + ret = ENOMEM; + goto done; + } + + ret = 0; + +done: + state->respond(state->arg, ret, padata); + sss_radiuskdc_challenge_free(state); + sss_passkey_message_free(message); + free(reply); + + /* padata should not be freed */ + + return; +} + +/* Send a password-less Access-Request and expect Access-Challenge response. */ +static krb5_error_code +sss_passkeykdc_challenge_send(krb5_context kctx, + krb5_kdcpreauth_callbacks cb, + krb5_kdcpreauth_rock rock, + krb5_kdcpreauth_edata_respond_fn respond, + void *arg, + struct sss_radiuskdc_config *config) +{ + struct sss_passkey_message message; + struct sss_radiuskdc_challenge *state; + char *encoded_message = NULL; + krb5_error_code ret; + + state = sss_radiuskdc_challenge_init(kctx, cb, rock, respond, arg, config); + if (state == NULL) { + ret = ENOMEM; + goto done; + } + + message.phase = SSS_PASSKEY_PHASE_INIT; + message.state = NULL; + message.data.challenge = NULL; + + encoded_message = sss_passkey_message_encode(&message); + if (encoded_message == NULL) { + ret = ENOMEM; + goto done; + } + + ret = sss_radiuskdc_set_attr_as_string(state->client->attrs, + "Proxy-State", + encoded_message); + if (ret != 0) { + goto done; + } + + ret = krad_client_send(state->client->client, + krad_code_name2num("Access-Request"), + state->client->attrs, config->server, + config->secret, config->timeout, config->retries, + sss_passkeykdc_challenge_done, state); + +done: + free(encoded_message); + + if (ret != 0) { + sss_radiuskdc_challenge_free(state); + } + + return ret; +} + +/* Send Access-Request with password and state set to indicate that the user has + * finished authentication against passkey provider. We expect Access-Accept. */ +static krb5_error_code +sss_passkeykdc_verify_send(krb5_context kctx, + krb5_kdcpreauth_rock rock, + krb5_kdcpreauth_callbacks cb, + krb5_enc_tkt_part *enc_tkt_reply, + krb5_kdcpreauth_verify_respond_fn respond, + void *arg, + const struct sss_passkey_message *message, + char **indicators, + struct sss_radiuskdc_config *config) +{ + struct sss_radiuskdc_verify *state; + char *encoded_message = NULL; + krb5_error_code ret; + + state = sss_radiuskdc_verify_init(kctx, rock, cb, enc_tkt_reply, respond, + arg, indicators, config); + if (state == NULL) { + return ENOMEM; + } + + encoded_message = sss_passkey_message_encode(message); + if (encoded_message == NULL) { + ret = ENOMEM; + goto done; + } + + ret = sss_radiuskdc_set_attr_as_string(state->client->attrs, + "Proxy-State", + encoded_message); + if (ret != 0) { + goto done; + } + + ret = krad_client_send(state->client->client, + krad_code_name2num("Access-Request"), + state->client->attrs, config->server, + config->secret, config->timeout, config->retries, + sss_radiuskdc_verify_done, state); + +done: + free(encoded_message); + + if (ret != 0) { + sss_radiuskdc_verify_free(state); + } + + return ret; +} + +static krb5_error_code +sss_passkeykdc_init(krb5_context kctx, + krb5_kdcpreauth_moddata *_moddata, + const char **_realmnames) +{ + return sss_radiuskdc_init(SSSD_PASSKEY_PLUGIN, kctx, _moddata, _realmnames); +} + +static void +sss_passkeykdc_edata(krb5_context kctx, + krb5_kdc_req *request, + krb5_kdcpreauth_callbacks cb, + krb5_kdcpreauth_rock rock, + krb5_kdcpreauth_moddata moddata, + krb5_preauthtype pa_type, + krb5_kdcpreauth_edata_respond_fn respond, + void *arg) +{ + struct sss_passkeykdc_config *config = NULL; + struct sss_radiuskdc_state *state; + krb5_keyblock *armor_key; + char *configstr = NULL; + krb5_error_code ret; + + state = (struct sss_radiuskdc_state *)moddata; + + ret = sss_radiuskdc_enabled(SSSD_PASSKEY_CONFIG, kctx, cb, rock, &configstr); + if (ret != 0) { + goto done; + } + + armor_key = cb->fast_armor(kctx, rock); + if (armor_key == NULL) { + ret = ENOENT; + goto done; + } + + ret = sss_passkeykdc_config_init(state, kctx, cb->client_name(kctx, rock), + configstr, &config); + if (ret != 0) { + goto done; + } + + ret = sss_passkeykdc_challenge_send(kctx, cb, rock, respond, arg, + config->radius); + +done: + if (ret != 0) { + respond(arg, ret, NULL); + } + + cb->free_string(kctx, rock, configstr); + sss_passkeykdc_config_free(config); +} + +static void +sss_passkeykdc_verify(krb5_context kctx, + krb5_data *req_pkt, + krb5_kdc_req *request, + krb5_enc_tkt_part *enc_tkt_reply, + krb5_pa_data *pa, + krb5_kdcpreauth_callbacks cb, + krb5_kdcpreauth_rock rock, + krb5_kdcpreauth_moddata moddata, + krb5_kdcpreauth_verify_respond_fn respond, + void *arg) +{ + struct sss_radiuskdc_state *state; + struct sss_passkeykdc_config *config = NULL; + struct sss_passkey_message *message = NULL; + struct sss_passkey_message *challenge = NULL; + char *configstr = NULL; + krb5_error_code ret; + krb5_data cookie; + + state = (struct sss_radiuskdc_state *)moddata; + + ret = sss_radiuskdc_enabled(SSSD_PASSKEY_CONFIG, kctx, cb, rock, &configstr); + if (ret != 0) { + goto done; + } + + ret = sss_passkeykdc_config_init(state, kctx, cb->client_name(kctx, rock), + configstr, &config); + if (ret != 0) { + goto done; + } + + ret = sss_radiuskdc_get_cookie(kctx, cb, rock, SSSD_PASSKEY_PADATA, + &cookie); + if (ret != 0) { + goto done; + } + + challenge = sss_passkey_message_decode(cookie.data); + if (challenge == NULL) { + ret = ENOMEM; + goto done; + } + + if (pa->pa_type != SSSD_PASSKEY_PADATA || pa->length == 0) { + ret = KRB5_PREAUTH_BAD_TYPE; + goto done; + } + + message = sss_passkey_message_decode_padata(pa); + if (message == NULL) { + ret = ENOMEM; + goto done; + } + + if (message->phase != SSS_PASSKEY_PHASE_REPLY + || strcmp(message->state, challenge->state) != 0 + || strcmp(message->data.reply->cryptographic_challenge, + challenge->data.challenge->cryptographic_challenge) != 0) { + ret = EINVAL; + goto done; + } + + ret = sss_passkeykdc_verify_send(kctx, rock, cb, enc_tkt_reply, respond, + arg, message, config->passkey->indicators, config->radius); + if (ret != 0) { + goto done; + } + + ret = 0; + +done: + if (ret != 0) { + respond(arg, ret, NULL, NULL, NULL); + } + + cb->free_string(kctx, rock, configstr); + sss_passkeykdc_config_free(config); + sss_passkey_message_free(message); + sss_passkey_message_free(challenge); +} + +krb5_error_code +kdcpreauth_passkey_initvt(krb5_context kctx, + int maj_ver, + int min_ver, + krb5_plugin_vtable vtable) +{ + static krb5_preauthtype pa_type_list[] = { SSSD_PASSKEY_PADATA, 0 }; + krb5_kdcpreauth_vtable vt; + + if (maj_ver != 1) { + return KRB5_PLUGIN_VER_NOTSUPP; + } + + vt = (krb5_kdcpreauth_vtable)vtable; + vt->name = discard_const(SSSD_PASSKEY_PLUGIN); + vt->pa_type_list = pa_type_list; + vt->init = sss_passkeykdc_init; + vt->fini = sss_radiuskdc_fini; + vt->flags = sss_radiuskdc_flags; + vt->edata = sss_passkeykdc_edata; + vt->verify = sss_passkeykdc_verify; + vt->return_padata = sss_radiuskdc_return_padata; + + com_err(SSSD_PASSKEY_PLUGIN, 0, "SSSD passkey plugin loaded"); + + return 0; +} diff --git a/src/krb5_plugin/passkey/passkey_utils.c b/src/krb5_plugin/passkey/passkey_utils.c new file mode 100644 index 0000000..e73da97 --- /dev/null +++ b/src/krb5_plugin/passkey/passkey_utils.c @@ -0,0 +1,648 @@ +/* + Authors: + Pavel Březina <pbrezina@redhat.com> + + Copyright (C) 2023 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 "config.h" + +#include <errno.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <jansson.h> +#include <arpa/inet.h> +#include <krb5/preauth_plugin.h> + +#include "krb5_plugin/common/utils.h" +#include "krb5_plugin/passkey/passkey.h" + +void +sss_passkey_config_free(struct sss_passkey_config *passkey) +{ + if (passkey == NULL) { + return; + } + + sss_string_array_free(passkey->indicators); + free(passkey); +} + +/** + * { + * "indicators": ["..."] (optional) + * } + */ +krb5_error_code +sss_passkey_config_init(const char *config, + struct sss_passkey_config **_passkey) +{ + struct sss_passkey_config *passkey; + json_t *jindicators = NULL; + json_error_t jret; + json_t *jroot; + krb5_error_code ret; + + passkey = malloc(sizeof(struct sss_passkey_config)); + if (passkey == NULL) { + return ENOMEM; + } + memset(passkey, 0, sizeof(struct sss_passkey_config)); + + jroot = json_loads(config, 0, &jret); + if (jroot == NULL) { + ret = EINVAL; + goto done; + } + + ret = json_unpack(jroot, "[{s?:o}]", "indicators", &jindicators); + if (ret != 0) { + ret = EINVAL; + goto done; + } + + /* Are indicators set? */ + if (jindicators != NULL) { + passkey->indicators = sss_json_array_to_strings(jindicators); + if (passkey->indicators == NULL) { + ret = EINVAL; + goto done; + } + } + + *_passkey = passkey; + + ret = 0; + +done: + if (ret != 0) { + sss_passkey_config_free(passkey); + } + + if (jroot != NULL) { + json_decref(jroot); + } + + return ret; +} + +void +sss_passkey_challenge_free(struct sss_passkey_challenge *data) +{ + if (data == NULL) { + return; + } + + free(data->domain); + free(data->cryptographic_challenge); + sss_string_array_free(data->credential_id_list); + + free(data); +} + +static struct sss_passkey_challenge * +sss_passkey_challenge_init(char *domain, + char **credential_id_list, + int user_verification, + char *cryptographic_challenge) +{ + struct sss_passkey_challenge *data; + krb5_error_code ret; + + /* These are required fields. */ + if (is_empty(domain) + || is_empty(cryptographic_challenge) + || credential_id_list == NULL || is_empty(credential_id_list[0])) { + return NULL; + } + + data = malloc(sizeof(struct sss_passkey_challenge)); + if (data == NULL) { + return NULL; + } + memset(data, 0, sizeof(struct sss_passkey_challenge)); + + data->user_verification = user_verification; + data->domain = strdup(domain); + data->cryptographic_challenge = strdup(cryptographic_challenge); + if (data->domain == NULL || data->cryptographic_challenge == NULL) { + ret = ENOMEM; + goto done; + } + + data->credential_id_list = sss_string_array_copy(credential_id_list); + if (data->credential_id_list == NULL) { + ret = ENOMEM; + goto done; + } + + ret = 0; + +done: + if (ret != 0) { + sss_passkey_challenge_free(data); + return NULL; + } + + return data; +} + +static struct sss_passkey_challenge * +sss_passkey_challenge_from_json_object(json_t *jobject) +{ + struct sss_passkey_challenge jdata = {0}; + struct sss_passkey_challenge *data = NULL; + json_t *jcredential_id_list = NULL; + char **credential_id_list = NULL; + int ret; + + if (jobject == NULL) { + return NULL; + } + + ret = json_unpack(jobject, "{s:s, s:o, s:i, s:s}", + "domain", &jdata.domain, + "credential_id_list", &jcredential_id_list, + "user_verification", &jdata.user_verification, + "cryptographic_challenge", &jdata.cryptographic_challenge); + if (ret != 0) { + return NULL; + } + + if (jcredential_id_list != NULL) { + credential_id_list = sss_json_array_to_strings(jcredential_id_list); + if (credential_id_list == NULL) { + return NULL; + } + } + + data = sss_passkey_challenge_init(jdata.domain, credential_id_list, + jdata.user_verification, + jdata.cryptographic_challenge); + + sss_string_array_free(credential_id_list); + return data; +} + +static json_t * +sss_passkey_challenge_to_json_object(const struct sss_passkey_challenge *data) +{ + json_t *jroot; + json_t *jcredential_id_list; + + if (data == NULL) { + return NULL; + } + + /* These are required fields. */ + if (data->domain == NULL || data->credential_id_list == NULL + || data->cryptographic_challenge == NULL) { + return NULL; + } + + jcredential_id_list = sss_strings_to_json_array(data->credential_id_list); + if (jcredential_id_list == NULL) { + return NULL; + } + + jroot = json_pack("{s:s, s:o, s:i, s:s}", "domain", data->domain, + "credential_id_list", jcredential_id_list, + "user_verification", data->user_verification, + "cryptographic_challenge", + data->cryptographic_challenge); + if (jroot == NULL) { + json_decref(jcredential_id_list); + return NULL; + } + + return jroot; +} + +void +sss_passkey_reply_free(struct sss_passkey_reply *data) +{ + if (data == NULL) { + return; + } + + free(data->credential_id); + free(data->cryptographic_challenge); + free(data->authenticator_data); + free(data->assertion_signature); + free(data->user_id); + free(data); +} + +static struct sss_passkey_reply * +sss_passkey_reply_init(char *credential_id, + char *cryptographic_challenge, + char *authenticator_data, + char *assertion_signature, + char *user_id) +{ + struct sss_passkey_reply *data; + krb5_error_code ret; + + /* These are required fields. */ + if (is_empty(credential_id) + || is_empty(cryptographic_challenge) + || is_empty(authenticator_data) + || is_empty(assertion_signature)) { + return NULL; + } + + data = malloc(sizeof(struct sss_passkey_reply)); + if (data == NULL) { + return NULL; + } + memset(data, 0, sizeof(struct sss_passkey_reply)); + + data->credential_id = strdup(credential_id); + data->cryptographic_challenge = strdup(cryptographic_challenge); + data->authenticator_data = strdup(authenticator_data); + data->assertion_signature = strdup(assertion_signature); + data->user_id = user_id == NULL ? NULL : strdup(user_id); + if (data->credential_id == NULL + || data->cryptographic_challenge == NULL + || data->authenticator_data == NULL + || data->assertion_signature == NULL + || (user_id != NULL && data->user_id == NULL)) { + ret = ENOMEM; + goto done; + } + + ret = 0; + +done: + if (ret != 0) { + sss_passkey_reply_free(data); + return NULL; + } + + return data; +} + +static struct sss_passkey_reply * +sss_passkey_reply_from_json_object(json_t *jobject) +{ + struct sss_passkey_reply jdata = {0}; + int ret; + + if (jobject == NULL) { + return NULL; + } + + ret = json_unpack(jobject, "{s:s, s:s, s:s, s:s, s?:s}", + "credential_id", &jdata.credential_id, + "cryptographic_challenge", &jdata.cryptographic_challenge, + "authenticator_data", &jdata.authenticator_data, + "assertion_signature", &jdata.assertion_signature, + "user_id", &jdata.user_id); + if (ret != 0) { + return NULL; + } + + return sss_passkey_reply_init(jdata.credential_id, + jdata.cryptographic_challenge, + jdata.authenticator_data, + jdata.assertion_signature, + jdata.user_id); +} + +static json_t * +sss_passkey_reply_to_json_object(const struct sss_passkey_reply *data) +{ + if (data == NULL) { + return NULL; + } + + /* These are required fields. */ + if (data->credential_id == NULL + || data->cryptographic_challenge == NULL + || data->authenticator_data == NULL + || data->assertion_signature == NULL) { + return NULL; + } + + return json_pack("{s:s, s:s, s:s, s:s, s:s*}", + "credential_id", data->credential_id, + "cryptographic_challenge", data->cryptographic_challenge, + "authenticator_data", data->authenticator_data, + "assertion_signature", data->assertion_signature, + "user_id", data->user_id); +} + +void +sss_passkey_message_free(struct sss_passkey_message *message) +{ + if (message == NULL) { + return; + } + + switch (message->phase) { + case SSS_PASSKEY_PHASE_INIT: + break; + case SSS_PASSKEY_PHASE_CHALLENGE: + sss_passkey_challenge_free(message->data.challenge); + break; + case SSS_PASSKEY_PHASE_REPLY: + sss_passkey_reply_free(message->data.reply); + break; + default: + /* nothing to do */ + break; + } + + free(message->state); + free(message); +} + +static struct sss_passkey_message * +sss_passkey_message_init(enum sss_passkey_phase phase, + const char *state, + void *data) +{ + struct sss_passkey_message *message; + + switch (phase) { + case SSS_PASSKEY_PHASE_INIT: + if (state != NULL || data != NULL) { + return NULL; + } + break; + case SSS_PASSKEY_PHASE_CHALLENGE: + case SSS_PASSKEY_PHASE_REPLY: + if (state == NULL || data == NULL) { + return NULL; + } + break; + default: + return NULL; + } + + message = malloc(sizeof(struct sss_passkey_message)); + if (message == NULL) { + return NULL; + } + memset(message, 0, sizeof(struct sss_passkey_message)); + + message->phase = phase; + message->state = state == NULL ? NULL : strdup(state); + message->data.ptr = data; + + if (state != NULL && message->state == NULL) { + sss_passkey_message_free(message); + return NULL; + } + + return message; +} + +static struct sss_passkey_message * +sss_passkey_message_from_json(const char *json_str) +{ + struct sss_passkey_message *message = NULL; + enum sss_passkey_phase phase = 0; + const char *state = NULL; + void *data = NULL; + json_error_t jret; + json_t *jdata = NULL; + json_t *jroot; + int ret; + + jroot = json_loads(json_str, 0, &jret); + if (jroot == NULL) { + return NULL; + } + + ret = json_unpack(jroot, "{s:i, s?:s, s?:o}", + "phase", &phase, + "state", &state, + "data", &jdata); + if (ret != 0) { + goto done; + } + + switch (phase) { + case SSS_PASSKEY_PHASE_INIT: + data = NULL; + break; + case SSS_PASSKEY_PHASE_CHALLENGE: + data = sss_passkey_challenge_from_json_object(jdata); + if (data == NULL) { + goto done; + } + break; + case SSS_PASSKEY_PHASE_REPLY: + data = sss_passkey_reply_from_json_object(jdata); + if (data == NULL) { + goto done; + } + break; + default: + goto done; + } + + message = sss_passkey_message_init(phase, state, data); + if (message == NULL && phase == SSS_PASSKEY_PHASE_CHALLENGE) { + sss_passkey_challenge_free(data); + } else if (message == NULL && phase == SSS_PASSKEY_PHASE_REPLY) { + sss_passkey_reply_free(data); + } + +done: + json_decref(jroot); + return message; +} + +static char * +sss_passkey_message_to_json(const struct sss_passkey_message *message) +{ + json_t *jroot; + json_t *jdata; + char *str; + + if (message == NULL) { + return NULL; + } + + switch (message->phase) { + case SSS_PASSKEY_PHASE_INIT: + if (message->state != NULL || message->data.ptr != NULL) { + return NULL; + } + jdata = NULL; + break; + case SSS_PASSKEY_PHASE_CHALLENGE: + if (message->state == NULL || message->data.challenge == NULL) { + return NULL; + } + + jdata = sss_passkey_challenge_to_json_object(message->data.challenge); + if (jdata == NULL) { + return NULL; + } + break; + case SSS_PASSKEY_PHASE_REPLY: + if (message->state == NULL || message->data.reply == NULL) { + return NULL; + } + + jdata = sss_passkey_reply_to_json_object(message->data.reply); + if (jdata == NULL) { + return NULL; + } + break; + default: + return NULL; + } + + jroot = json_pack("{s:i, s:s*, s:o*}", + "phase", message->phase, + "state", message->state, + "data", jdata); + if (jroot == NULL) { + json_decref(jdata); + return NULL; + } + + str = json_dumps(jroot, JSON_COMPACT); + json_decref(jroot); + + return str; +} + +struct sss_passkey_message * +sss_passkey_message_from_reply_json(enum sss_passkey_phase phase, + const char *state, + const char *json_str) +{ + json_error_t jret; + json_t *jroot; + struct sss_passkey_message *message; + struct sss_passkey_reply *data; + + if (json_str == NULL) { + return NULL; + } + + jroot = json_loads(json_str, 0, &jret); + if (jroot == NULL) { + return NULL; + } + + data = sss_passkey_reply_from_json_object(jroot); + if (data == NULL) { + json_decref(jroot); + return NULL; + } + + message = sss_passkey_message_init(phase, state, data); + if (message == NULL) { + sss_passkey_reply_free(data); + } + + json_decref(jroot); + return message; +} + +char * +sss_passkey_message_encode(const struct sss_passkey_message *data) +{ + return sss_radius_message_encode(SSSD_PASSKEY_PREFIX, + (sss_radius_message_encode_fn)sss_passkey_message_to_json, data); +} + +struct sss_passkey_message * +sss_passkey_message_decode(const char *str) +{ + return sss_radius_message_decode(SSSD_PASSKEY_PREFIX, + (sss_radius_message_decode_fn)sss_passkey_message_from_json, str); +} + +krb5_pa_data * +sss_passkey_message_encode_padata(const struct sss_passkey_message *data) +{ + return sss_radius_encode_padata(SSSD_PASSKEY_PADATA, + (sss_radius_message_encode_fn)sss_passkey_message_encode, data); +} + +struct sss_passkey_message * +sss_passkey_message_decode_padata(krb5_pa_data *padata) +{ + return sss_radius_decode_padata( + (sss_radius_message_decode_fn)sss_passkey_message_decode, padata); +} + +krb5_pa_data ** +sss_passkey_message_encode_padata_array(const struct sss_passkey_message *data) +{ + return sss_radius_encode_padata_array(SSSD_PASSKEY_PADATA, + (sss_radius_message_encode_fn)sss_passkey_message_encode, data); +} + +krb5_error_code +sss_passkey_concat_credentials(char **creds, + char **_creds_str) +{ + krb5_error_code ret; + char *result_creds = NULL; + size_t total_sz = 0; + size_t len = 0; + int rc = 0; + + for (int i = 0; creds[i] != NULL; i++) { + total_sz += strlen(creds[i]); + if (i > 0) { + /* separating comma in resulting creds string */ + total_sz++; + } + } + + result_creds = malloc(total_sz + 1); + if (result_creds == NULL) { + ret = ENOMEM; + goto done; + } + + len = strlen(creds[0]); + + rc = snprintf(result_creds, len + 1, "%s", creds[0]); + if (rc < 0 || rc > len) { + ret = ENOMEM; + free(result_creds); + goto done; + } + + for (int i = 1; creds[i] != NULL; i++) { + rc = snprintf(result_creds + len, total_sz - len + 1, ",%s", creds[i]); + if (rc < 0 || rc > total_sz - len) { + ret = ENOMEM; + free(result_creds); + goto done; + } + + len += rc; + } + + *_creds_str = result_creds; + + ret = 0; +done: + return ret; +} diff --git a/src/krb5_plugin/passkey/sssd_enable_passkey b/src/krb5_plugin/passkey/sssd_enable_passkey new file mode 100644 index 0000000..161e076 --- /dev/null +++ b/src/krb5_plugin/passkey/sssd_enable_passkey @@ -0,0 +1,14 @@ +# Enable SSSD Passkey Kerberos preauthentication plugins. +# +# This will allow you to obtain Kerberos TGT through passkey authentication. +# +# To disable the passkey plugin, comment out the following lines. + +[plugins] + clpreauth = { + module = passkey:/usr/lib64/sssd/modules/sssd_krb5_passkey_plugin.so + } + + kdcpreauth = { + module = passkey:/usr/lib64/sssd/modules/sssd_krb5_passkey_plugin.so + } |