/* * 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 * Copyright 2010-2012, 2014 * The Board of Trustees of the Leland Stanford Junior University * Copyright 2005 Andres Salomon * Copyright 1999-2000 Frank Cusack * * SPDX-License-Identifier: BSD-3-clause or GPL-1+ */ #include #include #include #include #include #ifdef HAVE_HX509_ERR_H # include #endif #include #include #include #include #include #include /* * 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); } #ifdef __FreeBSD__ if (args->config->allow_kdc_spoof) opts.flags &= ~KRB5_VERIFY_INIT_CREDS_OPT_AP_REQ_NOFAIL; else opts.flags |= KRB5_VERIFY_INIT_CREDS_OPT_AP_REQ_NOFAIL; #endif /* __FreeBSD__ */ 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; }