summaryrefslogtreecommitdiff
path: root/module/prompting.c
diff options
context:
space:
mode:
Diffstat (limited to 'module/prompting.c')
-rw-r--r--module/prompting.c481
1 files changed, 481 insertions, 0 deletions
diff --git a/module/prompting.c b/module/prompting.c
new file mode 100644
index 000000000000..506fb8fd2b22
--- /dev/null
+++ b/module/prompting.c
@@ -0,0 +1,481 @@
+/*
+ * Prompt users for information.
+ *
+ * Handles all interaction with the PAM conversation, either directly or
+ * indirectly through the Kerberos libraries.
+ *
+ * Copyright 2005-2007, 2009, 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <assert.h>
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Build a password prompt.
+ *
+ * The default prompt is simply "Password:". Optionally, a string describing
+ * the type of password is passed in as prefix. In this case, the prompts is:
+ *
+ * <prefix> <banner> password:
+ *
+ * where <prefix> is the argument passed and <banner> is the value of
+ * args->banner (defaulting to "Kerberos").
+ *
+ * If args->config->expose_account is set, we append the principal name (taken
+ * from args->config->ctx->princ) before the colon, so the prompts are:
+ *
+ * Password for <principal>:
+ * <prefix> <banner> password for <principal>:
+ *
+ * Normally this is not done because it exposes the realm and possibly any
+ * username to principal mappings, plus may confuse some ssh clients if sshd
+ * passes the prompt back to the client.
+ *
+ * Returns newly-allocated memory or NULL on failure. The caller is
+ * responsible for freeing.
+ */
+static char *
+build_password_prompt(struct pam_args *args, const char *prefix)
+{
+ struct context *ctx = args->config->ctx;
+ char *principal = NULL;
+ const char *banner, *bspace;
+ char *prompt, *tmp;
+ bool expose_account;
+ krb5_error_code k5_errno;
+ int retval;
+
+ /* If we're exposing the account, format the principal name. */
+ if (args->config->expose_account || prefix != NULL)
+ if (ctx != NULL && ctx->context != NULL && ctx->princ != NULL) {
+ k5_errno = krb5_unparse_name(ctx->context, ctx->princ, &principal);
+ if (k5_errno != 0)
+ putil_debug_krb5(args, k5_errno, "krb5_unparse_name failed");
+ }
+
+ /* Build the part of the prompt without the principal name. */
+ if (prefix == NULL)
+ tmp = strdup("Password");
+ else {
+ banner = (args->config->banner == NULL) ? "" : args->config->banner;
+ bspace = (args->config->banner == NULL) ? "" : " ";
+ retval = asprintf(&tmp, "%s%s%s password", prefix, bspace, banner);
+ if (retval < 0)
+ tmp = NULL;
+ }
+ if (tmp == NULL)
+ goto fail;
+
+ /* Add the principal, if desired, and the colon and space. */
+ expose_account = args->config->expose_account && principal != NULL;
+ if (expose_account)
+ retval = asprintf(&prompt, "%s for %s: ", tmp, principal);
+ else
+ retval = asprintf(&prompt, "%s: ", tmp);
+ free(tmp);
+ if (retval < 0)
+ goto fail;
+
+ /* Clean up and return. */
+ if (principal != NULL)
+ krb5_free_unparsed_name(ctx->context, principal);
+ return prompt;
+
+fail:
+ if (principal != NULL)
+ krb5_free_unparsed_name(ctx->context, principal);
+ return NULL;
+}
+
+
+/*
+ * Prompt for a password.
+ *
+ * The entered password is stored in password. The memory is allocated by the
+ * application and returned as part of the PAM conversation. It must be freed
+ * by the caller.
+ *
+ * Returns a PAM success or error code.
+ */
+int
+pamk5_get_password(struct pam_args *args, const char *prefix, char **password)
+{
+ char *prompt;
+ int retval;
+
+ prompt = build_password_prompt(args, prefix);
+ if (prompt == NULL)
+ return PAM_BUF_ERR;
+ retval = pamk5_conv(args, prompt, PAM_PROMPT_ECHO_OFF, password);
+ free(prompt);
+ return retval;
+}
+
+
+/*
+ * Get information from the user or display a message to the user, as
+ * determined by type. If PAM_SILENT was given, don't pass any text or error
+ * messages to the application.
+ *
+ * The response variable is set to the response returned by the conversation
+ * function on a successful return if a response was desired. Caller is
+ * responsible for freeing it.
+ */
+int
+pamk5_conv(struct pam_args *args, const char *message, int type,
+ char **response)
+{
+ int pamret;
+ struct pam_message msg;
+ PAM_CONST struct pam_message *pmsg;
+ struct pam_response *resp = NULL;
+ struct pam_conv *conv;
+ int want_reply;
+
+ if (args->silent && (type == PAM_ERROR_MSG || type == PAM_TEXT_INFO))
+ return PAM_SUCCESS;
+ pamret = pam_get_item(args->pamh, PAM_CONV, (PAM_CONST void **) &conv);
+ if (pamret != PAM_SUCCESS)
+ return pamret;
+ if (conv->conv == NULL)
+ return PAM_CONV_ERR;
+ pmsg = &msg;
+ msg.msg_style = type;
+ msg.msg = (PAM_CONST char *) message;
+ pamret = conv->conv(1, &pmsg, &resp, conv->appdata_ptr);
+ if (pamret != PAM_SUCCESS)
+ return pamret;
+
+ /*
+ * Only expect a response for PAM_PROMPT_ECHO_OFF or PAM_PROMPT_ECHO_ON
+ * message types. This mildly annoying logic makes sure that everything
+ * is freed properly (except the response itself, if wanted, which is
+ * returned for the caller to free) and that the success status is set
+ * based on whether the reply matched our expectations.
+ *
+ * If we got a reply even though we didn't want one, still overwrite the
+ * reply before freeing in case it was a password.
+ */
+ want_reply = (type == PAM_PROMPT_ECHO_OFF || type == PAM_PROMPT_ECHO_ON);
+ if (resp == NULL || resp->resp == NULL)
+ pamret = want_reply ? PAM_CONV_ERR : PAM_SUCCESS;
+ else if (want_reply && response != NULL) {
+ *response = resp->resp;
+ pamret = PAM_SUCCESS;
+ } else {
+ explicit_bzero(resp->resp, strlen(resp->resp));
+ free(resp->resp);
+ pamret = want_reply ? PAM_SUCCESS : PAM_CONV_ERR;
+ }
+ free(resp);
+ return pamret;
+}
+
+
+/*
+ * Allocate memory to copy all of the prompts into a pam_message.
+ *
+ * Linux PAM and Solaris PAM expect different things here. Solaris PAM
+ * expects to receive a pointer to a pointer to an array of pam_message
+ * structs. Linux PAM expects to receive a pointer to an array of pointers to
+ * pam_message structs. In order for the module to work with either PAM
+ * implementation, we need to set up a structure that is valid either way you
+ * look at it.
+ *
+ * We do this by making msg point to the array of struct pam_message pointers
+ * (what Linux PAM expects), and then make the first one of those pointers
+ * point to the array of pam_message structs. Solaris will then be happy,
+ * looking at only the first element of the outer array and finding it
+ * pointing to the inner array. Then, for Linux, we point the other elements
+ * of the outer array to the storage allocated in the inner array.
+ *
+ * All this also means we have to be careful how we free the resulting
+ * structure since it's double-linked in a subtle way. Thankfully, we get to
+ * free it ourselves.
+ */
+static struct pam_message **
+allocate_pam_message(size_t total_prompts)
+{
+ struct pam_message **msg;
+ size_t i;
+
+ msg = calloc(total_prompts, sizeof(struct pam_message *));
+ if (msg == NULL)
+ return NULL;
+ *msg = calloc(total_prompts, sizeof(struct pam_message));
+ if (*msg == NULL) {
+ free(msg);
+ return NULL;
+ }
+ for (i = 1; i < total_prompts; i++)
+ msg[i] = msg[0] + i;
+ return msg;
+}
+
+
+/*
+ * Free the structure created by allocate_pam_message.
+ */
+static void
+free_pam_message(struct pam_message **msg, size_t total_prompts)
+{
+ size_t i;
+
+ for (i = 0; i < total_prompts; i++)
+ free((char *) msg[i]->msg);
+ free(*msg);
+ free(msg);
+}
+
+
+/*
+ * Free the responses returned by the conversation function. These may
+ * contain passwords, so we overwrite them before we free them.
+ */
+static void
+free_pam_responses(struct pam_response *resp, size_t total_prompts)
+{
+ size_t i;
+
+ if (resp == NULL)
+ return;
+ for (i = 0; i < total_prompts; i++) {
+ if (resp[i].resp != NULL) {
+ explicit_bzero(resp[i].resp, strlen(resp[i].resp));
+ free(resp[i].resp);
+ }
+ }
+ free(resp);
+}
+
+
+/*
+ * Format a Kerberos prompt into a PAM prompt. Takes a krb5_prompt as input
+ * and writes the resulting PAM prompt into a struct pam_message.
+ */
+static krb5_error_code
+format_prompt(krb5_prompt *prompt, struct pam_message *message)
+{
+ size_t len = strlen(prompt->prompt);
+ bool has_colon;
+ const char *colon;
+ int retval, style;
+
+ /*
+ * Heimdal adds the trailing colon and space, while MIT does not.
+ * Work around the difference by looking to see if there's a trailing
+ * colon and space already and only adding it if there is not.
+ */
+ has_colon = (len > 2 && memcmp(&prompt->prompt[len - 2], ": ", 2) == 0);
+ colon = has_colon ? "" : ": ";
+ retval = asprintf((char **) &message->msg, "%s%s", prompt->prompt, colon);
+ if (retval < 0)
+ return retval;
+ style = prompt->hidden ? PAM_PROMPT_ECHO_OFF : PAM_PROMPT_ECHO_ON;
+ message->msg_style = style;
+ return 0;
+}
+
+
+/*
+ * Given an array of struct pam_response elements, record the responses in the
+ * corresponding krb5_prompt structures.
+ */
+static krb5_error_code
+record_prompt_answers(struct pam_response *resp, int num_prompts,
+ krb5_prompt *prompts)
+{
+ int i;
+
+ for (i = 0; i < num_prompts; i++) {
+ size_t len, allowed;
+
+ if (resp[i].resp == NULL)
+ return KRB5_LIBOS_CANTREADPWD;
+ len = strlen(resp[i].resp);
+ allowed = prompts[i].reply->length;
+ if (allowed == 0 || len > allowed - 1)
+ return KRB5_LIBOS_CANTREADPWD;
+
+ /*
+ * Since the first version of this module, it has copied a nul
+ * character into the prompt data buffer for MIT Kerberos with the
+ * note that "other applications expect it to be there." I suspect
+ * this is incorrect and nothing cares about this nul, but have
+ * preserved this behavior out of an abundance of caution.
+ *
+ * Note that it shortens the maximum response length we're willing to
+ * accept by one (implemented above) and is the source of one prior
+ * security vulnerability.
+ */
+ memcpy(prompts[i].reply->data, resp[i].resp, len + 1);
+ prompts[i].reply->length = (unsigned int) len;
+ }
+ return 0;
+}
+
+
+/*
+ * This is the generic prompting function called by both MIT Kerberos and
+ * Heimdal prompting implementations.
+ *
+ * There are a lot of structures and different layers of code at work here,
+ * making this code quite confusing. This function is a prompter function to
+ * pass into the Kerberos library, in particular krb5_get_init_creds_password.
+ * It is used by the Kerberos library to prompt for a password if need be, and
+ * also to prompt for password changes if the password was expired.
+ *
+ * The purpose of this function is to serve as glue between the Kerberos
+ * library and the application (by way of the PAM glue). PAM expects us to
+ * pass back to the conversation function an array of prompts and receive from
+ * the application an array of responses to those prompts. We pass the
+ * application an array of struct pam_message pointers, and the application
+ * passes us an array of struct pam_response pointers.
+ *
+ * Kerberos, meanwhile, passes us in an array of krb5_prompt structs. This
+ * struct contains the prompt, a flag saying whether to suppress echoing of
+ * what the user types for that prompt, and a buffer into which to store the
+ * response.
+ *
+ * Therefore, what we're doing here is copying the prompts from the
+ * krb5_prompt structs into pam_message structs, calling the conversation
+ * function, and then copying the responses back out of pam_response structs
+ * into the krb5_prompt structs to return to the Kerberos library.
+ */
+krb5_error_code
+pamk5_prompter_krb5(krb5_context context UNUSED, void *data, const char *name,
+ const char *banner, int num_prompts, krb5_prompt *prompts)
+{
+ struct pam_args *args = data;
+ int current_prompt, retval, pamret, i, offset;
+ int total_prompts = num_prompts;
+ struct pam_message **msg;
+ struct pam_response *resp = NULL;
+ struct pam_conv *conv;
+
+ /* Treat the name and banner as prompts that doesn't need input. */
+ if (name != NULL && !args->silent)
+ total_prompts++;
+ if (banner != NULL && !args->silent)
+ total_prompts++;
+
+ /* If we have zero prompts, do nothing, silently. */
+ if (total_prompts == 0)
+ return 0;
+
+ /* Obtain the conversation function from the application. */
+ pamret = pam_get_item(args->pamh, PAM_CONV, (PAM_CONST void **) &conv);
+ if (pamret != 0)
+ return KRB5_LIBOS_CANTREADPWD;
+ if (conv->conv == NULL)
+ return KRB5_LIBOS_CANTREADPWD;
+
+ /* Allocate memory to copy all of the prompts into a pam_message. */
+ msg = allocate_pam_message(total_prompts);
+ if (msg == NULL)
+ return ENOMEM;
+
+ /* current_prompt is an index into msg and a count when we're done. */
+ current_prompt = 0;
+ if (name != NULL && !args->silent) {
+ msg[current_prompt]->msg = strdup(name);
+ if (msg[current_prompt]->msg == NULL) {
+ retval = ENOMEM;
+ goto cleanup;
+ }
+ msg[current_prompt]->msg_style = PAM_TEXT_INFO;
+ current_prompt++;
+ }
+ if (banner != NULL && !args->silent) {
+ assert(current_prompt < total_prompts);
+ msg[current_prompt]->msg = strdup(banner);
+ if (msg[current_prompt]->msg == NULL) {
+ retval = ENOMEM;
+ goto cleanup;
+ }
+ msg[current_prompt]->msg_style = PAM_TEXT_INFO;
+ current_prompt++;
+ }
+ for (i = 0; i < num_prompts; i++) {
+ assert(current_prompt < total_prompts);
+ retval = format_prompt(&prompts[i], msg[current_prompt]);
+ if (retval < 0)
+ goto cleanup;
+ current_prompt++;
+ }
+
+ /* Call into the application conversation function. */
+ pamret = conv->conv(total_prompts, (PAM_CONST struct pam_message **) msg,
+ &resp, conv->appdata_ptr);
+ if (pamret != 0 || resp == NULL) {
+ retval = KRB5_LIBOS_CANTREADPWD;
+ goto cleanup;
+ }
+
+ /*
+ * Record the answers in the Kerberos data structure. If name or banner
+ * were provided, skip over the initial PAM responses that correspond to
+ * those messages.
+ */
+ offset = 0;
+ if (name != NULL && !args->silent)
+ offset++;
+ if (banner != NULL && !args->silent)
+ offset++;
+ retval = record_prompt_answers(resp + offset, num_prompts, prompts);
+
+cleanup:
+ free_pam_message(msg, total_prompts);
+ free_pam_responses(resp, total_prompts);
+ return retval;
+}
+
+
+/*
+ * This is a special version of krb5_prompter_krb5 that returns an error if
+ * the Kerberos library asks for a password. It is only used with MIT
+ * Kerberos as part of the implementation of try_pkinit and use_pkinit.
+ * (Heimdal has a different API for PKINIT authentication.)
+ */
+#ifdef HAVE_KRB5_GET_PROMPT_TYPES
+krb5_error_code
+pamk5_prompter_krb5_no_password(krb5_context context, void *data,
+ const char *name, const char *banner,
+ int num_prompts, krb5_prompt *prompts)
+{
+ krb5_prompt_type *ptypes;
+ int i;
+
+ ptypes = krb5_get_prompt_types(context);
+ for (i = 0; i < num_prompts; i++)
+ if (ptypes != NULL && ptypes[i] == KRB5_PROMPT_TYPE_PASSWORD)
+ return KRB5_LIBOS_CANTREADPWD;
+ return pamk5_prompter_krb5(context, data, name, banner, num_prompts,
+ prompts);
+}
+#else /* !HAVE_KRB5_GET_PROMPT_TYPES */
+krb5_error_code
+pamk5_prompter_krb5_no_password(krb5_context context, void *data,
+ const char *name, const char *banner,
+ int num_prompts, krb5_prompt *prompts)
+{
+ return pamk5_prompter_krb5(context, data, name, banner, num_prompts,
+ prompts);
+}
+#endif /* !HAVE_KRB5_GET_PROMPT_TYPES */