diff options
Diffstat (limited to 'module/auth.c')
| -rw-r--r-- | module/auth.c | 1135 |
1 files changed, 1135 insertions, 0 deletions
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; +} |
