mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-11-07 04:36:52 -10:00
Not only does the new pam_krb5 module not have the same allow_kdc_spoof option that the old one had, its behavior in this matter defaults to insecure. Reimplement allow_kdc_spoof and switch the default back. Reviewed by: cy Differential Revision: https://reviews.freebsd.org/D53884
1142 lines
42 KiB
C
1142 lines
42 KiB
C
/*
|
|
* 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 <[email protected]>
|
|
* Copyright 2010-2012, 2014
|
|
* The Board of Trustees of the Leland Stanford Junior University
|
|
* Copyright 2005 Andres Salomon <[email protected]>
|
|
* Copyright 1999-2000 Frank Cusack <[email protected]>
|
|
*
|
|
* 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);
|
|
}
|
|
#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;
|
|
}
|