diff options
Diffstat (limited to 'module')
| -rw-r--r-- | module/account.c | 92 | ||||
| -rw-r--r-- | module/alt-auth.c | 240 | ||||
| -rw-r--r-- | module/auth.c | 1135 | ||||
| -rw-r--r-- | module/cache.c | 185 | ||||
| -rw-r--r-- | module/context.c | 177 | ||||
| -rw-r--r-- | module/fast.c | 288 | ||||
| -rw-r--r-- | module/internal.h | 261 | ||||
| -rw-r--r-- | module/options.c | 259 | ||||
| -rw-r--r-- | module/pam_krb5.map | 11 | ||||
| -rw-r--r-- | module/pam_krb5.sym | 6 | ||||
| -rw-r--r-- | module/password.c | 401 | ||||
| -rw-r--r-- | module/prompting.c | 481 | ||||
| -rw-r--r-- | module/public.c | 260 | ||||
| -rw-r--r-- | module/setcred.c | 474 | ||||
| -rw-r--r-- | module/support.c | 141 |
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; +} |
