aboutsummaryrefslogtreecommitdiff
path: root/module
diff options
context:
space:
mode:
Diffstat (limited to 'module')
-rw-r--r--module/account.c92
-rw-r--r--module/alt-auth.c240
-rw-r--r--module/auth.c1135
-rw-r--r--module/cache.c185
-rw-r--r--module/context.c177
-rw-r--r--module/fast.c288
-rw-r--r--module/internal.h261
-rw-r--r--module/options.c259
-rw-r--r--module/pam_krb5.map11
-rw-r--r--module/pam_krb5.sym6
-rw-r--r--module/password.c401
-rw-r--r--module/prompting.c481
-rw-r--r--module/public.c260
-rw-r--r--module/setcred.c474
-rw-r--r--module/support.c141
15 files changed, 4411 insertions, 0 deletions
diff --git a/module/account.c b/module/account.c
new file mode 100644
index 000000000000..c270c9b97431
--- /dev/null
+++ b/module/account.c
@@ -0,0 +1,92 @@
+/*
+ * Implements the PAM authorization function (pam_acct_mgmt).
+ *
+ * We don't have much to do for account management, but we do recheck the
+ * user's authorization against .k5login (or whatever equivalent we've been
+ * configured for).
+ *
+ * Copyright 2005-2009, 2014, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * 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+
+ */
+
+/* Get prototypes for the account management functions. */
+#define PAM_SM_ACCOUNT
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Check the authorization of the user. It's not entirely clear what this
+ * function is supposed to do, but rechecking .k5login and friends makes the
+ * most sense.
+ */
+int
+pamk5_account(struct pam_args *args)
+{
+ struct context *ctx;
+ int retval;
+ const char *name;
+
+ /* If the account was expired, here's where we actually fail. */
+ ctx = args->config->ctx;
+ if (ctx->expired) {
+ pam_syslog(args->pamh, LOG_INFO, "user %s account password is expired",
+ ctx->name);
+ return PAM_NEW_AUTHTOK_REQD;
+ }
+
+ /*
+ * Re-retrieve the user rather than trusting our context; it's conceivable
+ * the application could have changed it. We have to cast &name due to
+ * C's broken type system.
+ *
+ * Use pam_get_item rather than pam_get_user here since the user should be
+ * set by the time we get to this point. If we would have to prompt for a
+ * user, something is definitely broken and we should fail.
+ */
+ retval = pam_get_item(args->pamh, PAM_USER, (PAM_CONST void **) &name);
+ if (retval != PAM_SUCCESS || name == NULL) {
+ putil_err_pam(args, retval, "unable to retrieve user");
+ return PAM_AUTH_ERR;
+ }
+ if (ctx->name != name) {
+ free(ctx->name);
+ ctx->name = strdup(name);
+ args->user = ctx->name;
+ }
+
+ /*
+ * If we have a ticket cache, then we can apply an additional bit of
+ * paranoia. Rather than trusting princ in the context, extract the
+ * principal from the Kerberos ticket cache we actually received and then
+ * validate that. This should make no difference in practice, but it's a
+ * bit more thorough.
+ */
+ if (ctx->cache != NULL) {
+ putil_debug(args, "retrieving principal from cache");
+ if (ctx->princ != NULL) {
+ krb5_free_principal(ctx->context, ctx->princ);
+ ctx->princ = NULL;
+ }
+ retval = krb5_cc_get_principal(ctx->context, ctx->cache, &ctx->princ);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot get principal from cache");
+ return PAM_AUTH_ERR;
+ }
+ }
+ return pamk5_authorized(args);
+}
diff --git a/module/alt-auth.c b/module/alt-auth.c
new file mode 100644
index 000000000000..e5294bbceb7b
--- /dev/null
+++ b/module/alt-auth.c
@@ -0,0 +1,240 @@
+/*
+ * Support for alternate authentication mapping.
+ *
+ * pam-krb5 supports a feature where the principal for authentication can be
+ * set via a PAM option and possibly based on the authenticating user. This
+ * can be used to, for example, require /root instances be used with sudo
+ * while still using normal instances for other system authentications.
+ *
+ * This file collects all the pieces related to that support.
+ *
+ * Original support written by Booker Bense <bbense@slac.stanford.edu>
+ * Further updates by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2008-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * 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 <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Map the user to a Kerberos principal according to alt_auth_map. Returns 0
+ * on success, storing the mapped principal name in newly allocated memory in
+ * principal. The caller is responsible for freeing. Returns an errno value
+ * on any error.
+ */
+int
+pamk5_map_principal(struct pam_args *args, const char *username,
+ char **principal)
+{
+ char *realm;
+ char *new_user = NULL;
+ const char *user;
+ const char *p;
+ size_t needed, offset;
+ int oerrno;
+
+ /* Makes no sense if alt_auth_map isn't set. */
+ if (args->config->alt_auth_map == NULL)
+ return EINVAL;
+
+ /* Need to split off the realm if it is present. */
+ realm = strchr(username, '@');
+ if (realm == NULL)
+ user = username;
+ else {
+ new_user = strdup(username);
+ if (new_user == NULL)
+ return errno;
+ realm = strchr(new_user, '@');
+ if (realm == NULL)
+ goto fail;
+ *realm = '\0';
+ realm++;
+ user = new_user;
+ }
+
+ /* Now, allocate a string and build the principal. */
+ needed = 0;
+ for (p = args->config->alt_auth_map; *p != '\0'; p++) {
+ if (p[0] == '%' && p[1] == 's') {
+ needed += strlen(user);
+ p++;
+ } else {
+ needed++;
+ }
+ }
+ if (realm != NULL && strchr(args->config->alt_auth_map, '@') == NULL)
+ needed += 1 + strlen(realm);
+ needed++;
+ *principal = malloc(needed);
+ if (*principal == NULL)
+ goto fail;
+ offset = 0;
+ for (p = args->config->alt_auth_map; *p != '\0'; p++) {
+ if (p[0] == '%' && p[1] == 's') {
+ memcpy(*principal + offset, user, strlen(user));
+ offset += strlen(user);
+ p++;
+ } else {
+ (*principal)[offset] = *p;
+ offset++;
+ }
+ }
+ if (realm != NULL && strchr(args->config->alt_auth_map, '@') == NULL) {
+ (*principal)[offset] = '@';
+ offset++;
+ memcpy(*principal + offset, realm, strlen(realm));
+ offset += strlen(realm);
+ }
+ (*principal)[offset] = '\0';
+ free(new_user);
+ return 0;
+
+fail:
+ if (new_user != NULL) {
+ oerrno = errno;
+ free(new_user);
+ errno = oerrno;
+ }
+ return errno;
+}
+
+
+/*
+ * Authenticate using an alternate principal mapping.
+ *
+ * Create a principal based on the principal mapping and the user, and use the
+ * provided password to try to authenticate as that user. If we succeed, fill
+ * out creds, set princ to the successful principal in the context, and return
+ * 0. Otherwise, return a Kerberos error code or an errno value.
+ */
+krb5_error_code
+pamk5_alt_auth(struct pam_args *args, const char *service,
+ krb5_get_init_creds_opt *opts, const char *pass,
+ krb5_creds *creds)
+{
+ struct context *ctx = args->config->ctx;
+ char *kuser;
+ krb5_principal princ;
+ krb5_error_code retval;
+
+ retval = pamk5_map_principal(args, ctx->name, &kuser);
+ if (retval != 0)
+ return retval;
+ retval = krb5_parse_name(ctx->context, kuser, &princ);
+ if (retval != 0) {
+ free(kuser);
+ return retval;
+ }
+ free(kuser);
+
+ /* Log the principal we're attempting to authenticate as. */
+ if (args->debug) {
+ char *principal;
+
+ retval = krb5_unparse_name(ctx->context, princ, &principal);
+ if (retval != 0)
+ putil_debug_krb5(args, retval, "krb5_unparse_name failed");
+ else {
+ putil_debug(args, "mapping %s to %s", ctx->name, principal);
+ krb5_free_unparsed_name(ctx->context, principal);
+ }
+ }
+
+ /*
+ * Now, attempt to authenticate as that user. On success, save the
+ * principal. Return the Kerberos status code.
+ */
+ retval = krb5_get_init_creds_password(ctx->context, creds, princ,
+ (char *) pass, pamk5_prompter_krb5,
+ args, 0, (char *) service, opts);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "alternate authentication failed");
+ krb5_free_principal(ctx->context, princ);
+ return retval;
+ } else {
+ putil_debug(args, "alternate authentication successful");
+ if (ctx->princ != NULL)
+ krb5_free_principal(ctx->context, ctx->princ);
+ ctx->princ = princ;
+ return 0;
+ }
+}
+
+
+/*
+ * Verify an alternate authentication.
+ *
+ * Meant to be called from pamk5_authorized, this checks that the principal in
+ * the context matches the alt_auth_map-derived identity of the user we're
+ * authenticating. Returns PAM_SUCCESS if they match, PAM_AUTH_ERR if they
+ * don't match, and PAM_SERVICE_ERR on an internal error.
+ */
+int
+pamk5_alt_auth_verify(struct pam_args *args)
+{
+ struct context *ctx;
+ char *name = NULL;
+ char *mapped = NULL;
+ char *authed = NULL;
+ krb5_principal princ = NULL;
+ krb5_error_code retval;
+ int status = PAM_SERVICE_ERR;
+
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+ if (ctx->context == NULL || ctx->name == NULL)
+ return PAM_SERVICE_ERR;
+ if (pamk5_map_principal(args, ctx->name, &name) != 0) {
+ putil_err(args, "cannot map principal name");
+ goto done;
+ }
+ retval = krb5_parse_name(ctx->context, name, &princ);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot parse mapped principal name %s",
+ mapped);
+ goto done;
+ }
+ retval = krb5_unparse_name(ctx->context, princ, &mapped);
+ if (retval != 0) {
+ putil_err_krb5(args, retval,
+ "krb5_unparse_name on mapped principal failed");
+ goto done;
+ }
+ retval = krb5_unparse_name(ctx->context, ctx->princ, &authed);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "krb5_unparse_name failed");
+ goto done;
+ }
+ if (strcmp(authed, mapped) == 0)
+ status = PAM_SUCCESS;
+ else {
+ putil_debug(args, "mapped user %s does not match principal %s", mapped,
+ authed);
+ status = PAM_AUTH_ERR;
+ }
+
+done:
+ free(name);
+ if (authed != NULL)
+ krb5_free_unparsed_name(ctx->context, authed);
+ if (mapped != NULL)
+ krb5_free_unparsed_name(ctx->context, mapped);
+ if (princ != NULL)
+ krb5_free_principal(ctx->context, princ);
+ return status;
+}
diff --git a/module/auth.c b/module/auth.c
new file mode 100644
index 000000000000..065ce97b6596
--- /dev/null
+++ b/module/auth.c
@@ -0,0 +1,1135 @@
+/*
+ * Core authentication routines for pam_krb5.
+ *
+ * The actual authentication work is done here, either via password or via
+ * PKINIT. The only external interface is pamk5_password_auth, which calls
+ * the appropriate internal functions. This interface is used by both the
+ * authentication and the password groups.
+ *
+ * Copyright 2005-2010, 2014-2015, 2017, 2020
+ * Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2012, 2014
+ * 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 <errno.h>
+#ifdef HAVE_HX509_ERR_H
+# include <hx509_err.h>
+#endif
+#include <pwd.h>
+#include <sys/stat.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+#include <pam-util/vector.h>
+
+/*
+ * If the PKINIT smart card error statuses aren't defined, define them to 0.
+ * This will cause the right thing to happen with the logic around PKINIT.
+ */
+#ifndef HX509_PKCS11_NO_TOKEN
+# define HX509_PKCS11_NO_TOKEN 0
+#endif
+#ifndef HX509_PKCS11_NO_SLOT
+# define HX509_PKCS11_NO_SLOT 0
+#endif
+
+
+/*
+ * Fill in ctx->princ from the value of ctx->name or (if configured) from
+ * prompting. If we don't prompt and ctx->name contains an @-sign,
+ * canonicalize it to a local account name unless no_update_user is set. If
+ * the canonicalization fails, don't worry about it. It may be that the
+ * application doesn't care.
+ */
+static krb5_error_code
+parse_name(struct pam_args *args)
+{
+ struct context *ctx = args->config->ctx;
+ krb5_context c = ctx->context;
+ char *user_realm;
+ char *user = ctx->name;
+ char *newuser = NULL;
+ char kuser[65] = ""; /* MAX_USERNAME == 65 (MIT Kerberos 1.4.1). */
+ krb5_error_code k5_errno;
+ int retval;
+
+ /*
+ * If configured to prompt for the principal, do that first. Fall back on
+ * using the local username as normal if prompting fails or if the user
+ * just presses Enter.
+ */
+ if (args->config->prompt_principal) {
+ retval = pamk5_conv(args, "Principal: ", PAM_PROMPT_ECHO_ON, &user);
+ if (retval != PAM_SUCCESS)
+ putil_err_pam(args, retval, "error getting principal");
+ if (*user == '\0') {
+ free(user);
+ user = ctx->name;
+ }
+ }
+
+ /*
+ * We don't just call krb5_parse_name so that we can work around a bug in
+ * MIT Kerberos versions prior to 1.4, which store the realm in a static
+ * variable inside the library and don't notice changes. If no realm is
+ * specified and a realm is set in our arguments, append the realm to
+ * force krb5_parse_name to do the right thing.
+ */
+ user_realm = args->realm;
+ if (args->config->user_realm)
+ user_realm = args->config->user_realm;
+ if (user_realm != NULL && strchr(user, '@') == NULL) {
+ if (asprintf(&newuser, "%s@%s", user, user_realm) < 0) {
+ if (user != ctx->name)
+ free(user);
+ return KRB5_CC_NOMEM;
+ }
+ if (user != ctx->name)
+ free(user);
+ user = newuser;
+ }
+ k5_errno = krb5_parse_name(c, user, &ctx->princ);
+ if (user != ctx->name)
+ free(user);
+ if (k5_errno != 0)
+ return k5_errno;
+
+ /*
+ * Now that we have a principal to call krb5_aname_to_localname, we can
+ * canonicalize ctx->name to a local name. We do this even if we were
+ * explicitly prompting for a principal, but we use ctx->name to generate
+ * the local username, not the principal name. It's unlikely, and would
+ * be rather weird, if the user were to specify a principal name for the
+ * username and then enter a different username at the principal prompt,
+ * but this behavior seems to make the most sense.
+ *
+ * Skip canonicalization if no_update_user was set. In that case,
+ * continue to use the initial authentication identity everywhere.
+ */
+ if (strchr(ctx->name, '@') != NULL && !args->config->no_update_user) {
+ if (krb5_aname_to_localname(c, ctx->princ, sizeof(kuser), kuser) != 0)
+ return 0;
+ user = strdup(kuser);
+ if (user == NULL) {
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ return 0;
+ }
+ free(ctx->name);
+ ctx->name = user;
+ args->user = user;
+ }
+ return k5_errno;
+}
+
+
+/*
+ * Set initial credential options based on our configuration information, and
+ * using the Heimdal call to set initial credential options if it's available.
+ * This function is used both for regular password authentication and for
+ * PKINIT. It also configures FAST if requested and the Kerberos libraries
+ * support it.
+ *
+ * Takes a flag indicating whether we're getting tickets for a specific
+ * service. If so, we don't try to get forwardable, renewable, or proxiable
+ * tickets.
+ */
+static void
+set_credential_options(struct pam_args *args, krb5_get_init_creds_opt *opts,
+ int service)
+{
+ struct pam_config *config = args->config;
+ krb5_context c = config->ctx->context;
+
+ krb5_get_init_creds_opt_set_default_flags(c, "pam", args->realm, opts);
+ if (!service) {
+ if (config->forwardable)
+ krb5_get_init_creds_opt_set_forwardable(opts, 1);
+ if (config->ticket_lifetime != 0)
+ krb5_get_init_creds_opt_set_tkt_life(opts,
+ config->ticket_lifetime);
+ if (config->renew_lifetime != 0)
+ krb5_get_init_creds_opt_set_renew_life(opts,
+ config->renew_lifetime);
+ krb5_get_init_creds_opt_set_change_password_prompt(
+ opts, (config->defer_pwchange || config->fail_pwchange) ? 0 : 1);
+ } else {
+ krb5_get_init_creds_opt_set_forwardable(opts, 0);
+ krb5_get_init_creds_opt_set_proxiable(opts, 0);
+ krb5_get_init_creds_opt_set_renew_life(opts, 0);
+ }
+ pamk5_fast_setup(args, opts);
+
+ /*
+ * Set options for PKINIT. Only used with MIT Kerberos; Heimdal's
+ * implementation of PKINIT uses a separate API instead of setting
+ * get_init_creds options.
+ */
+#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PA
+ if (config->use_pkinit || config->try_pkinit) {
+ if (config->pkinit_user != NULL)
+ krb5_get_init_creds_opt_set_pa(c, opts, "X509_user_identity",
+ config->pkinit_user);
+ if (config->pkinit_anchors != NULL)
+ krb5_get_init_creds_opt_set_pa(c, opts, "X509_anchors",
+ config->pkinit_anchors);
+ if (config->preauth_opt != NULL && config->preauth_opt->count > 0) {
+ size_t i;
+ char *name, *value;
+ char save = '\0';
+
+ for (i = 0; i < config->preauth_opt->count; i++) {
+ name = config->preauth_opt->strings[i];
+ if (name == NULL)
+ continue;
+ value = strchr(name, '=');
+ if (value != NULL) {
+ save = *value;
+ *value = '\0';
+ value++;
+ }
+ krb5_get_init_creds_opt_set_pa(
+ c, opts, name, (value != NULL) ? value : "yes");
+ if (value != NULL)
+ value[-1] = save;
+ }
+ }
+ }
+#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PA */
+}
+
+
+/*
+ * Retrieve the existing password (authtok) stored in the PAM data if
+ * appropriate and if available. We decide whether to retrieve it based on
+ * the PAM configuration, and also decied whether failing to retrieve it is a
+ * fatal error. Takes the PAM arguments, the PAM authtok code to retrieve
+ * (may be PAM_AUTHTOK or PAM_OLDAUTHTOK depending on whether we're
+ * authenticating or changing the password), and the place to store the
+ * password. Returns a PAM status code.
+ *
+ * If try_first_pass, use_first_pass, or force_first_pass is set, grab the old
+ * password (if set). If force_first_pass is set, fail if the password is not
+ * already set.
+ *
+ * The empty password has to be handled separately, since the Kerberos
+ * libraries may treat it as equivalent to no password and prompt when we
+ * don't want them to. We make the assumption here that the empty password is
+ * always invalid and is an authentication failure.
+ */
+static int
+maybe_retrieve_password(struct pam_args *args, int authtok, const char **pass)
+{
+ int status;
+ const bool try_first = args->config->try_first_pass;
+ const bool use = args->config->use_first_pass;
+ const bool force = args->config->force_first_pass;
+
+ *pass = NULL;
+ if (!try_first && !use && !force)
+ return PAM_SUCCESS;
+ status = pam_get_item(args->pamh, authtok, (PAM_CONST void **) pass);
+ if (*pass != NULL && **pass == '\0') {
+ if (use || force) {
+ putil_debug(args, "rejecting empty password");
+ return PAM_AUTH_ERR;
+ }
+ *pass = NULL;
+ }
+ if (*pass != NULL && strlen(*pass) > PAM_MAX_RESP_SIZE - 1) {
+ putil_debug(args, "rejecting password longer than %d",
+ PAM_MAX_RESP_SIZE - 1);
+ return PAM_AUTH_ERR;
+ }
+ if (force && (status != PAM_SUCCESS || *pass == NULL)) {
+ putil_debug_pam(args, status, "no stored password");
+ return PAM_AUTH_ERR;
+ }
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Prompt for the password. Takes the PAM arguments, the authtok for which
+ * we're prompting (may be PAM_AUTHTOK or PAM_OLDAUTHTOK depending on whether
+ * we're authenticating or changing the password), and the place to store the
+ * password. Returns a PAM status code.
+ *
+ * If we successfully get a password, store it in the PAM data, free it, and
+ * then return the password as retrieved from the PAM data so that we don't
+ * have to worry about memory allocation later.
+ *
+ * The empty password has to be handled separately, since the Kerberos
+ * libraries may treat it as equivalent to no password and prompt when we
+ * don't want them to. We make the assumption here that the empty password is
+ * always invalid and is an authentication failure.
+ */
+static int
+prompt_password(struct pam_args *args, int authtok, const char **pass)
+{
+ char *password;
+ int status;
+ const char *prompt = (authtok == PAM_AUTHTOK) ? NULL : "Current";
+
+ *pass = NULL;
+ status = pamk5_get_password(args, prompt, &password);
+ if (status != PAM_SUCCESS) {
+ putil_debug_pam(args, status, "error getting password");
+ return PAM_AUTH_ERR;
+ }
+ if (password[0] == '\0') {
+ putil_debug(args, "rejecting empty password");
+ free(password);
+ return PAM_AUTH_ERR;
+ }
+ if (strlen(password) > PAM_MAX_RESP_SIZE - 1) {
+ putil_debug(args, "rejecting password longer than %d",
+ PAM_MAX_RESP_SIZE - 1);
+ explicit_bzero(password, strlen(password));
+ free(password);
+ return PAM_AUTH_ERR;
+ }
+
+ /* Set this for the next PAM module. */
+ status = pam_set_item(args->pamh, authtok, password);
+ explicit_bzero(password, strlen(password));
+ free(password);
+ if (status != PAM_SUCCESS) {
+ putil_err_pam(args, status, "error storing password");
+ return PAM_AUTH_ERR;
+ }
+
+ /* Return the password retrieved from PAM. */
+ status = pam_get_item(args->pamh, authtok, (PAM_CONST void **) pass);
+ if (status != PAM_SUCCESS) {
+ putil_err_pam(args, status, "error retrieving password");
+ status = PAM_AUTH_ERR;
+ }
+ return status;
+}
+
+
+/*
+ * Authenticate via password.
+ *
+ * This is our basic authentication function. Log what principal we're
+ * attempting to authenticate with and then attempt password authentication.
+ * Returns 0 on success or a Kerberos error on failure.
+ */
+static krb5_error_code
+password_auth(struct pam_args *args, krb5_creds *creds,
+ krb5_get_init_creds_opt *opts, const char *service,
+ const char *pass)
+{
+ struct context *ctx = args->config->ctx;
+ krb5_error_code retval;
+
+ /* Log the principal as which we're attempting authentication. */
+ if (args->debug) {
+ char *principal;
+
+ retval = krb5_unparse_name(ctx->context, ctx->princ, &principal);
+ if (retval != 0)
+ putil_debug_krb5(args, retval, "krb5_unparse_name failed");
+ else {
+ if (service == NULL)
+ putil_debug(args, "attempting authentication as %s",
+ principal);
+ else
+ putil_debug(args, "attempting authentication as %s for %s",
+ principal, service);
+ free(principal);
+ }
+ }
+
+ /* Do the authentication. */
+ retval = krb5_get_init_creds_password(ctx->context, creds, ctx->princ,
+ (char *) pass, pamk5_prompter_krb5,
+ args, 0, (char *) service, opts);
+
+ /*
+ * Heimdal may return an expired key error even if the password is
+ * incorrect. To avoid accepting any incorrect password for the user
+ * in the fully correct password change case, confirm that we can get
+ * a password change ticket for the user using this password, and
+ * otherwise change the error to invalid password.
+ */
+ if (retval == KRB5KDC_ERR_KEY_EXP) {
+ krb5_get_init_creds_opt *heimdal_opts = NULL;
+
+ retval = krb5_get_init_creds_opt_alloc(ctx->context, &heimdal_opts);
+ if (retval == 0) {
+ set_credential_options(args, opts, 1);
+ retval = krb5_get_init_creds_password(
+ ctx->context, creds, ctx->princ, (char *) pass,
+ pamk5_prompter_krb5, args, 0, (char *) "kadmin/changepw",
+ heimdal_opts);
+ krb5_get_init_creds_opt_free(ctx->context, heimdal_opts);
+ }
+ if (retval == 0) {
+ retval = KRB5KDC_ERR_KEY_EXP;
+ krb5_free_cred_contents(ctx->context, creds);
+ explicit_bzero(creds, sizeof(krb5_creds));
+ }
+ }
+ return retval;
+}
+
+
+/*
+ * Authenticate by trying each principal in the .k5login file.
+ *
+ * Read through each line that parses correctly as a principal and use the
+ * provided password to try to authenticate as that user. If at any point we
+ * succeed, fill out creds, set princ to the successful principal in the
+ * context, and return 0. Otherwise, return either a Kerberos error code or
+ * errno for a system error.
+ */
+static krb5_error_code
+k5login_password_auth(struct pam_args *args, krb5_creds *creds,
+ krb5_get_init_creds_opt *opts, const char *service,
+ const char *pass)
+{
+ struct context *ctx = args->config->ctx;
+ char *filename = NULL;
+ char line[BUFSIZ];
+ size_t len;
+ FILE *k5login;
+ struct passwd *pwd;
+ struct stat st;
+ krb5_error_code k5_errno, retval;
+ krb5_principal princ;
+
+ /*
+ * C sucks at string manipulation. Generate the filename for the user's
+ * .k5login file. If the user doesn't exist, the .k5login file doesn't
+ * exist, or the .k5login file cannot be read, fall back on the easy way
+ * and assume ctx->princ is already set properly.
+ */
+ pwd = pam_modutil_getpwnam(args->pamh, ctx->name);
+ if (pwd != NULL)
+ if (asprintf(&filename, "%s/.k5login", pwd->pw_dir) < 0) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ return errno;
+ }
+ if (pwd == NULL || filename == NULL || access(filename, R_OK) != 0) {
+ free(filename);
+ return krb5_get_init_creds_password(ctx->context, creds, ctx->princ,
+ (char *) pass, pamk5_prompter_krb5,
+ args, 0, (char *) service, opts);
+ }
+
+ /*
+ * Make sure the ownership on .k5login is okay. The user must own their
+ * own .k5login or it must be owned by root. If that fails, set the
+ * Kerberos error code to errno.
+ */
+ k5login = fopen(filename, "r");
+ if (k5login == NULL) {
+ retval = errno;
+ free(filename);
+ return retval;
+ }
+ free(filename);
+ if (fstat(fileno(k5login), &st) != 0) {
+ retval = errno;
+ goto fail;
+ }
+ if (st.st_uid != 0 && (st.st_uid != pwd->pw_uid)) {
+ retval = EACCES;
+ putil_err(args, "unsafe .k5login ownership (saw %lu, expected %lu)",
+ (unsigned long) st.st_uid, (unsigned long) pwd->pw_uid);
+ goto fail;
+ }
+
+ /*
+ * Parse the .k5login file and attempt authentication for each principal.
+ * Ignore any lines that are too long or that don't parse into a Kerberos
+ * principal. Assume an invalid password error if there are no valid
+ * lines in .k5login.
+ */
+ retval = KRB5KRB_AP_ERR_BAD_INTEGRITY;
+ while (fgets(line, BUFSIZ, k5login) != NULL) {
+ len = strlen(line);
+ if (line[len - 1] != '\n') {
+ while (fgets(line, BUFSIZ, k5login) != NULL) {
+ len = strlen(line);
+ if (line[len - 1] == '\n')
+ break;
+ }
+ continue;
+ }
+ line[len - 1] = '\0';
+ k5_errno = krb5_parse_name(ctx->context, line, &princ);
+ if (k5_errno != 0)
+ continue;
+
+ /* Now, attempt to authenticate as that user. */
+ if (service == NULL)
+ putil_debug(args, "attempting authentication as %s", line);
+ else
+ putil_debug(args, "attempting authentication as %s for %s", line,
+ service);
+ retval = krb5_get_init_creds_password(
+ ctx->context, creds, princ, (char *) pass, pamk5_prompter_krb5,
+ args, 0, (char *) service, opts);
+
+ /*
+ * If that worked, update ctx->princ and return success. Otherwise,
+ * continue on to the next line.
+ */
+ if (retval == 0) {
+ if (ctx->princ != NULL)
+ krb5_free_principal(ctx->context, ctx->princ);
+ ctx->princ = princ;
+ fclose(k5login);
+ return 0;
+ }
+ krb5_free_principal(ctx->context, princ);
+ }
+
+fail:
+ fclose(k5login);
+ return retval;
+}
+
+
+#if (defined(HAVE_KRB5_HEIMDAL) \
+ && defined(HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT)) \
+ || defined(HAVE_KRB5_GET_PROMPT_TYPES)
+/*
+ * Attempt authentication via PKINIT. Currently, this uses an API specific to
+ * Heimdal. Once MIT Kerberos supports PKINIT, some of the details may need
+ * to move into the compat layer.
+ *
+ * Some smart card readers require the user to enter the PIN at the keyboard
+ * after inserting the smart card. Others have a pad on the card and no
+ * prompting by PAM is required. The Kerberos library prompting functions
+ * should be able to work out which is required.
+ *
+ * PKINIT is just one of many pre-authentication mechanisms that could be
+ * used. It's handled separately because of possible smart card interactions
+ * and the possibility that some users may be authenticated via PKINIT and
+ * others may not.
+ *
+ * Takes the same arguments as pamk5_password_auth and returns a
+ * krb5_error_code. If successful, the credentials will be stored in creds.
+ */
+static krb5_error_code
+pkinit_auth(struct pam_args *args, const char *service, krb5_creds **creds)
+{
+ struct context *ctx = args->config->ctx;
+ krb5_get_init_creds_opt *opts = NULL;
+ krb5_error_code retval;
+ char *dummy = NULL;
+
+ /*
+ * We may not be able to dive directly into the PKINIT functions because
+ * the user may not have a chance to enter the smart card. For example,
+ * gnome-screensaver jumps into PAM as soon as the mouse is moved and
+ * expects to be prompted for a password, which may not happen if the
+ * smart card is the type that has a pad for the PIN on the card.
+ *
+ * Allow the user to set pkinit_prompt as an option. If set, we tell the
+ * user they need to insert the card.
+ *
+ * We always ignore the input. If the user wants to use a password
+ * instead, they'll be prompted later when the PKINIT code discovers that
+ * no smart card is available.
+ */
+ if (args->config->pkinit_prompt) {
+ pamk5_conv(args,
+ args->config->use_pkinit
+ ? "Insert smart card and press Enter: "
+ : "Insert smart card if desired, then press Enter: ",
+ PAM_PROMPT_ECHO_OFF, &dummy);
+ }
+
+ /*
+ * Set credential options. We have to use the allocated version of the
+ * credential option struct to store the PKINIT options.
+ */
+ *creds = calloc(1, sizeof(krb5_creds));
+ if (*creds == NULL)
+ return ENOMEM;
+ retval = krb5_get_init_creds_opt_alloc(ctx->context, &opts);
+ if (retval != 0)
+ return retval;
+ set_credential_options(args, opts, service != NULL);
+
+ /* Finally, do the actual work and return the results. */
+# ifdef HAVE_KRB5_HEIMDAL
+ retval = krb5_get_init_creds_opt_set_pkinit(
+ ctx->context, opts, ctx->princ, args->config->pkinit_user,
+ args->config->pkinit_anchors, NULL, NULL, 0, pamk5_prompter_krb5, args,
+ NULL);
+ if (retval == 0)
+ retval = krb5_get_init_creds_password(ctx->context, *creds, ctx->princ,
+ NULL, NULL, args, 0,
+ (char *) service, opts);
+# else /* !HAVE_KRB5_HEIMDAL */
+ retval = krb5_get_init_creds_password(
+ ctx->context, *creds, ctx->princ, NULL,
+ pamk5_prompter_krb5_no_password, args, 0, (char *) service, opts);
+# endif /* !HAVE_KRB5_HEIMDAL */
+
+ krb5_get_init_creds_opt_free(ctx->context, opts);
+ if (retval != 0) {
+ krb5_free_cred_contents(ctx->context, *creds);
+ free(*creds);
+ *creds = NULL;
+ }
+ return retval;
+}
+#endif
+
+
+/*
+ * Attempt authentication once with a given password. This is the core of the
+ * authentication loop, and handles alt_auth_map and search_k5login. It takes
+ * the PAM arguments, the service for which to get tickets (NULL for the
+ * default TGT), the initial credential options, and the password, and returns
+ * a Kerberos status code or errno. On success (return status 0), it stores
+ * the obtained credentials in the provided creds argument.
+ */
+static krb5_error_code
+password_auth_attempt(struct pam_args *args, const char *service,
+ krb5_get_init_creds_opt *opts, const char *pass,
+ krb5_creds *creds)
+{
+ krb5_error_code retval;
+
+ /*
+ * First, try authenticating as the alternate principal if one were
+ * configured. If that fails or wasn't configured, continue on to trying
+ * search_k5login or a regular authentication unless configuration
+ * indicates that regular authentication should not be attempted.
+ */
+ if (args->config->alt_auth_map != NULL) {
+ retval = pamk5_alt_auth(args, service, opts, pass, creds);
+ if (retval == 0)
+ return retval;
+
+ /* If only_alt_auth is set, we cannot continue. */
+ if (args->config->only_alt_auth)
+ return retval;
+
+ /*
+ * If force_alt_auth is set, skip attempting normal authentication iff
+ * the alternate principal exists.
+ */
+ if (args->config->force_alt_auth)
+ if (retval != KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN)
+ return retval;
+ }
+
+ /* Attempt regular authentication, via either search_k5login or normal. */
+ if (args->config->search_k5login)
+ retval = k5login_password_auth(args, creds, opts, service, pass);
+ else
+ retval = password_auth(args, creds, opts, service, pass);
+ if (retval != 0)
+ putil_debug_krb5(args, retval, "krb5_get_init_creds_password");
+ return retval;
+}
+
+
+/*
+ * Try to verify credentials by obtaining and checking a service ticket. This
+ * is required to verify that no one is spoofing the KDC, but requires read
+ * access to a keytab with a valid key. By default, the Kerberos library will
+ * silently succeed if no verification keys are available, but the user can
+ * change this by setting verify_ap_req_nofail in [libdefaults] in
+ * /etc/krb5.conf.
+ *
+ * The MIT Kerberos implementation of krb5_verify_init_creds hardwires the
+ * host key for the local system as the desired principal if no principal is
+ * given. If we have an explicitly configured keytab, instead read that
+ * keytab, find the first principal in that keytab, and use that.
+ *
+ * Returns a Kerberos status code (0 for success).
+ */
+static krb5_error_code
+verify_creds(struct pam_args *args, krb5_creds *creds)
+{
+ krb5_verify_init_creds_opt opts;
+ krb5_keytab keytab = NULL;
+ krb5_kt_cursor cursor;
+ int cursor_valid = 0;
+ krb5_keytab_entry entry;
+ krb5_principal princ = NULL;
+ krb5_error_code retval;
+ krb5_context c = args->config->ctx->context;
+
+ memset(&entry, 0, sizeof(entry));
+ krb5_verify_init_creds_opt_init(&opts);
+ if (args->config->keytab) {
+ retval = krb5_kt_resolve(c, args->config->keytab, &keytab);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot open keytab %s",
+ args->config->keytab);
+ keytab = NULL;
+ }
+ if (retval == 0)
+ retval = krb5_kt_start_seq_get(c, keytab, &cursor);
+ if (retval == 0) {
+ cursor_valid = 1;
+ retval = krb5_kt_next_entry(c, keytab, &entry, &cursor);
+ }
+ if (retval == 0)
+ retval = krb5_copy_principal(c, entry.principal, &princ);
+ if (retval != 0)
+ putil_err_krb5(args, retval, "error reading keytab %s",
+ args->config->keytab);
+ if (entry.principal != NULL)
+ krb5_kt_free_entry(c, &entry);
+ if (cursor_valid)
+ krb5_kt_end_seq_get(c, keytab, &cursor);
+ }
+ retval = krb5_verify_init_creds(c, creds, princ, keytab, NULL, &opts);
+ if (retval != 0)
+ putil_err_krb5(args, retval, "credential verification failed");
+ if (princ != NULL)
+ krb5_free_principal(c, princ);
+ if (keytab != NULL)
+ krb5_kt_close(c, keytab);
+ return retval;
+}
+
+
+/*
+ * Give the user a nicer error message when we've attempted PKINIT without
+ * success. We can only do this if the rich status codes are available.
+ * Currently, this only works with Heimdal.
+ */
+static void UNUSED
+report_pkinit_error(struct pam_args *args, krb5_error_code retval UNUSED)
+{
+ const char *message;
+
+#ifdef HAVE_HX509_ERR_H
+ switch (retval) {
+# ifdef HX509_PKCS11_PIN_LOCKED
+ case HX509_PKCS11_PIN_LOCKED:
+ message = "PKINIT failed: user PIN locked";
+ break;
+# endif
+# ifdef HX509_PKCS11_PIN_EXPIRED
+ case HX509_PKCS11_PIN_EXPIRED:
+ message = "PKINIT failed: user PIN expired";
+ break;
+# endif
+# ifdef HX509_PKCS11_PIN_INCORRECT
+ case HX509_PKCS11_PIN_INCORRECT:
+ message = "PKINIT failed: user PIN incorrect";
+ break;
+# endif
+# ifdef HX509_PKCS11_PIN_NOT_INITIALIZED
+ case HX509_PKCS11_PIN_NOT_INITIALIZED:
+ message = "PKINIT fialed: user PIN not initialized";
+ break;
+# endif
+ default:
+ message = "PKINIT failed";
+ break;
+ }
+#else
+ message = "PKINIT failed";
+#endif
+ pamk5_conv(args, message, PAM_TEXT_INFO, NULL);
+}
+
+
+/*
+ * Prompt the user for a password and authenticate the password with the KDC.
+ * If correct, fill in creds with the obtained TGT or ticket. service, if
+ * non-NULL, specifies the service to get tickets for; the only interesting
+ * non-null case is kadmin/changepw for changing passwords. Therefore, if it
+ * is non-null, we look for the password in PAM_OLDAUTHOK and save it there
+ * instead of using PAM_AUTHTOK.
+ */
+int
+pamk5_password_auth(struct pam_args *args, const char *service,
+ krb5_creds **creds)
+{
+ struct context *ctx;
+ krb5_get_init_creds_opt *opts = NULL;
+ krb5_error_code retval = 0;
+ int status = PAM_SUCCESS;
+ bool retry, prompt;
+ bool creds_valid = false;
+ const char *pass = NULL;
+ int authtok = (service == NULL) ? PAM_AUTHTOK : PAM_OLDAUTHTOK;
+
+ /* Sanity check and initialization. */
+ if (args->config->ctx == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+
+ /*
+ * Fill in the default principal to authenticate as. alt_auth_map or
+ * search_k5login may change this later.
+ */
+ if (ctx->princ == NULL) {
+ retval = parse_name(args);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "parse_name failed");
+ return PAM_SERVICE_ERR;
+ }
+ }
+
+ /*
+ * If PKINIT is available and we were configured to attempt it, try
+ * authenticating with PKINIT first. Otherwise, fail all authentication
+ * if PKINIT is not available and use_pkinit was set. Fake an error code
+ * that gives an approximately correct error message.
+ */
+#if defined(HAVE_KRB5_HEIMDAL) \
+ && defined(HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT)
+ if (args->config->use_pkinit || args->config->try_pkinit) {
+ retval = pkinit_auth(args, service, creds);
+ if (retval == 0)
+ goto verify;
+ putil_debug_krb5(args, retval, "PKINIT failed");
+ if (retval != HX509_PKCS11_NO_TOKEN && retval != HX509_PKCS11_NO_SLOT)
+ goto done;
+ if (retval != 0) {
+ report_pkinit_error(args, retval);
+ if (args->config->use_pkinit)
+ goto done;
+ }
+ }
+#elif defined(HAVE_KRB5_GET_PROMPT_TYPES)
+ if (args->config->use_pkinit) {
+ retval = pkinit_auth(args, service, creds);
+ if (retval == 0)
+ goto verify;
+ putil_debug_krb5(args, retval, "PKINIT failed");
+ report_pkinit_error(args, retval);
+ goto done;
+ }
+#endif
+
+ /* Allocate cred structure and set credential options. */
+ *creds = calloc(1, sizeof(krb5_creds));
+ if (*creds == NULL) {
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ status = PAM_SERVICE_ERR;
+ goto done;
+ }
+ retval = krb5_get_init_creds_opt_alloc(ctx->context, &opts);
+ if (retval != 0) {
+ putil_crit_krb5(args, retval, "cannot allocate credential options");
+ goto done;
+ }
+ set_credential_options(args, opts, service != NULL);
+
+ /*
+ * Obtain the saved password, if appropriate and available, and determine
+ * our retry strategy. If try_first_pass is set, we will prompt for a
+ * password and retry the authentication if the stored password didn't
+ * work.
+ */
+ status = maybe_retrieve_password(args, authtok, &pass);
+ if (status != PAM_SUCCESS)
+ goto done;
+
+ /*
+ * Main authentication loop.
+ *
+ * If we had no stored password, we prompt for a password the first time
+ * through. If try_first_pass is set and we had an old password, we try
+ * with it. If the old password doesn't work, we loop once, prompt for a
+ * password, and retry. If use_first_pass is set, we'll prompt once if
+ * the password isn't already set but won't retry.
+ *
+ * If we don't have a password but try_pkinit or no_prompt are true, we
+ * don't attempt to prompt for a password and we go into the Kerberos
+ * libraries with no password. We rely on the Kerberos libraries to do
+ * the prompting if PKINIT fails. In this case, make sure we don't retry.
+ * Be aware that in this case, we also have no way of saving whatever
+ * password or other credentials the user might enter, so subsequent PAM
+ * modules will not see a stored authtok.
+ *
+ * We've already handled empty passwords in our other functions.
+ */
+ retry = args->config->try_first_pass;
+ prompt = !(args->config->try_pkinit || args->config->no_prompt);
+ do {
+ if (pass == NULL)
+ retry = false;
+ if (pass == NULL && prompt) {
+ status = prompt_password(args, authtok, &pass);
+ if (status != PAM_SUCCESS)
+ goto done;
+ }
+
+ /*
+ * Attempt authentication. If we succeeded, we're done. Otherwise,
+ * clear the password and then see if we should try again after
+ * prompting for a password.
+ */
+ retval = password_auth_attempt(args, service, opts, pass, *creds);
+ if (retval == 0) {
+ creds_valid = true;
+ break;
+ }
+ pass = NULL;
+ } while (retry
+ && (retval == KRB5KRB_AP_ERR_BAD_INTEGRITY
+ || retval == KRB5KRB_AP_ERR_MODIFIED
+ || retval == KRB5KDC_ERR_PREAUTH_FAILED
+ || retval == KRB5_GET_IN_TKT_LOOP
+ || retval == KRB5_BAD_ENCTYPE));
+
+verify:
+ UNUSED
+ /*
+ * If we think we succeeded, whether through the regular path or via
+ * PKINIT, try to verify the credentials. Don't do this if we're
+ * authenticating for password changes (or any other case where we're not
+ * getting a TGT). We can't get a service ticket from a kadmin/changepw
+ * ticket.
+ */
+ if (retval == 0 && service == NULL)
+ retval = verify_creds(args, *creds);
+
+done:
+ /*
+ * Free resources, including any credentials we have sitting around if we
+ * failed, and return the appropriate PAM error code. If status is
+ * already set to something other than PAM_SUCCESS, we encountered a PAM
+ * error and will just return that code. Otherwise, we need to map the
+ * Kerberos status code in retval to a PAM error code.
+ */
+ if (status == PAM_SUCCESS) {
+ switch (retval) {
+ case 0:
+ status = PAM_SUCCESS;
+ break;
+ case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
+ status = PAM_USER_UNKNOWN;
+ break;
+ case KRB5KDC_ERR_KEY_EXP:
+ status = PAM_NEW_AUTHTOK_REQD;
+ break;
+ case KRB5KDC_ERR_NAME_EXP:
+ status = PAM_ACCT_EXPIRED;
+ break;
+ case KRB5_KDC_UNREACH:
+ case KRB5_LIBOS_CANTREADPWD:
+ case KRB5_REALM_CANT_RESOLVE:
+ case KRB5_REALM_UNKNOWN:
+ status = PAM_AUTHINFO_UNAVAIL;
+ break;
+ default:
+ status = PAM_AUTH_ERR;
+ break;
+ }
+ }
+ if (status != PAM_SUCCESS && *creds != NULL) {
+ if (creds_valid)
+ krb5_free_cred_contents(ctx->context, *creds);
+ free(*creds);
+ *creds = NULL;
+ }
+ if (opts != NULL)
+ krb5_get_init_creds_opt_free(ctx->context, opts);
+
+ /* Whatever the results, destroy the anonymous FAST cache. */
+ if (ctx->fast_cache != NULL) {
+ krb5_cc_destroy(ctx->context, ctx->fast_cache);
+ ctx->fast_cache = NULL;
+ }
+ return status;
+}
+
+
+/*
+ * Authenticate a user via Kerberos.
+ *
+ * It would be nice to be able to save the ticket cache temporarily as a
+ * memory cache and then only write it out to disk during the session
+ * initialization. Unfortunately, OpenSSH 4.2 and later do PAM authentication
+ * in a subprocess and therefore has no saved module-specific data available
+ * once it opens a session, so we have to save the ticket cache to disk and
+ * store in the environment where it is. The alternative is to use something
+ * like System V shared memory, which seems like more trouble than it's worth.
+ */
+int
+pamk5_authenticate(struct pam_args *args)
+{
+ struct context *ctx = NULL;
+ krb5_creds *creds = NULL;
+ char *pass = NULL;
+ char *principal;
+ int pamret;
+ bool set_context = false;
+ krb5_error_code retval;
+
+ /* Temporary backward compatibility. */
+ if (args->config->use_authtok && !args->config->force_first_pass) {
+ putil_err(args, "use_authtok option in authentication group should"
+ " be changed to force_first_pass");
+ args->config->force_first_pass = true;
+ }
+
+ /* Create a context and obtain the user. */
+ pamret = pamk5_context_new(args);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ ctx = args->config->ctx;
+
+ /* Check whether we should ignore this user. */
+ if (pamk5_should_ignore(args, ctx->name)) {
+ pamret = PAM_USER_UNKNOWN;
+ goto done;
+ }
+
+ /*
+ * Do the actual authentication.
+ *
+ * The complexity arises if the password was expired (which means the
+ * Kerberos library was also unable to prompt for the password change
+ * internally). In that case, there are three possibilities:
+ * fail_pwchange says we treat that as an authentication failure and stop,
+ * defer_pwchange says to set a flag that will result in an error at the
+ * acct_mgmt step, and force_pwchange says that we should change the
+ * password here and now.
+ *
+ * defer_pwchange is the formally correct behavior. Set a flag in the
+ * context and return success. That flag will later be checked by
+ * pam_sm_acct_mgmt. We need to set the context as PAM data in the
+ * defer_pwchange case, but we don't want to set the PAM data until we've
+ * checked .k5login. If we've stacked multiple pam-krb5 invocations in
+ * different realms as optional, we don't want to override a previous
+ * successful authentication.
+ *
+ * Note this means that, if the user can authenticate with multiple realms
+ * and authentication succeeds in one realm and is then expired in a later
+ * realm, the expiration in the latter realm wins. This isn't ideal, but
+ * avoiding that case is more complicated than it's worth.
+ *
+ * We would like to set the current password as PAM_OLDAUTHTOK so that
+ * when the application subsequently calls pam_chauthtok, the user won't
+ * be reprompted. However, the PAM library clears all the auth tokens
+ * when pam_authenticate exits, so this isn't possible.
+ *
+ * In the force_pwchange case, try to use the password the user just
+ * entered to authenticate to the password changing service, but don't
+ * throw an error if that doesn't work. We have to move it from
+ * PAM_AUTHTOK to PAM_OLDAUTHTOK to be in the place where password
+ * changing expects, and have to unset PAM_AUTHTOK or we'll just change
+ * the password to the same thing it was.
+ */
+ pamret = pamk5_password_auth(args, NULL, &creds);
+ if (pamret == PAM_NEW_AUTHTOK_REQD) {
+ if (args->config->fail_pwchange)
+ pamret = PAM_AUTH_ERR;
+ else if (args->config->defer_pwchange) {
+ putil_debug(args, "expired account, deferring failure");
+ ctx->expired = 1;
+ pamret = PAM_SUCCESS;
+ } else if (args->config->force_pwchange) {
+ pam_syslog(args->pamh, LOG_INFO,
+ "user %s password expired, forcing password change",
+ ctx->name);
+ pamk5_conv(args, "Password expired. You must change it now.",
+ PAM_TEXT_INFO, NULL);
+ pamret = pam_get_item(args->pamh, PAM_AUTHTOK,
+ (PAM_CONST void **) &pass);
+ if (pamret == PAM_SUCCESS && pass != NULL)
+ pam_set_item(args->pamh, PAM_OLDAUTHTOK, pass);
+ pam_set_item(args->pamh, PAM_AUTHTOK, NULL);
+ args->config->use_first_pass = true;
+ pamret = pamk5_password_change(args, false);
+ if (pamret == PAM_SUCCESS)
+ putil_debug(args, "successfully changed expired password");
+ }
+ }
+ if (pamret != PAM_SUCCESS) {
+ putil_log_failure(args, "authentication failure");
+ goto done;
+ }
+
+ /* Check .k5login and alt_auth_map. */
+ pamret = pamk5_authorized(args);
+ if (pamret != PAM_SUCCESS) {
+ putil_log_failure(args, "failed authorization check");
+ goto done;
+ }
+
+ /* Reset PAM_USER in case we canonicalized, but ignore errors. */
+ if (!ctx->expired && !args->config->no_update_user) {
+ pamret = pam_set_item(args->pamh, PAM_USER, ctx->name);
+ if (pamret != PAM_SUCCESS)
+ putil_err_pam(args, pamret, "cannot set PAM_USER");
+ }
+
+ /* Log the successful authentication. */
+ retval = krb5_unparse_name(ctx->context, ctx->princ, &principal);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "krb5_unparse_name failed");
+ pam_syslog(args->pamh, LOG_INFO, "user %s authenticated as UNKNOWN",
+ ctx->name);
+ } else {
+ pam_syslog(args->pamh, LOG_INFO, "user %s authenticated as %s%s",
+ ctx->name, principal, ctx->expired ? " (expired)" : "");
+ krb5_free_unparsed_name(ctx->context, principal);
+ }
+
+ /* Now that we know we're successful, we can store the context. */
+ pamret = pam_set_data(args->pamh, "pam_krb5", ctx, pamk5_context_destroy);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "cannot set context data");
+ pamk5_context_free(args);
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ set_context = true;
+
+ /*
+ * If we have an expired account or if we're not creating a ticket cache,
+ * we're done. Otherwise, store the obtained credentials in a temporary
+ * cache.
+ */
+ if (!args->config->no_ccache && !ctx->expired)
+ pamret = pamk5_cache_init_random(args, creds);
+
+done:
+ if (creds != NULL && ctx != NULL) {
+ krb5_free_cred_contents(ctx->context, creds);
+ free(creds);
+ }
+
+ /*
+ * Don't free our Kerberos context if we set a context, since the context
+ * will take care of that.
+ */
+ if (set_context)
+ args->ctx = NULL;
+
+ /*
+ * Clear the context on failure so that the account management module
+ * knows that we didn't authenticate with Kerberos. Only clear the
+ * context if we set it. Otherwise, we may be blowing away the context of
+ * a previous successful authentication.
+ */
+ if (pamret != PAM_SUCCESS) {
+ if (set_context)
+ pam_set_data(args->pamh, "pam_krb5", NULL, NULL);
+ else
+ pamk5_context_free(args);
+ }
+ return pamret;
+}
diff --git a/module/cache.c b/module/cache.c
new file mode 100644
index 000000000000..7acfef07b8eb
--- /dev/null
+++ b/module/cache.c
@@ -0,0 +1,185 @@
+/*
+ * Ticket cache initialization.
+ *
+ * Provides functions for creating ticket caches, used by pam_authenticate,
+ * pam_setcred, and pam_chauthtok after changing an expired password.
+ *
+ * Copyright 2005-2009, 2014, 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 <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Get the name of a cache. Takes the name of the environment variable that
+ * should be set to indicate which cache to use, either the permanent cache
+ * (KRB5CCNAME) or the temporary cache (PAM_KRB5CCNAME).
+ *
+ * Treat an empty environment variable setting the same as if the variable
+ * was not set, since on FreeBSD we can't delete the environment variable,
+ * only set it to an empty value.
+ */
+const char *
+pamk5_get_krb5ccname(struct pam_args *args, const char *key)
+{
+ const char *name;
+
+ /* When refreshing a cache, we need to try the regular environment. */
+ name = pam_getenv(args->pamh, key);
+ if (name == NULL || *name == '\0')
+ name = getenv(key);
+ if (name == NULL || *name == '\0')
+ return NULL;
+ else
+ return name;
+}
+
+
+/*
+ * Put the ticket cache information into the environment. Takes the path and
+ * the environment variable to set, since this is used both for the permanent
+ * cache (KRB5CCNAME) and the temporary cache (PAM_KRB5CCNAME). Returns a PAM
+ * status code.
+ */
+int
+pamk5_set_krb5ccname(struct pam_args *args, const char *name, const char *key)
+{
+ char *env_name = NULL;
+ int pamret;
+
+ if (asprintf(&env_name, "%s=%s", key, name) < 0) {
+ putil_crit(args, "asprintf failed: %s", strerror(errno));
+ pamret = PAM_BUF_ERR;
+ goto done;
+ }
+ pamret = pam_putenv(args->pamh, env_name);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "pam_putenv failed");
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ pamret = PAM_SUCCESS;
+
+done:
+ free(env_name);
+ return pamret;
+}
+
+
+/*
+ * Given the template for a ticket cache name, initialize that file securely
+ * mkstemp. Returns a PAM success or error code.
+ */
+int
+pamk5_cache_mkstemp(struct pam_args *args, char *template)
+{
+ int ccfd, oerrno;
+
+ ccfd = mkstemp(template);
+ if (ccfd < 0) {
+ oerrno = errno;
+ putil_crit(args, "mkstemp(\"%s\") failed: %s", template,
+ strerror(errno));
+ errno = oerrno;
+ return PAM_SERVICE_ERR;
+ }
+ close(ccfd);
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Given a cache name and the initial credentials, initialize the cache, store
+ * the credentials in that cache, and return a pointer to the new cache in the
+ * cache argument. Returns a PAM success or error code.
+ */
+int
+pamk5_cache_init(struct pam_args *args, const char *ccname, krb5_creds *creds,
+ krb5_ccache *cache)
+{
+ struct context *ctx;
+ int retval;
+
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL
+ || args->config->ctx->context == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+ retval = krb5_cc_resolve(ctx->context, ccname, cache);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot resolve ticket cache %s", ccname);
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+ retval = krb5_cc_initialize(ctx->context, *cache, ctx->princ);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot initialize ticket cache %s",
+ ccname);
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+ retval = krb5_cc_store_cred(ctx->context, *cache, creds);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot store credentials in %s", ccname);
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+
+done:
+ if (retval != PAM_SUCCESS && *cache != NULL) {
+ krb5_cc_destroy(ctx->context, *cache);
+ *cache = NULL;
+ }
+ return retval;
+}
+
+
+/*
+ * Initialize an internal ticket cache with a random name, store the given
+ * credentials in the cache, and store the cache in the context. Put the path
+ * in PAM_KRB5CCNAME where it can be picked up later by pam_setcred. Returns
+ * a PAM success or error code.
+ */
+int
+pamk5_cache_init_random(struct pam_args *args, krb5_creds *creds)
+{
+ char *cache_name = NULL;
+ const char *dir;
+ int pamret;
+
+ /* Store the obtained credentials in a temporary cache. */
+ dir = args->config->ccache_dir;
+ if (strncmp("FILE:", args->config->ccache_dir, strlen("FILE:")) == 0)
+ dir += strlen("FILE:");
+ if (asprintf(&cache_name, "%s/krb5cc_pam_XXXXXX", dir) < 0) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ return PAM_SERVICE_ERR;
+ }
+ pamret = pamk5_cache_mkstemp(args, cache_name);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ pamret =
+ pamk5_cache_init(args, cache_name, creds, &args->config->ctx->cache);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ putil_debug(args, "temporarily storing credentials in %s", cache_name);
+ pamret = pamk5_set_krb5ccname(args, cache_name, "PAM_KRB5CCNAME");
+
+done:
+ free(cache_name);
+ return pamret;
+}
diff --git a/module/context.c b/module/context.c
new file mode 100644
index 000000000000..bd90f51f5549
--- /dev/null
+++ b/module/context.c
@@ -0,0 +1,177 @@
+/*
+ * Manage context structure.
+ *
+ * The context structure is the internal state maintained by the pam-krb5
+ * module between calls to the various public interfaces.
+ *
+ * Copyright 2005-2009, 2014, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * 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/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Create a new context and populate it with the user from PAM and the current
+ * Kerberos context. Set the default realm if one was configured.
+ */
+int
+pamk5_context_new(struct pam_args *args)
+{
+ struct context *ctx;
+ int retval;
+ PAM_CONST char *name;
+
+ ctx = calloc(1, sizeof(struct context));
+ if (ctx == NULL) {
+ retval = PAM_BUF_ERR;
+ goto done;
+ }
+ ctx->cache = NULL;
+ ctx->princ = NULL;
+ ctx->creds = NULL;
+ ctx->fast_cache = NULL;
+ ctx->context = args->ctx;
+ args->config->ctx = ctx;
+
+ /*
+ * This will prompt for the username if it's not already set (generally it
+ * will be). Otherwise, grab the saved username.
+ */
+ retval = pam_get_user(args->pamh, &name, NULL);
+ if (retval != PAM_SUCCESS || name == NULL) {
+ if (retval == PAM_CONV_AGAIN)
+ retval = PAM_INCOMPLETE;
+ else
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ctx->name = strdup(name);
+ args->user = ctx->name;
+
+ /* Set a default realm if one was configured. */
+ if (args->realm != NULL) {
+ retval = krb5_set_default_realm(ctx->context, args->realm);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot set default realm");
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+ }
+
+done:
+ if (ctx != NULL && retval != PAM_SUCCESS)
+ pamk5_context_free(args);
+ return retval;
+}
+
+
+/*
+ * Retrieve a context from the PAM data structures, returning failure if no
+ * context was present. Note that OpenSSH loses contexts between authenticate
+ * and setcred, so failure shouldn't always be fatal.
+ */
+int
+pamk5_context_fetch(struct pam_args *args)
+{
+ int pamret;
+
+ pamret = pam_get_data(args->pamh, "pam_krb5", (void *) &args->config->ctx);
+ if (pamret != PAM_SUCCESS)
+ args->config->ctx = NULL;
+ if (pamret == PAM_SUCCESS && args->config->ctx == NULL)
+ return PAM_SERVICE_ERR;
+ if (args->config->ctx != NULL)
+ args->user = args->config->ctx->name;
+ return pamret;
+}
+
+
+/*
+ * Free a context and all of the data that's stored in it. Normally this also
+ * includes destroying the ticket cache, but don't do this (just close it) if
+ * a flag was set to preserve it.
+ *
+ * This function is common code between pamk5_context_free (called internally
+ * by our code) and pamk5_context_destroy (called by PAM as a data callback).
+ */
+static void
+context_free(struct context *ctx, bool free_context)
+{
+ if (ctx == NULL)
+ return;
+ free(ctx->name);
+ if (ctx->context != NULL) {
+ if (ctx->princ != NULL)
+ krb5_free_principal(ctx->context, ctx->princ);
+ if (ctx->cache != NULL) {
+ if (ctx->dont_destroy_cache)
+ krb5_cc_close(ctx->context, ctx->cache);
+ else
+ krb5_cc_destroy(ctx->context, ctx->cache);
+ }
+ if (ctx->creds != NULL) {
+ krb5_free_cred_contents(ctx->context, ctx->creds);
+ free(ctx->creds);
+ }
+ if (free_context)
+ krb5_free_context(ctx->context);
+ }
+ if (ctx->fast_cache != NULL)
+ krb5_cc_destroy(ctx->context, ctx->fast_cache);
+ free(ctx);
+}
+
+
+/*
+ * Free the current context, used internally by pam-krb5 code. This is a
+ * wrapper around context_free that makes sure we don't destroy the Kerberos
+ * context if it's the same as the top-level context and handles other
+ * bookkeeping in the top-level pam_args struct.
+ */
+void
+pamk5_context_free(struct pam_args *args)
+{
+ if (args->config->ctx == NULL)
+ return;
+ if (args->user == args->config->ctx->name)
+ args->user = NULL;
+ context_free(args->config->ctx, args->ctx != args->config->ctx->context);
+ args->config->ctx = NULL;
+}
+
+
+/*
+ * The PAM callback to destroy the context stored in the PAM data structures.
+ */
+void
+pamk5_context_destroy(pam_handle_t *pamh UNUSED, void *data,
+ int pam_end_status)
+{
+ struct context *ctx = (struct context *) data;
+
+ /*
+ * Do not destroy the cache if the status contains PAM_DATA_SILENT, since
+ * in that case we may be in a child and the parent will still rely on
+ * underlying resources such as the ticket cache to exist.
+ */
+ if (PAM_DATA_SILENT != 0 && (pam_end_status & PAM_DATA_SILENT))
+ ctx->dont_destroy_cache = true;
+
+ /* The rest of the work is in context_free. */
+ if (ctx != NULL)
+ context_free(ctx, true);
+}
diff --git a/module/fast.c b/module/fast.c
new file mode 100644
index 000000000000..466199977fad
--- /dev/null
+++ b/module/fast.c
@@ -0,0 +1,288 @@
+/*
+ * Support for FAST (Flexible Authentication Secure Tunneling).
+ *
+ * FAST is a mechanism to protect Kerberos against password guessing attacks
+ * and provide other security improvements. It requires existing credentials
+ * to protect the initial preauthentication exchange. These can come either
+ * from a ticket cache for another principal or via anonymous PKINIT.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Contributions from Sam Hartman and Yair Yarom
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010, 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Initialize an internal anonymous ticket cache with a random name and store
+ * the resulting ticket cache in the ccache argument. Returns a Kerberos
+ * error code.
+ */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ANONYMOUS
+
+static krb5_error_code
+cache_init_anonymous(struct pam_args *args, krb5_ccache *ccache UNUSED)
+{
+ putil_debug(args, "not built with anonymous FAST support");
+ return KRB5KDC_ERR_BADOPTION;
+}
+
+#else /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ANONYMOUS */
+
+static krb5_error_code
+cache_init_anonymous(struct pam_args *args, krb5_ccache *ccache)
+{
+ krb5_context c = args->config->ctx->context;
+ krb5_error_code retval;
+ krb5_principal princ = NULL;
+ char *realm;
+ char *name = NULL;
+ krb5_creds creds;
+ bool creds_valid = false;
+ krb5_get_init_creds_opt *opts = NULL;
+
+ *ccache = NULL;
+ memset(&creds, 0, sizeof(creds));
+
+ /* Construct the anonymous principal name. */
+ retval = krb5_get_default_realm(c, &realm);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "cannot find realm for anonymous FAST");
+ return retval;
+ }
+ retval = krb5_build_principal_ext(
+ c, &princ, (unsigned int) strlen(realm), realm,
+ strlen(KRB5_WELLKNOWN_NAME), KRB5_WELLKNOWN_NAME,
+ strlen(KRB5_ANON_NAME), KRB5_ANON_NAME, NULL);
+ if (retval != 0) {
+ krb5_free_default_realm(c, realm);
+ putil_debug_krb5(args, retval, "cannot create anonymous principal");
+ return retval;
+ }
+ krb5_free_default_realm(c, realm);
+
+ /*
+ * Set up the credential cache the anonymous credentials. We use a
+ * memory cache whose name is based on the pointer value of our Kerberos
+ * context, since that should be unique among threads.
+ */
+ if (asprintf(&name, "MEMORY:%p", (void *) c) < 0) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ retval = errno;
+ goto done;
+ }
+ retval = krb5_cc_resolve(c, name, ccache);
+ if (retval != 0) {
+ putil_err_krb5(args, retval,
+ "cannot create anonymous FAST credential cache %s",
+ name);
+ goto done;
+ }
+
+ /* Obtain the credentials. */
+ retval = krb5_get_init_creds_opt_alloc(c, &opts);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot create FAST credential options");
+ goto done;
+ }
+ krb5_get_init_creds_opt_set_anonymous(opts, 1);
+ krb5_get_init_creds_opt_set_tkt_life(opts, 60);
+# ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_OUT_CCACHE
+ krb5_get_init_creds_opt_set_out_ccache(c, opts, *ccache);
+# endif
+ retval = krb5_get_init_creds_password(c, &creds, princ, NULL, NULL, NULL,
+ 0, NULL, opts);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval,
+ "cannot obtain anonymous credentials for FAST");
+ goto done;
+ }
+ creds_valid = true;
+
+ /*
+ * If set_out_ccache was available, we're done. Otherwise, we have to
+ * manually set up the ticket cache. Use the principal from the acquired
+ * credentials when initializing the ticket cache, since the realm will
+ * not match the realm of our input principal.
+ */
+# ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_OUT_CCACHE
+ retval = krb5_cc_initialize(c, *ccache, creds.client);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot initialize FAST ticket cache");
+ goto done;
+ }
+ retval = krb5_cc_store_cred(c, *ccache, &creds);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot store FAST credentials");
+ goto done;
+ }
+# endif /* !HAVE_KRB5_GET_INIT_CREDS_OPT_SET_OUT_CCACHE */
+
+done:
+ if (retval != 0 && *ccache != NULL) {
+ krb5_cc_destroy(c, *ccache);
+ *ccache = NULL;
+ }
+ if (princ != NULL)
+ krb5_free_principal(c, princ);
+ free(name);
+ if (opts != NULL)
+ krb5_get_init_creds_opt_free(c, opts);
+ if (creds_valid)
+ krb5_free_cred_contents(c, &creds);
+ return retval;
+}
+#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ANONYMOUS */
+
+
+/*
+ * Attempt to use an existing ticket cache for FAST. Checks whether
+ * fast_ccache is set in the options and, if so, opens that cache and does
+ * some sanity checks, returning the cache name to use if everything checks
+ * out in newly allocated memory. Caller is responsible for freeing. If not,
+ * returns NULL.
+ */
+UNUSED static char *
+fast_setup_cache(struct pam_args *args)
+{
+ krb5_context c = args->config->ctx->context;
+ krb5_error_code retval;
+ krb5_principal princ;
+ krb5_ccache ccache;
+ char *result;
+ const char *cache = args->config->fast_ccache;
+
+ if (cache == NULL)
+ return NULL;
+ retval = krb5_cc_resolve(c, cache, &ccache);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "cannot open FAST ccache %s", cache);
+ return NULL;
+ }
+ retval = krb5_cc_get_principal(c, ccache, &princ);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval,
+ "failed to get principal from FAST"
+ " ccache %s",
+ cache);
+ krb5_cc_close(c, ccache);
+ return NULL;
+ } else {
+ krb5_free_principal(c, princ);
+ krb5_cc_close(c, ccache);
+ result = strdup(cache);
+ if (result == NULL)
+ putil_crit(args, "strdup failure: %s", strerror(errno));
+ return result;
+ }
+}
+
+
+/*
+ * Attempt to use an anonymous ticket cache for FAST. Checks whether
+ * anon_fast is set in the options and, if so, opens that cache and does some
+ * sanity checks, returning the cache name to use if everything checks out in
+ * newly allocated memory. Caller is responsible for freeing. If not,
+ * returns NULL.
+ *
+ * If successful, store the anonymous FAST cache in the context where it will
+ * be freed following authentication.
+ */
+UNUSED static char *
+fast_setup_anon(struct pam_args *args)
+{
+ krb5_context c = args->config->ctx->context;
+ krb5_error_code retval;
+ krb5_ccache ccache;
+ char *cache, *result;
+
+ if (!args->config->anon_fast)
+ return NULL;
+ retval = cache_init_anonymous(args, &ccache);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "skipping anonymous FAST");
+ return NULL;
+ }
+ retval = krb5_cc_get_full_name(c, ccache, &cache);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval,
+ "cannot get name of anonymous FAST"
+ " credential cache");
+ krb5_cc_destroy(c, ccache);
+ return NULL;
+ }
+ result = strdup(cache);
+ if (result == NULL) {
+ putil_crit(args, "strdup failure: %s", strerror(errno));
+ krb5_cc_destroy(c, ccache);
+ }
+ krb5_free_string(c, cache);
+ putil_debug(args, "anonymous authentication for FAST succeeded");
+ if (args->config->ctx->fast_cache != NULL)
+ krb5_cc_destroy(c, args->config->ctx->fast_cache);
+ args->config->ctx->fast_cache = ccache;
+ return result;
+}
+
+
+/*
+ * Set initial credential options for FAST if support is available.
+ *
+ * If fast_ccache is set, we try to use that ticket cache first. Open it and
+ * read the principal from it first to ensure that the cache exists and
+ * contains credentials. If that fails, skip setting the FAST cache.
+ *
+ * If anon_fast is set and fast_ccache is not or is skipped for the reasons
+ * described above, try to obtain anonymous credentials and then use them as
+ * FAST armor.
+ *
+ * Note that this function cannot fail. If anything about FAST setup doesn't
+ * work, we continue without FAST.
+ */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
+
+void
+pamk5_fast_setup(struct pam_args *args UNUSED,
+ krb5_get_init_creds_opt *opts UNUSED)
+{
+}
+
+#else /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME */
+
+void
+pamk5_fast_setup(struct pam_args *args, krb5_get_init_creds_opt *opts)
+{
+ krb5_context c = args->config->ctx->context;
+ krb5_error_code retval;
+ char *cache;
+
+ /* First try to use fast_ccache, and then fall back on anon_fast. */
+ cache = fast_setup_cache(args);
+ if (cache == NULL)
+ cache = fast_setup_anon(args);
+ if (cache == NULL)
+ return;
+
+ /* We have a valid FAST ticket cache. Set the option. */
+ retval = krb5_get_init_creds_opt_set_fast_ccache_name(c, opts, cache);
+ if (retval != 0)
+ putil_err_krb5(args, retval, "failed to set FAST ccache");
+ else
+ putil_debug(args, "setting FAST credential cache to %s", cache);
+ free(cache);
+}
+
+#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME */
diff --git a/module/internal.h b/module/internal.h
new file mode 100644
index 000000000000..f3d832a17248
--- /dev/null
+++ b/module/internal.h
@@ -0,0 +1,261 @@
+/*
+ * Internal prototypes and structures for pam-krb5.
+ *
+ * Copyright 2005-2009, 2014, 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+
+ */
+
+#ifndef INTERNAL_H
+#define INTERNAL_H 1
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/macros.h>
+#include <portable/pam.h>
+
+#include <stdarg.h>
+#include <syslog.h>
+
+/* Forward declarations to avoid unnecessary includes. */
+struct pam_args;
+struct passwd;
+struct vector;
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+/*
+ * An authentication context, including all the data we want to preserve
+ * across calls to the public entry points. This context is stored in the PAM
+ * state and a pointer to it is stored in the pam_args struct that is passed
+ * as the first argument to most internal functions.
+ */
+struct context {
+ char *name; /* Username being authenticated. */
+ krb5_context context; /* Kerberos context. */
+ krb5_ccache cache; /* Active credential cache, if any. */
+ krb5_principal princ; /* Principal being authenticated. */
+ int expired; /* If set, account was expired. */
+ int dont_destroy_cache; /* If set, don't destroy cache on shutdown. */
+ int initialized; /* If set, ticket cache initialized. */
+ krb5_creds *creds; /* Credentials for password changing. */
+ krb5_ccache fast_cache; /* Temporary credential cache for FAST. */
+};
+
+/*
+ * The global structure holding our arguments, both from krb5.conf and from
+ * the PAM configuration. Filled in by pamk5_init and stored in the pam_args
+ * struct passed as a first argument to most internal functions. Sort by
+ * documentation order.
+ */
+struct pam_config {
+ /* Authorization. */
+ char *alt_auth_map; /* An sprintf pattern to map principals. */
+ bool force_alt_auth; /* Alt principal must be used if it exists. */
+ bool ignore_k5login; /* Don't check .k5login files. */
+ bool ignore_root; /* Skip authentication for root. */
+ long minimum_uid; /* Ignore users below this UID. */
+ bool only_alt_auth; /* Alt principal must be used. */
+ bool search_k5login; /* Try password with each line of .k5login. */
+
+ /* Kerberos behavior. */
+ char *fast_ccache; /* Cache containing armor ticket. */
+ bool anon_fast; /* sets up an anonymous fast armor cache */
+ bool forwardable; /* Obtain forwardable tickets. */
+ char *keytab; /* Keytab for credential validation. */
+ char *realm; /* Default realm for Kerberos. */
+ krb5_deltat renew_lifetime; /* Renewable lifetime of credentials. */
+ krb5_deltat ticket_lifetime; /* Lifetime of credentials. */
+ char *user_realm; /* Default realm for user principals. */
+
+ /* PAM behavior. */
+ bool clear_on_fail; /* Delete saved password on change failure. */
+ bool debug; /* Log debugging information. */
+ bool defer_pwchange; /* Defer expired account fail to account. */
+ bool fail_pwchange; /* Treat expired password as auth failure. */
+ bool force_pwchange; /* Change expired passwords in auth. */
+ bool no_update_user; /* Don't update PAM_USER with local name. */
+ bool silent; /* Suppress text and errors (PAM_SILENT). */
+ char *trace; /* File name for trace logging. */
+
+ /* PKINIT. */
+ char *pkinit_anchors; /* Trusted certificates, usually per realm. */
+ bool pkinit_prompt; /* Prompt user to insert smart card. */
+ char *pkinit_user; /* User ID to pass to PKINIT. */
+ struct vector *preauth_opt; /* Preauth options. */
+ bool try_pkinit; /* Attempt PKINIT, fall back to password. */
+ bool use_pkinit; /* Require PKINIT. */
+
+ /* Prompting. */
+ char *banner; /* Addition to password changing prompts. */
+ bool expose_account; /* Display principal in password prompts. */
+ bool force_first_pass; /* Require a previous password be stored. */
+ bool no_prompt; /* Let Kerberos handle password prompting. */
+ bool prompt_principal; /* Prompt for the Kerberos principal. */
+ bool try_first_pass; /* Try the previously entered password. */
+ bool use_authtok; /* Use the stored new password for changes. */
+ bool use_first_pass; /* Always use the previous password. */
+
+ /* Ticket caches. */
+ char *ccache; /* Path to write ticket cache to. */
+ char *ccache_dir; /* Directory for ticket cache. */
+ bool no_ccache; /* Don't create a ticket cache. */
+ bool retain_after_close; /* Don't destroy the cache on session end. */
+
+ /* The authentication context, which bundles together Kerberos data. */
+ struct context *ctx;
+};
+
+/* Default to a hidden visibility for all internal functions. */
+#pragma GCC visibility push(hidden)
+
+/* Parse the PAM flags, arguments, and krb5.conf and fill out pam_args. */
+struct pam_args *pamk5_init(pam_handle_t *, int flags, int, const char **);
+
+/* Free the pam_args struct when we're done. */
+void pamk5_free(struct pam_args *);
+
+/*
+ * The underlying functions between several of the major PAM interfaces.
+ */
+int pamk5_account(struct pam_args *);
+int pamk5_authenticate(struct pam_args *);
+
+/*
+ * The underlying function below pam_sm_chauthtok. If the second argument is
+ * true, we're doing the preliminary check and shouldn't actually change the
+ * password.
+ */
+int pamk5_password(struct pam_args *, bool only_auth);
+
+/*
+ * Create or refresh the user's ticket cache. This is the underlying function
+ * beneath pam_sm_setcred and pam_sm_open_session.
+ */
+int pamk5_setcred(struct pam_args *, bool refresh);
+
+/*
+ * Authenticate the user. Prompts for the password as needed and obtains
+ * tickets for in_tkt_service, krbtgt/<realm> by default. Stores the initial
+ * credentials in the final argument, allocating a new krb5_creds structure.
+ * If possible, the initial credentials are verified by checking them against
+ * the local system key.
+ */
+int pamk5_password_auth(struct pam_args *, const char *service, krb5_creds **);
+
+/*
+ * Prompt the user for a new password, twice so that they can confirm. Sets
+ * PAM_AUTHTOK and puts the new password in newly allocated memory in pass if
+ * it's not NULL.
+ */
+int pamk5_password_prompt(struct pam_args *, char **pass);
+
+/*
+ * Change the user's password. Prompts for the current password as needed and
+ * the new password. If the second argument is true, only obtains the
+ * necessary credentials without changing anything.
+ */
+int pamk5_password_change(struct pam_args *, bool only_auth);
+
+/*
+ * Generic conversation function to display messages or get information from
+ * the user. Takes the message, the message type, and a place to put the
+ * result of a prompt.
+ */
+int pamk5_conv(struct pam_args *, const char *, int, char **);
+
+/*
+ * Function specifically for getting a password. Takes a prefix (if non-NULL,
+ * args->banner will also be prepended) and a pointer into which to store the
+ * password. The password must be freed by the caller.
+ */
+int pamk5_get_password(struct pam_args *, const char *, char **);
+
+/* Prompting function for the Kerberos libraries. */
+krb5_error_code pamk5_prompter_krb5(krb5_context, void *data, const char *name,
+ const char *banner, int, krb5_prompt *);
+
+/* Prompting function that doesn't allow passwords. */
+krb5_error_code pamk5_prompter_krb5_no_password(krb5_context, void *data,
+ const char *name,
+ const char *banner, int,
+ krb5_prompt *);
+
+/* Check the user with krb5_kuserok or the configured equivalent. */
+int pamk5_authorized(struct pam_args *);
+
+/* Returns true if we should ignore this user (root or low UID). */
+int pamk5_should_ignore(struct pam_args *, PAM_CONST char *);
+
+/*
+ * alt_auth_map support.
+ *
+ * pamk5_map_principal attempts to map the user to a Kerberos principal
+ * according to alt_auth_map. Returns 0 on success, storing the mapped
+ * principal name in newly allocated memory in principal. The caller is
+ * responsiple for freeing. Returns an errno value on any error.
+ *
+ * pamk5_alt_auth attempts an authentication to the given service with the
+ * given options and password and returns a Kerberos error code. On success,
+ * the new credentials are stored in krb5_creds.
+ *
+ * pamk5_alt_auth_verify verifies that Kerberos credentials are authorized to
+ * access the account given the configured alt_auth_map and is meant to be
+ * called from pamk5_authorized. It returns a PAM status code.
+ */
+int pamk5_map_principal(struct pam_args *, const char *username,
+ char **principal);
+krb5_error_code pamk5_alt_auth(struct pam_args *, const char *service,
+ krb5_get_init_creds_opt *, const char *pass,
+ krb5_creds *);
+int pamk5_alt_auth_verify(struct pam_args *);
+
+/* FAST support. Set up FAST protection of authentication. */
+void pamk5_fast_setup(struct pam_args *, krb5_get_init_creds_opt *);
+
+/* Context management. */
+int pamk5_context_new(struct pam_args *);
+int pamk5_context_fetch(struct pam_args *);
+void pamk5_context_free(struct pam_args *);
+void pamk5_context_destroy(pam_handle_t *, void *data, int pam_end_status);
+
+/* Get and set environment variables for the ticket cache. */
+const char *pamk5_get_krb5ccname(struct pam_args *, const char *key);
+int pamk5_set_krb5ccname(struct pam_args *, const char *, const char *key);
+
+/*
+ * Create a ticket cache file securely given a mkstemp template. Modifies
+ * template in place to store the name of the created file.
+ */
+int pamk5_cache_mkstemp(struct pam_args *, char *template);
+
+/*
+ * Create a ticket cache and initialize it with the provided credentials,
+ * returning the new cache in the last argument
+ */
+int pamk5_cache_init(struct pam_args *, const char *ccname, krb5_creds *,
+ krb5_ccache *);
+
+/*
+ * Create a ticket cache with a random path, initialize it with the provided
+ * credentials, store it in the context, and put the path into PAM_KRB5CCNAME.
+ */
+int pamk5_cache_init_random(struct pam_args *, krb5_creds *);
+
+/*
+ * Compatibility functions. Depending on whether pam_krb5 is built with MIT
+ * Kerberos or Heimdal, appropriate implementations for the Kerberos
+ * implementation will be provided.
+ */
+krb5_error_code pamk5_compat_set_realm(struct pam_config *, const char *);
+void pamk5_compat_free_realm(struct pam_config *);
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+#endif /* !INTERNAL_H */
diff --git a/module/options.c b/module/options.c
new file mode 100644
index 000000000000..f2c3791d895a
--- /dev/null
+++ b/module/options.c
@@ -0,0 +1,259 @@
+/*
+ * Option handling for pam-krb5.
+ *
+ * Responsible for initializing the args struct that's passed to nearly all
+ * internal functions. Retrieves configuration information from krb5.conf and
+ * parses the PAM configuration.
+ *
+ * Copyright 2005-2010, 2014, 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/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+#include <pam-util/options.h>
+#include <pam-util/vector.h>
+
+/* Our option definition. Must be sorted. */
+#define K(name) (#name), offsetof(struct pam_config, name)
+/* clang-format off */
+static const struct option options[] = {
+ { K(alt_auth_map), true, STRING (NULL) },
+ { K(anon_fast), true, BOOL (false) },
+ { K(banner), true, STRING ("Kerberos") },
+ { K(ccache), true, STRING (NULL) },
+ { K(ccache_dir), true, STRING ("FILE:/tmp") },
+ { K(clear_on_fail), true, BOOL (false) },
+ { K(debug), true, BOOL (false) },
+ { K(defer_pwchange), true, BOOL (false) },
+ { K(expose_account), true, BOOL (false) },
+ { K(fail_pwchange), true, BOOL (false) },
+ { K(fast_ccache), true, STRING (NULL) },
+ { K(force_alt_auth), true, BOOL (false) },
+ { K(force_first_pass), false, BOOL (false) },
+ { K(force_pwchange), true, BOOL (false) },
+ { K(forwardable), true, BOOL (false) },
+ { K(ignore_k5login), true, BOOL (false) },
+ { K(ignore_root), true, BOOL (false) },
+ { K(keytab), true, STRING (NULL) },
+ { K(minimum_uid), true, NUMBER (0) },
+ { K(no_ccache), false, BOOL (false) },
+ { K(no_prompt), true, BOOL (false) },
+ { K(no_update_user), true, BOOL (false) },
+ { K(only_alt_auth), true, BOOL (false) },
+ { K(pkinit_anchors), true, STRING (NULL) },
+ { K(pkinit_prompt), true, BOOL (false) },
+ { K(pkinit_user), true, STRING (NULL) },
+ { K(preauth_opt), true, LIST (NULL) },
+ { K(prompt_principal), true, BOOL (false) },
+ { K(realm), false, STRING (NULL) },
+ { K(renew_lifetime), true, TIME (0) },
+ { K(retain_after_close), true, BOOL (false) },
+ { K(search_k5login), true, BOOL (false) },
+ { K(silent), false, BOOL (false) },
+ { K(ticket_lifetime), true, TIME (0) },
+ { K(trace), false, STRING (NULL) },
+ { K(try_first_pass), false, BOOL (false) },
+ { K(try_pkinit), true, BOOL (false) },
+ { K(use_authtok), false, BOOL (false) },
+ { K(use_first_pass), false, BOOL (false) },
+ { K(use_pkinit), true, BOOL (false) },
+ { K(user_realm), true, STRING (NULL) },
+};
+/* clang-format on */
+static const size_t optlen = sizeof(options) / sizeof(options[0]);
+
+
+/*
+ * Allocate a new struct pam_args and initialize its data members, including
+ * parsing the arguments and getting settings from krb5.conf. Check the
+ * resulting options for consistency.
+ */
+struct pam_args *
+pamk5_init(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ int i;
+ struct pam_args *args;
+ struct pam_config *config = NULL;
+
+ args = putil_args_new(pamh, flags);
+ if (args == NULL) {
+ return NULL;
+ }
+ config = calloc(1, sizeof(struct pam_config));
+ if (config == NULL) {
+ goto nomem;
+ }
+ args->config = config;
+
+ /*
+ * Do an initial scan to see if the realm is already set in our options.
+ * If so, make sure that's set before we start loading option values,
+ * since it affects what comes out of krb5.conf.
+ *
+ * We will then ignore args->config->realm, set later by option parsing,
+ * in favor of using args->realm extracted here. However, the latter must
+ * exist to avoid throwing unknown option errors.
+ */
+ for (i = 0; i < argc; i++) {
+ if (strncmp(argv[i], "realm=", 6) != 0)
+ continue;
+ free(args->realm);
+ args->realm = strdup(&argv[i][strlen("realm=")]);
+ if (args->realm == NULL)
+ goto nomem;
+ }
+
+ if (!putil_args_defaults(args, options, optlen)) {
+ free(config);
+ putil_args_free(args);
+ return NULL;
+ }
+ if (!putil_args_krb5(args, "pam", options, optlen)) {
+ goto fail;
+ }
+ if (!putil_args_parse(args, argc, argv, options, optlen)) {
+ goto fail;
+ }
+ if (config->debug) {
+ args->debug = true;
+ }
+ if (config->silent) {
+ args->silent = true;
+ }
+
+ /* An empty banner should be treated the same as not having one. */
+ if (config->banner != NULL && config->banner[0] == '\0') {
+ free(config->banner);
+ config->banner = NULL;
+ }
+
+ /* Sanity-check try_first_pass, use_first_pass, and force_first_pass. */
+ if (config->force_first_pass && config->try_first_pass) {
+ putil_err(args, "force_first_pass set, ignoring try_first_pass");
+ config->try_first_pass = 0;
+ }
+ if (config->force_first_pass && config->use_first_pass) {
+ putil_err(args, "force_first_pass set, ignoring use_first_pass");
+ config->use_first_pass = 0;
+ }
+ if (config->use_first_pass && config->try_first_pass) {
+ putil_err(args, "use_first_pass set, ignoring try_first_pass");
+ config->try_first_pass = 0;
+ }
+
+ /*
+ * Don't set expose_account if we're using search_k5login. The user will
+ * get a principal formed from the account into which they're logging in,
+ * which isn't the password they'll use (that's the whole point of
+ * search_k5login).
+ */
+ if (config->search_k5login) {
+ config->expose_account = 0;
+ }
+
+ /* UIDs are unsigned on some systems. */
+ if (config->minimum_uid < 0) {
+ config->minimum_uid = 0;
+ }
+
+ /*
+ * Warn if PKINIT options were set and PKINIT isn't supported. The MIT
+ * method (krb5_get_init_creds_opt_set_pa) can't support use_pkinit.
+ */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT
+# ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PA
+ if (config->try_pkinit) {
+ putil_err(args, "try_pkinit requested but PKINIT not available");
+ } else if (config->use_pkinit) {
+ putil_err(args, "use_pkinit requested but PKINIT not available");
+ }
+# endif
+# ifndef HAVE_KRB5_GET_PROMPT_TYPES
+ if (config->use_pkinit) {
+ putil_err(args, "use_pkinit requested but PKINIT cannot be enforced");
+ }
+# endif
+#endif
+
+ /* Warn if the FAST option was set and FAST isn't supported. */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
+ if (config->fast_ccache || config->anon_fast) {
+ putil_err(args, "fast_ccache or anon_fast requested but FAST not"
+ " supported by Kerberos libraries");
+ }
+#endif
+
+ /* If tracing was requested enable it if possible. */
+#ifdef HAVE_KRB5_SET_TRACE_FILENAME
+ if (config->trace != NULL) {
+ krb5_error_code retval;
+
+ retval = krb5_set_trace_filename(args->ctx, config->trace);
+ if (retval == 0)
+ putil_debug(args, "enabled trace logging to %s", config->trace);
+ else
+ putil_err_krb5(args, retval, "cannot enable trace logging to %s",
+ config->trace);
+ }
+#else
+ if (config->trace != NULL) {
+ putil_err(args, "trace logging requested but not supported");
+ }
+#endif
+
+ return args;
+
+nomem:
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ free(config);
+ putil_args_free(args);
+ return NULL;
+
+fail:
+ pamk5_free(args);
+ return NULL;
+}
+
+
+/*
+ * Free the allocated args struct and any memory it points to.
+ */
+void
+pamk5_free(struct pam_args *args)
+{
+ struct pam_config *config;
+
+ if (args == NULL)
+ return;
+ config = args->config;
+ if (config != NULL) {
+ free(config->alt_auth_map);
+ free(config->banner);
+ free(config->ccache);
+ free(config->ccache_dir);
+ free(config->fast_ccache);
+ free(config->keytab);
+ free(config->pkinit_anchors);
+ free(config->pkinit_user);
+ vector_free(config->preauth_opt);
+ free(config->realm);
+ free(config->trace);
+ free(config->user_realm);
+ free(args->config);
+ args->config = NULL;
+ }
+ putil_args_free(args);
+}
diff --git a/module/pam_krb5.map b/module/pam_krb5.map
new file mode 100644
index 000000000000..b187908ee26a
--- /dev/null
+++ b/module/pam_krb5.map
@@ -0,0 +1,11 @@
+{
+ global:
+ pam_sm_acct_mgmt;
+ pam_sm_authenticate;
+ pam_sm_chauthtok;
+ pam_sm_close_session;
+ pam_sm_open_session;
+ pam_sm_setcred;
+ local:
+ *;
+};
diff --git a/module/pam_krb5.sym b/module/pam_krb5.sym
new file mode 100644
index 000000000000..1e7fc6b967c9
--- /dev/null
+++ b/module/pam_krb5.sym
@@ -0,0 +1,6 @@
+pam_sm_acct_mgmt
+pam_sm_authenticate
+pam_sm_chauthtok
+pam_sm_close_session
+pam_sm_open_session
+pam_sm_setcred
diff --git a/module/password.c b/module/password.c
new file mode 100644
index 000000000000..c1371234fa07
--- /dev/null
+++ b/module/password.c
@@ -0,0 +1,401 @@
+/*
+ * Kerberos password changing.
+ *
+ * Copyright 2005-2009, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * 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 <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Get the new password. Store it in PAM_AUTHTOK if we obtain it and verify
+ * it successfully and return it in the pass parameter. If pass is set to
+ * NULL, only store the new password in PAM_AUTHTOK.
+ *
+ * Returns a PAM error code, usually either PAM_AUTHTOK_ERR or PAM_SUCCESS.
+ */
+int
+pamk5_password_prompt(struct pam_args *args, char **pass)
+{
+ int pamret = PAM_AUTHTOK_ERR;
+ char *pass1 = NULL;
+ char *pass2;
+ PAM_CONST void *tmp;
+
+ /* Use the password from a previous module, if so configured. */
+ if (pass != NULL)
+ *pass = NULL;
+ if (args->config->use_authtok) {
+ pamret = pam_get_item(args->pamh, PAM_AUTHTOK, &tmp);
+ if (tmp == NULL) {
+ putil_debug_pam(args, pamret, "no stored password");
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ if (strlen(tmp) > PAM_MAX_RESP_SIZE - 1) {
+ putil_debug(args, "rejecting password longer than %d",
+ PAM_MAX_RESP_SIZE - 1);
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ pass1 = strdup((const char *) tmp);
+ }
+
+ /* Prompt for the new password if necessary. */
+ if (pass1 == NULL) {
+ pamret = pamk5_get_password(args, "Enter new", &pass1);
+ if (pamret != PAM_SUCCESS) {
+ putil_debug_pam(args, pamret, "error getting new password");
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ if (strlen(pass1) > PAM_MAX_RESP_SIZE - 1) {
+ putil_debug(args, "rejecting password longer than %d",
+ PAM_MAX_RESP_SIZE - 1);
+ pamret = PAM_AUTHTOK_ERR;
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ goto done;
+ }
+ pamret = pamk5_get_password(args, "Retype new", &pass2);
+ if (pamret != PAM_SUCCESS) {
+ putil_debug_pam(args, pamret, "error getting new password");
+ pamret = PAM_AUTHTOK_ERR;
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ goto done;
+ }
+ if (strcmp(pass1, pass2) != 0) {
+ putil_debug(args, "new passwords don't match");
+ pamk5_conv(args, "Passwords don't match", PAM_ERROR_MSG, NULL);
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ explicit_bzero(pass2, strlen(pass2));
+ free(pass2);
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ explicit_bzero(pass2, strlen(pass2));
+ free(pass2);
+
+ /* Save the new password for other modules. */
+ pamret = pam_set_item(args->pamh, PAM_AUTHTOK, pass1);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "error storing password");
+ pamret = PAM_AUTHTOK_ERR;
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ goto done;
+ }
+ }
+ if (pass != NULL)
+ *pass = pass1;
+ else {
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ }
+
+done:
+ return pamret;
+}
+
+
+/*
+ * We've obtained credentials for the password changing interface and gotten
+ * the new password, so do the work of actually changing the password.
+ */
+static int
+change_password(struct pam_args *args, const char *pass)
+{
+ struct context *ctx;
+ int retval = PAM_SUCCESS;
+ int result_code;
+ krb5_data result_code_string, result_string;
+ const char *message;
+
+ /* Sanity check. */
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL
+ || args->config->ctx->creds == NULL)
+ return PAM_AUTHTOK_ERR;
+ ctx = args->config->ctx;
+
+ /*
+ * The actual change.
+ *
+ * There are two password protocols in use: the change password protocol,
+ * which doesn't allow specification of the principal, and the newer set
+ * password protocol, which does. For our purposes, either will do.
+ *
+ * Both Heimdal and MIT provide krb5_set_password. With Heimdal,
+ * krb5_change_password is deprecated and krb5_set_password tries both
+ * protocols in turn, so will work with new and old servers. With MIT,
+ * krb5_set_password will use the old protocol if the principal is NULL
+ * and the new protocol if it is not.
+ *
+ * We would like to just use krb5_set_password with a NULL principal
+ * argument, but Heimdal 1.5 uses the default principal for the local user
+ * rather than the principal from the credentials, so we need to pass in a
+ * principal for Heimdal. So we're stuck with an #ifdef.
+ */
+#ifdef HAVE_KRB5_MIT
+ retval =
+ krb5_set_password(ctx->context, ctx->creds, (char *) pass, NULL,
+ &result_code, &result_code_string, &result_string);
+#else
+ retval =
+ krb5_set_password(ctx->context, ctx->creds, (char *) pass, ctx->princ,
+ &result_code, &result_code_string, &result_string);
+#endif
+
+ /* Everything from here on is just handling diagnostics and output. */
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "krb5_change_password failed");
+ message = krb5_get_error_message(ctx->context, retval);
+ pamk5_conv(args, message, PAM_ERROR_MSG, NULL);
+ krb5_free_error_message(ctx->context, message);
+ retval = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ if (result_code != 0) {
+ char *output;
+ int status;
+
+ putil_debug(args, "krb5_change_password: %s",
+ (char *) result_code_string.data);
+ retval = PAM_AUTHTOK_ERR;
+ status =
+ asprintf(&output, "%.*s%s%.*s", (int) result_code_string.length,
+ (char *) result_code_string.data,
+ result_string.length == 0 ? "" : ": ",
+ (int) result_string.length, (char *) result_string.data);
+ if (status < 0)
+ putil_crit(args, "asprintf failed: %s", strerror(errno));
+ else {
+ pamk5_conv(args, output, PAM_ERROR_MSG, NULL);
+ free(output);
+ }
+ }
+ krb5_free_data_contents(ctx->context, &result_string);
+ krb5_free_data_contents(ctx->context, &result_code_string);
+
+done:
+ /*
+ * On failure, when clear_on_fail is set, we set the new password to NULL
+ * so that subsequent password change PAM modules configured with
+ * use_authtok will also fail. Otherwise, since the order of the stack is
+ * fixed once the pre-check function runs, subsequent modules would
+ * continue even when we failed.
+ */
+ if (retval != PAM_SUCCESS && args->config->clear_on_fail) {
+ if (pam_set_item(args->pamh, PAM_AUTHTOK, NULL))
+ putil_err(args, "error clearing password");
+ }
+ return retval;
+}
+
+
+/*
+ * Change a user's password. Returns a PAM status code for success or
+ * failure. This does the work of pam_sm_chauthtok, but also needs to be
+ * called from pam_sm_authenticate if we're working around a library that
+ * can't handle password change during authentication.
+ *
+ * If the second argument is true, only do the authentication without actually
+ * doing the password change (PAM_PRELIM_CHECK).
+ */
+int
+pamk5_password_change(struct pam_args *args, bool only_auth)
+{
+ struct context *ctx = args->config->ctx;
+ int pamret = PAM_SUCCESS;
+ char *pass = NULL;
+
+ /*
+ * Authenticate to the password changing service using the old password.
+ */
+ if (ctx->creds == NULL) {
+ pamret = pamk5_password_auth(args, "kadmin/changepw", &ctx->creds);
+ if (pamret == PAM_SERVICE_ERR || pamret == PAM_AUTH_ERR)
+ pamret = PAM_AUTHTOK_RECOVER_ERR;
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ }
+
+ /*
+ * Now, get the new password and change it unless we're just doing the
+ * first check.
+ */
+ if (only_auth)
+ goto done;
+ pamret = pamk5_password_prompt(args, &pass);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ pamret = change_password(args, pass);
+ if (pamret == PAM_SUCCESS)
+ pam_syslog(args->pamh, LOG_INFO, "user %s changed Kerberos password",
+ ctx->name);
+
+done:
+ if (pass != NULL) {
+ explicit_bzero(pass, strlen(pass));
+ free(pass);
+ }
+ return pamret;
+}
+
+
+/*
+ * The function underlying the main PAM interface for password changing.
+ * Performs preliminary checks, user notification, and any reauthentication
+ * that's required.
+ *
+ * If the second argument is true, only do the authentication without actually
+ * doing the password change (PAM_PRELIM_CHECK).
+ */
+int
+pamk5_password(struct pam_args *args, bool only_auth)
+{
+ struct context *ctx = NULL;
+ int pamret, status;
+ PAM_CONST char *user;
+ char *pass = NULL;
+ bool set_context = false;
+
+ /*
+ * Check whether we should ignore this user.
+ *
+ * If we do ignore this user, and we're not in the preliminary check
+ * phase, still prompt the user for the new password, but suppress our
+ * banner. This is a little strange, but it allows another module to be
+ * stacked behind pam-krb5 with use_authtok and have it still work for
+ * ignored users.
+ *
+ * We ignore the return status when prompting for the new password in this
+ * case. The worst thing that can happen is to fail to get the password,
+ * in which case the other module will fail (or might even not care).
+ */
+ if (args->config->ignore_root || args->config->minimum_uid > 0) {
+ status = pam_get_user(args->pamh, &user, NULL);
+ if (status == PAM_SUCCESS && pamk5_should_ignore(args, user)) {
+ if (!only_auth) {
+ if (args->config->banner != NULL) {
+ free(args->config->banner);
+ args->config->banner = NULL;
+ }
+ pamk5_password_prompt(args, NULL);
+ }
+ pamret = PAM_IGNORE;
+ goto done;
+ }
+ }
+
+ /*
+ * If we weren't able to find an existing context to use, we're going
+ * into this fresh and need to create a new context.
+ */
+ if (args->config->ctx == NULL) {
+ pamret = pamk5_context_new(args);
+ if (pamret != PAM_SUCCESS) {
+ putil_debug_pam(args, pamret, "creating context failed");
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ pamret = pam_set_data(args->pamh, "pam_krb5", args->config->ctx,
+ pamk5_context_destroy);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "cannot set context data");
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ set_context = true;
+ }
+ ctx = args->config->ctx;
+
+ /*
+ * Tell the user what's going on if we're handling an expiration, but not
+ * if we were configured to use the same password as an earlier module in
+ * the stack. The correct behavior here is not clear (what if the
+ * Kerberos password expired but the other one didn't?), but warning
+ * unconditionally leads to a strange message in the middle of doing the
+ * password change.
+ */
+ if (ctx->expired && ctx->creds == NULL)
+ if (!args->config->force_first_pass && !args->config->use_first_pass)
+ pamk5_conv(args, "Password expired. You must change it now.",
+ PAM_TEXT_INFO, NULL);
+
+ /*
+ * Do the password change. This may only get tickets if we're doing the
+ * preliminary check phase.
+ */
+ pamret = pamk5_password_change(args, only_auth);
+ if (only_auth)
+ goto done;
+
+ /*
+ * If we were handling a forced password change for an expired password,
+ * now try to get a ticket cache with the new password. If this succeeds,
+ * clear the expired flag in the context.
+ */
+ if (pamret == PAM_SUCCESS && ctx->expired) {
+ krb5_creds *creds = NULL;
+ char *principal;
+ krb5_error_code retval;
+
+ putil_debug(args, "obtaining credentials with new password");
+ args->config->force_first_pass = 1;
+ pamret = pamk5_password_auth(args, NULL, &creds);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ retval = krb5_unparse_name(ctx->context, ctx->princ, &principal);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "krb5_unparse_name failed");
+ pam_syslog(args->pamh, LOG_INFO,
+ "user %s authenticated as UNKNOWN", ctx->name);
+ } else {
+ pam_syslog(args->pamh, LOG_INFO, "user %s authenticated as %s",
+ ctx->name, principal);
+ krb5_free_unparsed_name(ctx->context, principal);
+ }
+ ctx->expired = false;
+ pamret = pamk5_cache_init_random(args, creds);
+ krb5_free_cred_contents(ctx->context, creds);
+ free(creds);
+ }
+
+done:
+ if (pass != NULL) {
+ explicit_bzero(pass, strlen(pass));
+ free(pass);
+ }
+
+ /*
+ * Don't free our Kerberos context if we set a context, since the context
+ * will take care of that.
+ */
+ if (set_context)
+ args->ctx = NULL;
+
+ if (pamret != PAM_SUCCESS) {
+ if (pamret == PAM_SERVICE_ERR || pamret == PAM_AUTH_ERR)
+ pamret = PAM_AUTHTOK_ERR;
+ if (pamret == PAM_AUTHINFO_UNAVAIL)
+ pamret = PAM_AUTHTOK_ERR;
+ }
+ return pamret;
+}
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 */
diff --git a/module/public.c b/module/public.c
new file mode 100644
index 000000000000..44d5f7736794
--- /dev/null
+++ b/module/public.c
@@ -0,0 +1,260 @@
+/*
+ * The public APIs of the pam-afs-session PAM module.
+ *
+ * Provides the public pam_sm_authenticate, pam_sm_setcred,
+ * pam_sm_open_session, pam_sm_close_session, and pam_sm_chauthtok functions.
+ * These must all be specified in the same file to work with the symbol export
+ * and linking mechanism used in OpenPAM, since OpenPAM will mark them all as
+ * static functions and export a function table instead.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2005-2009, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * 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+
+ */
+
+/* Get prototypes for all of the functions. */
+#define PAM_SM_ACCOUNT
+#define PAM_SM_AUTH
+#define PAM_SM_PASSWORD
+#define PAM_SM_SESSION
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * The main PAM interface for authorization checking.
+ */
+PAM_EXTERN int
+pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_AUTH_ERR;
+ goto done;
+ }
+ pamret = pamk5_context_fetch(args);
+ ENTRY(args, flags);
+
+ /*
+ * Succeed if the user did not use krb5 to login. Ideally, we should
+ * probably fail and require that the user set up policy properly in their
+ * PAM configuration, but it's not common for the user to do so and that's
+ * not how other krb5 PAM modules work. If we don't do this, root logins
+ * with the system root password fail, which is a bad failure mode.
+ */
+ if (pamret != PAM_SUCCESS || args->config->ctx == NULL) {
+ pamret = PAM_IGNORE;
+ putil_debug(args, "skipping non-Kerberos login");
+ goto done;
+ }
+
+ pamret = pamk5_account(args);
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface for authentication. We also do authorization checks
+ * here, since many applications don't call pam_acct_mgmt.
+ */
+PAM_EXTERN int
+pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ENTRY(args, flags);
+
+ pamret = pamk5_authenticate(args);
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface, in the auth stack, for establishing credentials
+ * obtained during authentication.
+ */
+PAM_EXTERN int
+pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ bool refresh = false;
+ int pamret, allow;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ENTRY(args, flags);
+
+ /*
+ * Special case. Just free the context data, which will destroy the
+ * ticket cache as well.
+ */
+ if (flags & PAM_DELETE_CRED) {
+ pamret = pam_set_data(pamh, "pam_krb5", NULL, NULL);
+ if (pamret != PAM_SUCCESS)
+ putil_err_pam(args, pamret, "cannot clear context data");
+ goto done;
+ }
+
+ /*
+ * Reinitialization requested, which means that rather than creating a new
+ * ticket cache and setting KRB5CCNAME, we should figure out the existing
+ * ticket cache and just refresh its tickets.
+ */
+ if (flags & (PAM_REINITIALIZE_CRED | PAM_REFRESH_CRED))
+ refresh = true;
+ if (refresh && (flags & PAM_ESTABLISH_CRED)) {
+ putil_err(args, "requested establish and refresh at the same time");
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ allow = PAM_REINITIALIZE_CRED | PAM_REFRESH_CRED | PAM_ESTABLISH_CRED;
+ if (!(flags & allow)) {
+ putil_err(args, "invalid pam_setcred flags %d", flags);
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+
+ /* Do the work. */
+ pamret = pamk5_setcred(args, refresh);
+
+ /*
+ * Never return PAM_IGNORE from pam_setcred since this can confuse the
+ * Linux PAM library, at least for applications that call pam_setcred
+ * without pam_authenticate (possibly because authentication was done
+ * some other way), when used with jumps with the [] syntax. Since we
+ * do nothing in this case, and since the stack is already frozen from
+ * the auth group, success makes sense.
+ *
+ * Don't return an error here or the PAM stack will fail if pam-krb5 is
+ * used with [success=ok default=1], since jumps are treated as required
+ * during the second pass with pam_setcred.
+ */
+ if (pamret == PAM_IGNORE)
+ pamret = PAM_SUCCESS;
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface for password changing.
+ */
+PAM_EXTERN int
+pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ pamk5_context_fetch(args);
+ ENTRY(args, flags);
+
+ /* We only support password changes. */
+ if (!(flags & PAM_UPDATE_AUTHTOK) && !(flags & PAM_PRELIM_CHECK)) {
+ putil_err(args, "invalid pam_chauthtok flags %d", flags);
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+
+ pamret = pamk5_password(args, (flags & PAM_PRELIM_CHECK) != 0);
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface for opening a session.
+ */
+PAM_EXTERN int
+pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ENTRY(args, flags);
+ pamret = pamk5_setcred(args, 0);
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface for closing a session.
+ */
+PAM_EXTERN int
+pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
+ const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ENTRY(args, flags);
+ pamret = pam_set_data(pamh, "pam_krb5", NULL, NULL);
+ if (pamret != PAM_SUCCESS)
+ putil_err_pam(args, pamret, "cannot clear context data");
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/* OpenPAM uses this macro to set up a table of entry points. */
+#ifdef PAM_MODULE_ENTRY
+PAM_MODULE_ENTRY("pam_krb5");
+#endif
diff --git a/module/setcred.c b/module/setcred.c
new file mode 100644
index 000000000000..5b98b2919c88
--- /dev/null
+++ b/module/setcred.c
@@ -0,0 +1,474 @@
+/*
+ * Ticket creation routines for pam-krb5.
+ *
+ * pam_setcred and pam_open_session need to do similar but not identical work
+ * to create the user's ticket cache. The shared code is abstracted here into
+ * the pamk5_setcred function.
+ *
+ * Copyright 2005-2009, 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * 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 <pwd.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Given a cache name and an existing cache, initialize a new cache, store the
+ * credentials from the existing cache in it, and return a pointer to the new
+ * cache in the cache argument. Returns either PAM_SUCCESS or
+ * PAM_SERVICE_ERR.
+ */
+static int
+cache_init_from_cache(struct pam_args *args, const char *ccname,
+ krb5_ccache old, krb5_ccache *cache)
+{
+ struct context *ctx;
+ krb5_creds creds;
+ krb5_cc_cursor cursor;
+ int pamret;
+ krb5_error_code status;
+
+ *cache = NULL;
+ memset(&creds, 0, sizeof(creds));
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL
+ || args->config->ctx->context == NULL)
+ return PAM_SERVICE_ERR;
+ if (old == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+ status = krb5_cc_start_seq_get(ctx->context, old, &cursor);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot open new credentials");
+ return PAM_SERVICE_ERR;
+ }
+ status = krb5_cc_next_cred(ctx->context, old, &cursor, &creds);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot read new credentials");
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ pamret = pamk5_cache_init(args, ccname, &creds, cache);
+ if (pamret != PAM_SUCCESS) {
+ krb5_free_cred_contents(ctx->context, &creds);
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ krb5_free_cred_contents(ctx->context, &creds);
+
+ /*
+ * There probably won't be any additional credentials, but check for them
+ * and copy them just in case.
+ */
+ while (krb5_cc_next_cred(ctx->context, old, &cursor, &creds) == 0) {
+ status = krb5_cc_store_cred(ctx->context, *cache, &creds);
+ krb5_free_cred_contents(ctx->context, &creds);
+ if (status != 0) {
+ putil_err_krb5(args, status,
+ "cannot store additional credentials"
+ " in %s",
+ ccname);
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ }
+ pamret = PAM_SUCCESS;
+
+done:
+ krb5_cc_end_seq_get(ctx->context, ctx->cache, &cursor);
+ if (pamret != PAM_SUCCESS && *cache != NULL) {
+ krb5_cc_destroy(ctx->context, *cache);
+ *cache = NULL;
+ }
+ return pamret;
+}
+
+
+/*
+ * Determine the name of a new ticket cache. Handles ccache and ccache_dir
+ * PAM options and returns newly allocated memory.
+ *
+ * The ccache option, if set, contains a string with possible %u and %p
+ * escapes. The former is replaced by the UID and the latter is replaced by
+ * the PID (a suitable unique string).
+ */
+static char *
+build_ccache_name(struct pam_args *args, uid_t uid)
+{
+ char *cache_name = NULL;
+ int retval;
+
+ if (args->config->ccache == NULL) {
+ retval = asprintf(&cache_name, "%s/krb5cc_%d_XXXXXX",
+ args->config->ccache_dir, (int) uid);
+ if (retval < 0) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ return NULL;
+ }
+ } else {
+ size_t len = 0, delta;
+ char *p, *q;
+
+ for (p = args->config->ccache; *p != '\0'; p++) {
+ if (p[0] == '%' && p[1] == 'u') {
+ len += snprintf(NULL, 0, "%ld", (long) uid);
+ p++;
+ } else if (p[0] == '%' && p[1] == 'p') {
+ len += snprintf(NULL, 0, "%ld", (long) getpid());
+ p++;
+ } else {
+ len++;
+ }
+ }
+ len++;
+ cache_name = malloc(len);
+ if (cache_name == NULL) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ return NULL;
+ }
+ for (p = args->config->ccache, q = cache_name; *p != '\0'; p++) {
+ if (p[0] == '%' && p[1] == 'u') {
+ delta = snprintf(q, len, "%ld", (long) uid);
+ q += delta;
+ len -= delta;
+ p++;
+ } else if (p[0] == '%' && p[1] == 'p') {
+ delta = snprintf(q, len, "%ld", (long) getpid());
+ q += delta;
+ len -= delta;
+ p++;
+ } else {
+ *q = *p;
+ q++;
+ len--;
+ }
+ }
+ *q = '\0';
+ }
+ return cache_name;
+}
+
+
+/*
+ * Create a new context for a session if we've lost the context created during
+ * authentication (such as when running under OpenSSH). Return PAM_IGNORE if
+ * we're ignoring this user or if apparently our pam_authenticate never
+ * succeeded.
+ */
+static int
+create_session_context(struct pam_args *args)
+{
+ struct context *ctx = NULL;
+ PAM_CONST char *user;
+ const char *tmpname;
+ int status, pamret;
+
+ /* If we're going to ignore the user anyway, don't even bother. */
+ if (args->config->ignore_root || args->config->minimum_uid > 0) {
+ pamret = pam_get_user(args->pamh, &user, NULL);
+ if (pamret == PAM_SUCCESS && pamk5_should_ignore(args, user)) {
+ pamret = PAM_IGNORE;
+ goto fail;
+ }
+ }
+
+ /*
+ * Create the context and locate the temporary ticket cache. Load the
+ * ticket cache back into the context and flush out the other data that
+ * would have been set if we'd kept our original context.
+ */
+ pamret = pamk5_context_new(args);
+ if (pamret != PAM_SUCCESS) {
+ putil_crit_pam(args, pamret, "creating session context failed");
+ goto fail;
+ }
+ ctx = args->config->ctx;
+ tmpname = pamk5_get_krb5ccname(args, "PAM_KRB5CCNAME");
+ if (tmpname == NULL) {
+ putil_debug(args, "unable to get PAM_KRB5CCNAME, assuming"
+ " non-Kerberos login");
+ pamret = PAM_IGNORE;
+ goto fail;
+ }
+ putil_debug(args, "found initial ticket cache at %s", tmpname);
+ status = krb5_cc_resolve(ctx->context, tmpname, &ctx->cache);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot resolve cache %s", tmpname);
+ pamret = PAM_SERVICE_ERR;
+ goto fail;
+ }
+ status = krb5_cc_get_principal(ctx->context, ctx->cache, &ctx->princ);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot retrieve principal");
+ pamret = PAM_SERVICE_ERR;
+ goto fail;
+ }
+
+ /*
+ * We've rebuilt the context. Push it back into the PAM state for any
+ * further calls to session or account management, which OpenSSH does keep
+ * the context for.
+ */
+ pamret = pam_set_data(args->pamh, "pam_krb5", ctx, pamk5_context_destroy);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "cannot set context data");
+ goto fail;
+ }
+ return PAM_SUCCESS;
+
+fail:
+ pamk5_context_free(args);
+ return pamret;
+}
+
+
+/*
+ * Sets user credentials by creating the permanent ticket cache and setting
+ * the proper ownership. This function may be called by either pam_sm_setcred
+ * or pam_sm_open_session. The refresh flag should be set to true if we
+ * should reinitialize an existing ticket cache instead of creating a new one.
+ */
+int
+pamk5_setcred(struct pam_args *args, bool refresh)
+{
+ struct context *ctx = NULL;
+ krb5_ccache cache = NULL;
+ char *cache_name = NULL;
+ bool set_context = false;
+ int status = 0;
+ int pamret;
+ struct passwd *pw = NULL;
+ uid_t uid;
+ gid_t gid;
+
+ /* If configured not to create a cache, we have nothing to do. */
+ if (args->config->no_ccache) {
+ pamret = PAM_SUCCESS;
+ goto done;
+ }
+
+ /*
+ * If we weren't able to obtain a context, we were probably run by OpenSSH
+ * with its weird PAM handling, so we're going to cobble up a new context
+ * for ourselves.
+ */
+ pamret = pamk5_context_fetch(args);
+ if (pamret != PAM_SUCCESS) {
+ putil_debug(args, "no context found, creating one");
+ pamret = create_session_context(args);
+ if (pamret != PAM_SUCCESS || args->config->ctx == NULL)
+ goto done;
+ set_context = true;
+ }
+ ctx = args->config->ctx;
+
+ /*
+ * Some programs (xdm, for instance) appear to call setcred over and over
+ * again, so avoid doing useless work.
+ */
+ if (ctx->initialized) {
+ pamret = PAM_SUCCESS;
+ goto done;
+ }
+
+ /*
+ * Get the uid. The user is not required to be a local account for
+ * pam_authenticate, but for either pam_setcred (other than DELETE) or for
+ * pam_open_session, the user must be a local account.
+ */
+ pw = pam_modutil_getpwnam(args->pamh, ctx->name);
+ if (pw == NULL) {
+ putil_err(args, "getpwnam failed for %s", ctx->name);
+ pamret = PAM_USER_UNKNOWN;
+ goto done;
+ }
+ uid = pw->pw_uid;
+ gid = pw->pw_gid;
+
+ /* Get the cache name. If reinitializing, this is our existing cache. */
+ if (refresh) {
+ const char *name, *k5name;
+
+ /*
+ * Solaris su calls pam_setcred as root with PAM_REINITIALIZE_CREDS,
+ * preserving the user-supplied environment. An xlock program may
+ * also do this if it's setuid root and doesn't drop credentials
+ * before calling pam_setcred.
+ *
+ * There isn't any safe way of reinitializing the exiting ticket cache
+ * for the user if we're setuid without calling setreuid(). Calling
+ * setreuid() is possible, but if the calling application is threaded,
+ * it will change credentials for the whole application, with possibly
+ * bizarre and unintended (and insecure) results. Trying to verify
+ * ownership of the existing ticket cache before using it fails under
+ * various race conditions (for example, having one of the elements of
+ * the path be a symlink and changing the target of that symlink
+ * between our check and the call to krb5_cc_resolve). Without
+ * calling setreuid(), we run the risk of replacing a file owned by
+ * another user with a credential cache.
+ *
+ * We could fail with an error in the setuid case, which would be
+ * maximally safe, but it would prevent use of the module for
+ * authentication with programs such as Solaris su. Failure to
+ * reinitialize the cache is normally not a serious problem, just a
+ * missing feature. We therefore log an error and exit with
+ * PAM_SUCCESS for the setuid case.
+ *
+ * We do not use issetugid here since it always returns true if setuid
+ * was was involved anywhere in the process of running the binary.
+ * This would prevent a setuid screensaver that drops permissions from
+ * refreshing a credential cache. The issetugid behavior is safer,
+ * since the environment should ideally not be trusted even if the
+ * binary completely changed users away from the original user, but in
+ * that case the binary needs to take some responsibility for either
+ * sanitizing the environment or being certain that the calling user
+ * is permitted to act as the target user.
+ */
+ if (getuid() != geteuid() || getgid() != getegid()) {
+ putil_err(args, "credential reinitialization in a setuid context"
+ " ignored");
+ pamret = PAM_SUCCESS;
+ goto done;
+ }
+ name = pamk5_get_krb5ccname(args, "KRB5CCNAME");
+ if (name == NULL)
+ name = krb5_cc_default_name(ctx->context);
+ if (name == NULL) {
+ putil_err(args, "unable to get ticket cache name");
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ if (strncmp(name, "FILE:", strlen("FILE:")) == 0)
+ name += strlen("FILE:");
+
+ /*
+ * If the cache we have in the context and the cache we're
+ * reinitializing are the same cache, don't do anything; otherwise,
+ * we'll end up destroying the cache. This should never happen; this
+ * case triggering is a sign of a bug, probably in the calling
+ * application.
+ */
+ if (ctx->cache != NULL) {
+ k5name = krb5_cc_get_name(ctx->context, ctx->cache);
+ if (k5name != NULL) {
+ if (strncmp(k5name, "FILE:", strlen("FILE:")) == 0)
+ k5name += strlen("FILE:");
+ if (strcmp(name, k5name) == 0) {
+ pamret = PAM_SUCCESS;
+ goto done;
+ }
+ }
+ }
+
+ cache_name = strdup(name);
+ if (cache_name == NULL) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ pamret = PAM_BUF_ERR;
+ goto done;
+ }
+ putil_debug(args, "refreshing ticket cache %s", cache_name);
+
+ /*
+ * If we're refreshing the cache, we didn't really create it and the
+ * user's open session created by login is probably still managing
+ * it. Thus, don't remove it when PAM is shut down.
+ */
+ ctx->dont_destroy_cache = 1;
+ } else {
+ char *cache_name_tmp;
+ size_t len;
+
+ cache_name = build_ccache_name(args, uid);
+ if (cache_name == NULL) {
+ pamret = PAM_BUF_ERR;
+ goto done;
+ }
+ len = strlen(cache_name);
+ if (len > 6 && strncmp("XXXXXX", cache_name + len - 6, 6) == 0) {
+ if (strncmp(cache_name, "FILE:", strlen("FILE:")) == 0)
+ cache_name_tmp = cache_name + strlen("FILE:");
+ else
+ cache_name_tmp = cache_name;
+ pamret = pamk5_cache_mkstemp(args, cache_name_tmp);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ }
+ putil_debug(args, "initializing ticket cache %s", cache_name);
+ }
+
+ /*
+ * Initialize the new ticket cache and point the environment at it. Only
+ * chown the cache if the cache is of type FILE or has no type (making the
+ * assumption that the default cache type is FILE; otherwise, due to the
+ * type prefix, we'd end up with an invalid path.
+ */
+ pamret = cache_init_from_cache(args, cache_name, ctx->cache, &cache);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ if (strncmp(cache_name, "FILE:", strlen("FILE:")) == 0)
+ status = chown(cache_name + strlen("FILE:"), uid, gid);
+ else if (strchr(cache_name, ':') == NULL)
+ status = chown(cache_name, uid, gid);
+ if (status == -1) {
+ putil_crit(args, "chown of ticket cache failed: %s", strerror(errno));
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ pamret = pamk5_set_krb5ccname(args, cache_name, "KRB5CCNAME");
+ if (pamret != PAM_SUCCESS) {
+ putil_crit(args, "setting KRB5CCNAME failed: %s", strerror(errno));
+ goto done;
+ }
+
+ /*
+ * If we had a temporary ticket cache, delete the environment variable so
+ * that we won't get confused and think we still have a temporary ticket
+ * cache when called again.
+ *
+ * FreeBSD PAM, at least as of 7.2, doesn't support deleting environment
+ * variables using the syntax supported by Solaris and Linux. Work
+ * around that by setting the variable to an empty value if deleting it
+ * fails.
+ */
+ if (pam_getenv(args->pamh, "PAM_KRB5CCNAME") != NULL) {
+ pamret = pam_putenv(args->pamh, "PAM_KRB5CCNAME");
+ if (pamret != PAM_SUCCESS)
+ pamret = pam_putenv(args->pamh, "PAM_KRB5CCNAME=");
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ }
+
+ /* Destroy the temporary cache and put the new cache in the context. */
+ krb5_cc_destroy(ctx->context, ctx->cache);
+ ctx->cache = cache;
+ cache = NULL;
+ ctx->initialized = 1;
+ if (args->config->retain_after_close)
+ ctx->dont_destroy_cache = 1;
+
+done:
+ if (ctx != NULL && cache != NULL)
+ krb5_cc_destroy(ctx->context, cache);
+ free(cache_name);
+
+ /* If we stored our Kerberos context in PAM data, don't free it. */
+ if (set_context)
+ args->ctx = NULL;
+
+ return pamret;
+}
diff --git a/module/support.c b/module/support.c
new file mode 100644
index 000000000000..79b654ed2f32
--- /dev/null
+++ b/module/support.c
@@ -0,0 +1,141 @@
+/*
+ * Support functions for pam-krb5.
+ *
+ * Some general utility functions used by multiple PAM groups that aren't
+ * associated with any particular chunk of functionality.
+ *
+ * Copyright 2005-2007, 2009, 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 <errno.h>
+#include <pwd.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Given the PAM arguments and the user we're authenticating, see if we should
+ * ignore that user because they're root or have a low-numbered UID and we
+ * were configured to ignore such users. Returns true if we should ignore
+ * them, false otherwise. Ignores any fully-qualified principal names.
+ */
+int
+pamk5_should_ignore(struct pam_args *args, PAM_CONST char *username)
+{
+ struct passwd *pwd;
+
+ if (args->config->ignore_root && strcmp("root", username) == 0) {
+ putil_debug(args, "ignoring root user");
+ return 1;
+ }
+ if (args->config->minimum_uid > 0 && strchr(username, '@') == NULL) {
+ pwd = pam_modutil_getpwnam(args->pamh, username);
+ if (pwd != NULL && pwd->pw_uid < (uid_t) args->config->minimum_uid) {
+ putil_debug(args, "ignoring low-UID user (%lu < %ld)",
+ (unsigned long) pwd->pw_uid,
+ args->config->minimum_uid);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+
+/*
+ * Verify the user authorization. Call krb5_kuserok if this is a local
+ * account, or do the krb5_aname_to_localname verification if ignore_k5login
+ * was requested. For non-local accounts, the principal must match the
+ * authentication identity.
+ */
+int
+pamk5_authorized(struct pam_args *args)
+{
+ struct context *ctx;
+ krb5_context c;
+ krb5_error_code retval;
+ int status;
+ struct passwd *pwd;
+ char kuser[65]; /* MAX_USERNAME == 65 (MIT Kerberos 1.4.1). */
+
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL
+ || args->config->ctx->context == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+ if (ctx->name == NULL)
+ return PAM_SERVICE_ERR;
+ c = ctx->context;
+
+ /*
+ * If alt_auth_map was set, authorize the user if the authenticated
+ * principal matches the mapped principal. alt_auth_map essentially
+ * serves as a supplemental .k5login. PAM_SERVICE_ERR indicates fatal
+ * errors that should abort remaining processing; PAM_AUTH_ERR indicates
+ * that it just didn't match, in which case we continue to try other
+ * authorization methods.
+ */
+ if (args->config->alt_auth_map != NULL) {
+ status = pamk5_alt_auth_verify(args);
+ if (status == PAM_SUCCESS || status == PAM_SERVICE_ERR)
+ return status;
+ }
+
+ /*
+ * If the name to which we're authenticating contains @ (is fully
+ * qualified), it must match the principal exactly.
+ */
+ if (strchr(ctx->name, '@') != NULL) {
+ char *principal;
+
+ retval = krb5_unparse_name(c, ctx->princ, &principal);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "krb5_unparse_name failed");
+ return PAM_SERVICE_ERR;
+ }
+ if (strcmp(principal, ctx->name) != 0) {
+ putil_err(args, "user %s does not match principal %s", ctx->name,
+ principal);
+ krb5_free_unparsed_name(c, principal);
+ return PAM_AUTH_ERR;
+ }
+ krb5_free_unparsed_name(c, principal);
+ return PAM_SUCCESS;
+ }
+
+ /*
+ * Otherwise, apply either krb5_aname_to_localname or krb5_kuserok
+ * depending on the situation.
+ */
+ pwd = pam_modutil_getpwnam(args->pamh, ctx->name);
+ if (args->config->ignore_k5login || pwd == NULL) {
+ retval = krb5_aname_to_localname(c, ctx->princ, sizeof(kuser), kuser);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot convert principal to user");
+ return PAM_AUTH_ERR;
+ }
+ if (strcmp(kuser, ctx->name) != 0) {
+ putil_err(args, "user %s does not match local name %s", ctx->name,
+ kuser);
+ return PAM_AUTH_ERR;
+ }
+ } else {
+ if (!krb5_kuserok(c, ctx->princ, ctx->name)) {
+ putil_err(args, "krb5_kuserok for user %s failed", ctx->name);
+ return PAM_AUTH_ERR;
+ }
+ }
+
+ return PAM_SUCCESS;
+}