/*
 * 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;
}
