From 4897093455a2bf08f3db3a1132cc2f6f5484d77c Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 08:03:02 +0200 Subject: Adding upstream version 1:2.6.4. Signed-off-by: Daniel Baumann --- utils/gssd/krb5_util.c | 1649 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1649 insertions(+) create mode 100644 utils/gssd/krb5_util.c (limited to 'utils/gssd/krb5_util.c') diff --git a/utils/gssd/krb5_util.c b/utils/gssd/krb5_util.c new file mode 100644 index 0000000..6f66ef4 --- /dev/null +++ b/utils/gssd/krb5_util.c @@ -0,0 +1,1649 @@ +/* + * Adapted in part from MIT Kerberos 5-1.2.1 slave/kprop.c and from + * http://docs.sun.com/?p=/doc/816-1331/6m7oo9sms&a=view + * + * Copyright (c) 2002-2004 The Regents of the University of Michigan. + * All rights reserved. + * + * Andy Adamson + * J. Bruce Fields + * Marius Aamodt Eriksen + * Kevin Coffman + */ + +/* + * slave/kprop.c + * + * Copyright 1990,1991 by the Massachusetts Institute of Technology. + * All Rights Reserved. + * + * Export of this software from the United States of America may + * require a specific license from the United States Government. + * It is the responsibility of any person or organization contemplating + * export to obtain such a license before exporting. + * + * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and + * distribute this software and its documentation for any purpose and + * without fee is hereby granted, provided that the above copyright + * notice appear in all copies and that both that copyright notice and + * this permission notice appear in supporting documentation, and that + * the name of M.I.T. not be used in advertising or publicity pertaining + * to distribution of the software without specific, written prior + * permission. Furthermore if you modify this software you must label + * your software as modified software and not distribute it in such a + * fashion that it might be confused with the original M.I.T. software. + * M.I.T. makes no representations about the suitability of + * this software for any purpose. It is provided "as is" without express + * or implied warranty. + */ + +/* + * Copyright 1994 by OpenVision Technologies, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software + * and its documentation for any purpose is hereby granted without fee, + * provided that the above copyright notice appears in all copies and + * that both that copyright notice and this permission notice appear in + * supporting documentation, and that the name of OpenVision not be used + * in advertising or publicity pertaining to distribution of the software + * without specific, written prior permission. OpenVision makes no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied warranty. + * + * OPENVISION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO + * EVENT SHALL OPENVISION BE LIABLE FOR ANY SPECIAL, INDIRECT OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF + * USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ +/* + krb5_util.c + + Copyright (c) 2004 The Regents of the University of Michigan. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the University nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif /* HAVE_CONFIG_H */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef USE_PRIVATE_KRB5_FUNCTIONS +#include +#endif +#include +#include + +#include +#include + +#include "nfslib.h" +#include "gssd.h" +#include "err_util.h" +#include "gss_util.h" +#include "krb5_util.h" + +/* + * List of principals from our keytab that we + * will try to use to obtain credentials + * (known as a principal list entry (ple)) + */ +struct gssd_k5_kt_princ { + struct gssd_k5_kt_princ *next; + // Only protect against deletion, not modification + int refcount; + // Only set during creation in new_ple() + krb5_principal princ; + char *realm; + // Modified during usage by gssd_get_single_krb5_cred() + char *ccname; + krb5_timestamp endtime; +}; + + +/* Global list of principals/cache file names for machine credentials */ +static struct gssd_k5_kt_princ *gssd_k5_kt_princ_list = NULL; +/* This mutex protects list modification & ple->ccname */ +static pthread_mutex_t ple_lock = PTHREAD_MUTEX_INITIALIZER; + +#ifdef HAVE_SET_ALLOWABLE_ENCTYPES +int limit_to_legacy_enctypes = 0; +#endif + +/*==========================*/ +/*=== Internal routines ===*/ +/*==========================*/ + +static int select_krb5_ccache(const struct dirent *d); +static int gssd_find_existing_krb5_ccache(uid_t uid, char *dirname, + const char **cctype, struct dirent **d); +static int gssd_get_single_krb5_cred(krb5_context context, + krb5_keytab kt, struct gssd_k5_kt_princ *ple, int force_renew); +static int query_krb5_ccache(const char* cred_cache, char **ret_princname, + char **ret_realm); + +static void release_ple_locked(krb5_context context, + struct gssd_k5_kt_princ *ple) +{ + if (--ple->refcount) + return; + + printerr(3, "freeing cached principal (ccname=%s, realm=%s)\n", + ple->ccname, ple->realm); + krb5_free_principal(context, ple->princ); + free(ple->ccname); + free(ple->realm); + free(ple); +} + +static void release_ple(krb5_context context, struct gssd_k5_kt_princ *ple) +{ + pthread_mutex_lock(&ple_lock); + release_ple_locked(context, ple); + pthread_mutex_unlock(&ple_lock); +} + + +/* + * Called from the scandir function to weed out potential krb5 + * credentials cache files + * + * Returns: + * 0 => don't select this one + * 1 => select this one + */ +static int +select_krb5_ccache(const struct dirent *d) +{ + /* + * Note: We used to check d->d_type for DT_REG here, + * but apparenlty reiser4 always has DT_UNKNOWN. + * Check for IS_REG after stat() call instead. + */ + if (strstr(d->d_name, GSSD_DEFAULT_CRED_PREFIX)) + return 1; + else + return 0; +} + +/* + * Look in directory "dirname" for files that look like they + * are Kerberos Credential Cache files for a given UID. + * + * Returns 0 if a valid-looking entry is found. "*cctype" is + * set to the name of the cache type. A pointer to the dirent + * is planted in "*d". Caller must free "*d" with free(3). + * + * Otherwise, a negative errno is returned. + */ +static int +gssd_find_existing_krb5_ccache(uid_t uid, char *dirname, + const char **cctype, struct dirent **d) +{ + struct dirent **namelist; + int n; + int i; + int found = 0; + struct dirent *best_match_dir = NULL; + struct stat best_match_stat, tmp_stat; + /* dirname + cctype + d_name + NULL */ + char buf[PATH_MAX+5+256+1]; + char *princname = NULL; + char *realm = NULL; + int score, best_match_score = 0, err = -EACCES; + + memset(&best_match_stat, 0, sizeof(best_match_stat)); + *cctype = NULL; + *d = NULL; + n = scandir(dirname, &namelist, select_krb5_ccache, 0); + if (n < 0) { + printerr(1, "Error doing scandir on directory '%s': %s\n", + dirname, strerror(errno)); + } + else if (n > 0) { + for (i = 0; i < n; i++) { + snprintf(buf, sizeof(buf), + "%s/%s", dirname, namelist[i]->d_name); + printerr(3, "CC '%s' being considered, " + "with preferred realm '%s'\n", + buf, preferred_realm ? + preferred_realm : ""); + if (lstat(buf, &tmp_stat)) { + printerr(0, "Error doing stat on '%s'\n", buf); + free(namelist[i]); + continue; + } + /* Only pick caches owned by the user (uid) */ + if (tmp_stat.st_uid != uid) { + printerr(3, "CC '%s' owned by %u, not %u\n", + buf, tmp_stat.st_uid, uid); + free(namelist[i]); + continue; + } + if (!S_ISREG(tmp_stat.st_mode) && + !S_ISDIR(tmp_stat.st_mode)) { + printerr(3, "CC '%s' is not a regular " + "file or directory\n", buf); + free(namelist[i]); + continue; + } + if (uid == 0 && !root_uses_machine_creds && + strstr(namelist[i]->d_name, "machine_")) { + printerr(3, "CC '%s' not available to root\n", buf); + free(namelist[i]); + continue; + } + if (S_ISDIR(tmp_stat.st_mode)) { + *cctype = "DIR"; + } else + if (S_ISREG(tmp_stat.st_mode)) { + *cctype = "FILE"; + } else { + continue; + } + snprintf(buf, sizeof(buf), "%s:%s/%s", *cctype, + dirname, namelist[i]->d_name); + if (!query_krb5_ccache(buf, &princname, &realm)) { + printerr(3, "CC '%s' is expired or corrupt\n", + buf); + free(namelist[i]); + err = -EKEYEXPIRED; + continue; + } + + score = 0; + if (preferred_realm && + strcmp(realm, preferred_realm) == 0) + score++; + + printerr(3, "CC '%s'(%s@%s) passed all checks and" + " has mtime of %u\n", + buf, princname, realm, + tmp_stat.st_mtime); + /* + * if more than one match is found, return the most + * recent (the one with the latest mtime), and + * don't free the dirent + */ + if (!found) { + best_match_dir = namelist[i]; + best_match_stat = tmp_stat; + best_match_score = score; + found++; + } + else { + /* + * If current score is higher than best match + * score, we use the current match. Otherwise, + * if the current match has an mtime later + * than the one we are looking at, then use + * the current match. Otherwise, we still + * have the best match. + */ + if (best_match_score < score || + (best_match_score == score && + tmp_stat.st_mtime > + best_match_stat.st_mtime)) { + free(best_match_dir); + best_match_dir = namelist[i]; + best_match_stat = tmp_stat; + best_match_score = score; + } + else { + free(namelist[i]); + } + printerr(3, "CC '%s:%s/%s' is our " + "current best match " + "with mtime of %u\n", + cctype, dirname, + best_match_dir->d_name, + best_match_stat.st_mtime); + } + free(princname); + free(realm); + } + free(namelist); + } + if (found) { + *d = best_match_dir; + return 0; + } + + return err; +} + +/* check if the ticket cache exists, if not set nocache=1 so that new + * tgt is gotten + */ +static int +gssd_check_if_cc_exists(struct gssd_k5_kt_princ *ple) +{ + int fd; + char cc_name[BUFSIZ]; + + snprintf(cc_name, sizeof(cc_name), "%s/%s%s_%s", + ccachesearch[0], GSSD_DEFAULT_CRED_PREFIX, + GSSD_DEFAULT_MACHINE_CRED_SUFFIX, ple->realm); + fd = open(cc_name, O_RDONLY); + if (fd < 0) + return 1; + close(fd); + return 0; +} + +/* + * Obtain credentials via a key in the keytab given + * a keytab handle and a gssd_k5_kt_princ structure. + * Checks to see if current credentials are expired, + * if not, uses the keytab to obtain new credentials. + * + * Returns: + * 0 => success (or credentials have not expired) + * nonzero => error + */ +static int +gssd_get_single_krb5_cred(krb5_context context, + krb5_keytab kt, + struct gssd_k5_kt_princ *ple, + int force_renew) +{ +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ADDRESSLESS + krb5_get_init_creds_opt *init_opts = NULL; +#else + krb5_get_init_creds_opt options; +#endif + krb5_get_init_creds_opt *opts; + krb5_creds my_creds; + krb5_ccache ccache = NULL; + char kt_name[BUFSIZ]; + char cc_name[BUFSIZ]; + int code; + time_t now = time(0); + char *cache_type; + char *pname = NULL; + char *k5err = NULL; + int nocache = 0; + pthread_t tid = pthread_self(); + + memset(&my_creds, 0, sizeof(my_creds)); + + if (!use_memcache) + nocache = gssd_check_if_cc_exists(ple); + /* + * Workaround for clock skew among NFS server, NFS client and KDC + * 300 because clock skew must be within 300sec for kerberos + */ + now += 300; + pthread_mutex_lock(&ple_lock); + if (ple->ccname && ple->endtime > now && !nocache && !force_renew) { + printerr(3, "%s(0x%lx): Credentials in CC '%s' are good until %s", + __func__, tid, ple->ccname, ctime((time_t *)&ple->endtime)); + code = 0; + pthread_mutex_unlock(&ple_lock); + goto out; + } + pthread_mutex_unlock(&ple_lock); + + if ((code = krb5_kt_get_name(context, kt, kt_name, BUFSIZ))) { + printerr(0, "ERROR: Unable to get keytab name in " + "gssd_get_single_krb5_cred\n"); + goto out; + } + + if ((krb5_unparse_name(context, ple->princ, &pname))) + pname = NULL; + +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ADDRESSLESS + code = krb5_get_init_creds_opt_alloc(context, &init_opts); + if (code) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "ERROR: %s allocating gic options\n", k5err); + goto out; + } + if (krb5_get_init_creds_opt_set_addressless(context, init_opts, 1)) + printerr(1, "WARNING: Unable to set option for addressless " + "tickets. May have problems behind a NAT.\n"); +#ifdef TEST_SHORT_LIFETIME + /* set a short lifetime (for debugging only!) */ + printerr(1, "WARNING: Using (debug) short machine cred lifetime!\n"); + krb5_get_init_creds_opt_set_tkt_life(init_opts, 5*60); +#endif + opts = init_opts; + +#else /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ADDRESSLESS */ + + krb5_get_init_creds_opt_init(&options); + krb5_get_init_creds_opt_set_address_list(&options, NULL); +#ifdef TEST_SHORT_LIFETIME + /* set a short lifetime (for debugging only!) */ + printerr(0, "WARNING: Using (debug) short machine cred lifetime!\n"); + krb5_get_init_creds_opt_set_tkt_life(&options, 5*60); +#endif + opts = &options; +#endif + + if ((code = krb5_get_init_creds_keytab(context, &my_creds, ple->princ, + kt, 0, NULL, opts))) { + k5err = gssd_k5_err_msg(context, code); + printerr(1, "WARNING: %s while getting initial ticket for " + "principal '%s' using keytab '%s'\n", k5err, + pname ? pname : "", kt_name); + goto out; + } + + /* + * Initialize cache file which we're going to be using + */ + + pthread_mutex_lock(&ple_lock); + if (use_memcache) + cache_type = "MEMORY"; + else + cache_type = "FILE"; + snprintf(cc_name, sizeof(cc_name), "%s:%s/%s%s_%s", + cache_type, + ccachesearch[0], GSSD_DEFAULT_CRED_PREFIX, + GSSD_DEFAULT_MACHINE_CRED_SUFFIX, ple->realm); + ple->endtime = my_creds.times.endtime; + if (ple->ccname == NULL || strcmp(ple->ccname, cc_name) != 0) { + free(ple->ccname); + ple->ccname = strdup(cc_name); + if (ple->ccname == NULL) { + printerr(0, "ERROR: no storage to duplicate credentials " + "cache name '%s'\n", cc_name); + code = ENOMEM; + pthread_mutex_unlock(&ple_lock); + goto out; + } + } + pthread_mutex_unlock(&ple_lock); + if ((code = krb5_cc_resolve(context, cc_name, &ccache))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "ERROR: %s while opening credential cache '%s'\n", + k5err, cc_name); + goto out; + } + if ((code = krb5_cc_initialize(context, ccache, ple->princ))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "ERROR: %s while initializing credential " + "cache '%s'\n", k5err, cc_name); + goto out; + } + if ((code = krb5_cc_store_cred(context, ccache, &my_creds))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "ERROR: %s while storing credentials in '%s'\n", + k5err, cc_name); + goto out; + } + + code = 0; + printerr(2, "%s(0x%lx): principal '%s' ccache:'%s'\n", + __func__, tid, pname, cc_name); + out: +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ADDRESSLESS + if (init_opts) + krb5_get_init_creds_opt_free(context, init_opts); +#endif + if (pname) + k5_free_unparsed_name(context, pname); + if (ccache) + krb5_cc_close(context, ccache); + krb5_free_cred_contents(context, &my_creds); + free(k5err); + return (code); +} + +/* + * Given a principal, find a matching ple structure + * Called with mutex held + */ +static struct gssd_k5_kt_princ * +find_ple_by_princ(krb5_context context, krb5_principal princ) +{ + struct gssd_k5_kt_princ *ple; + + for (ple = gssd_k5_kt_princ_list; ple != NULL; ple = ple->next) { + if (krb5_principal_compare(context, ple->princ, princ)) + return ple; + } + /* no match found */ + return NULL; +} + +/* + * Create, initialize, and add a new ple structure to the global list + * Called with mutex held + */ +static struct gssd_k5_kt_princ * +new_ple(krb5_context context, krb5_principal princ) +{ + struct gssd_k5_kt_princ *ple = NULL, *p; + krb5_error_code code; + char *default_realm; + int is_default_realm = 0; + + ple = malloc(sizeof(struct gssd_k5_kt_princ)); + if (ple == NULL) + goto outerr; + memset(ple, 0, sizeof(*ple)); + +#ifdef HAVE_KRB5 + ple->realm = strndup(princ->realm.data, + princ->realm.length); +#else + ple->realm = strdup(princ->realm); +#endif + if (ple->realm == NULL) + goto outerr; + code = krb5_copy_principal(context, princ, &ple->princ); + if (code) + goto outerr; + + /* + * Add new entry onto the list (if this is the default + * realm, always add to the front of the list) + */ + + code = krb5_get_default_realm(context, &default_realm); + if (code == 0) { + if (strcmp(ple->realm, default_realm) == 0) + is_default_realm = 1; + k5_free_default_realm(context, default_realm); + } + + if (is_default_realm) { + ple->next = gssd_k5_kt_princ_list; + gssd_k5_kt_princ_list = ple; + } else { + p = gssd_k5_kt_princ_list; + while (p != NULL && p->next != NULL) + p = p->next; + if (p == NULL) + gssd_k5_kt_princ_list = ple; + else + p->next = ple; + } + + ple->refcount = 1; + return ple; +outerr: + if (ple) { + if (ple->realm) + free(ple->realm); + free(ple); + } + return NULL; +} + +/* + * Given a principal, find an existing ple structure, or create one + */ +static struct gssd_k5_kt_princ * +get_ple_by_princ(krb5_context context, krb5_principal princ) +{ + struct gssd_k5_kt_princ *ple; + + pthread_mutex_lock(&ple_lock); + ple = find_ple_by_princ(context, princ); + if (ple == NULL) { + ple = new_ple(context, princ); + } + if (ple != NULL) { + ple->refcount++; + } + pthread_mutex_unlock(&ple_lock); + + return ple; +} + +/* + * Given a (possibly unqualified) hostname, + * return the fully qualified (lower-case!) hostname + */ +static int +get_full_hostname(const char *inhost, char *outhost, int outhostlen) +{ + struct addrinfo *addrs = NULL; + struct addrinfo hints; + int retval; + char *c; + pthread_t tid = pthread_self(); + + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = PF_UNSPEC; + hints.ai_flags = AI_CANONNAME; + + /* Get full target hostname */ + retval = getaddrinfo(inhost, NULL, &hints, &addrs); + if (retval) { + printerr(1, "%s(0x%lx): getaddrinfo(%s) failed: %s\n", + __func__, tid, inhost, gai_strerror(retval)); + goto out; + } + strncpy(outhost, addrs->ai_canonname, outhostlen); + nfs_freeaddrinfo(addrs); + for (c = outhost; *c != '\0'; c++) + *c = tolower(*c); + + if (get_verbosity() && strcmp(inhost, outhost)) + printerr(1, "%s(0x%0lx): inhost '%s' different than outhost '%s'\n", + __func__, tid, inhost, outhost); + + retval = 0; +out: + return retval; +} + +/* + * If principal matches the given realm and service name, + * and has *any* instance (hostname), return 1. + * Otherwise return 0, indicating no match. + */ +#ifdef HAVE_KRB5 +static int +realm_and_service_match(krb5_principal p, const char *realm, const char *service) +{ + /* Must have two components */ + if (p->length != 2) + return 0; + + if ((strlen(realm) == p->realm.length) + && (strncmp(realm, p->realm.data, p->realm.length) == 0) + && (strlen(service) == p->data[0].length) + && (strncmp(service, p->data[0].data, p->data[0].length) == 0)) + return 1; + + return 0; +} +#else +static int +realm_and_service_match(krb5_context context, krb5_principal p, + const char *realm, const char *service) +{ + const char *name, *inst; + + if (p->name.name_string.len != 2) + return 0; + + name = krb5_principal_get_comp_string(context, p, 0); + inst = krb5_principal_get_comp_string(context, p, 1); + if (name == NULL || inst == NULL) + return 0; + if ((strcmp(realm, p->realm) == 0) + && (strcmp(service, name) == 0)) + return 1; + + return 0; +} +#endif + +/* + * Search the given keytab file looking for an entry with the given + * service name and realm, ignoring hostname (instance). + * + * Returns: + * 0 => No error + * non-zero => An error occurred + * + * If a keytab entry is found, "found" is set to one, and the keytab + * entry is returned in "kte". Otherwise, "found" is zero, and the + * value of "kte" is unpredictable. + */ +static int +gssd_search_krb5_keytab(krb5_context context, krb5_keytab kt, + const char *realm, const char *service, + int *found, krb5_keytab_entry *kte) +{ + krb5_kt_cursor cursor; + krb5_error_code code; + struct gssd_k5_kt_princ *ple; + int retval = -1, status; + char kt_name[BUFSIZ]; + char *pname; + char *k5err = NULL; + + if (found == NULL) { + retval = EINVAL; + goto out; + } + *found = 0; + + /* + * Look through each entry in the keytab file and determine + * if we might want to use it as machine credentials. If so, + * save info in the global principal list (gssd_k5_kt_princ_list). + */ + if ((code = krb5_kt_get_name(context, kt, kt_name, BUFSIZ))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "ERROR: %s attempting to get keytab name\n", k5err); + retval = code; + goto out; + } + if ((code = krb5_kt_start_seq_get(context, kt, &cursor))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "ERROR: %s while beginning keytab scan " + "for keytab '%s'\n", k5err, kt_name); + retval = code; + goto out; + } + + printerr(4, "Scanning keytab for %s/*@%s\n", service, realm); + while ((code = krb5_kt_next_entry(context, kt, kte, &cursor)) == 0) { + if ((code = krb5_unparse_name(context, kte->principal, + &pname))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "WARNING: Skipping keytab entry because " + "we failed to unparse principal name: %s\n", + k5err); + k5_free_kt_entry(context, kte); + free(k5err); + k5err = NULL; + continue; + } + printerr(4, "Processing keytab entry for principal '%s'\n", + pname); + /* Use the first matching keytab entry found */ +#ifdef HAVE_KRB5 + status = realm_and_service_match(kte->principal, realm, service); +#else + status = realm_and_service_match(context, kte->principal, realm, service); +#endif + if (status) { + printerr(4, "We WILL use this entry (%s)\n", pname); + ple = get_ple_by_princ(context, kte->principal); + /* + * Return, don't free, keytab entry if + * we were successful! + */ + if (ple == NULL) { + retval = ENOMEM; + k5_free_kt_entry(context, kte); + } else { + release_ple(context, ple); + ple = NULL; + retval = 0; + *found = 1; + } + k5_free_unparsed_name(context, pname); + break; + } + else { + printerr(4, "We will NOT use this entry (%s)\n", + pname); + } + k5_free_unparsed_name(context, pname); + k5_free_kt_entry(context, kte); + } + + if ((code = krb5_kt_end_seq_get(context, kt, &cursor))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "WARNING: %s while ending keytab scan for " + "keytab '%s'\n", k5err, kt_name); + } + + /* Only clear the retval if has not been set */ + if (retval < 0) + retval = 0; + out: + free(k5err); + return retval; +} + +/* + * Find a keytab entry to use for a given target realm. + * Tries to find the most appropriate keytab to use given the + * name of the host we are trying to connect with. + * + * Note: the tgtname contains a hostname in the realm that we + * are authenticating to. It may, or may not be the same as + * the server hostname. + */ +static int +find_keytab_entry(krb5_context context, krb5_keytab kt, + const char *srchost, const char *tgtname, + krb5_keytab_entry *kte, const char **svcnames) +{ + krb5_error_code code; + char **realmnames = NULL; + char myhostname[NI_MAXHOST], targethostname[NI_MAXHOST]; + char myhostad[NI_MAXHOST+1]; + int i, j, k, retval; + char *default_realm = NULL; + char *realm; + char *k5err = NULL; + int tried_all = 0, tried_default = 0, tried_upper = 0; + krb5_principal princ; + const char *notsetstr = "not set"; + char *adhostoverride = NULL; + pthread_t tid = pthread_self(); + + + /* Get full target hostname */ + retval = get_full_hostname(tgtname, targethostname, + sizeof(targethostname)); + if (retval) + goto out; + + /* Get full local hostname */ + if (srchost) { + strcpy(myhostname, srchost); + strcpy(myhostad, myhostname); + } else { + /* Borrow myhostad for gethostname(), we need it later anyways */ + if (gethostname(myhostad, sizeof(myhostad)-1) == -1) { + retval = errno; + k5err = gssd_k5_err_msg(context, retval); + printerr(1, "%s while getting local hostname\n", k5err); + goto out; + } + retval = get_full_hostname(myhostad, myhostname, sizeof(myhostname)); + if (retval) { + /* Don't use myhostname */ + myhostname[0] = 0; + } + } + + /* Compute the active directory machine name HOST$ */ + krb5_appdefault_string(context, "nfs", NULL, "ad_principal_name", + notsetstr, &adhostoverride); + if (adhostoverride && strcmp(adhostoverride, notsetstr) != 0) { + printerr(1, + "AD host string overridden with \"%s\" from appdefaults\n", + adhostoverride); + /* No overflow: Windows cannot handle strings longer than 19 chars */ + strcpy(myhostad, adhostoverride); + } else { + /* In this case, it's been pre-filled above */ + for (i = 0; myhostad[i] != 0; ++i) { + if (myhostad[i] == '.') break; + } + myhostad[i] = '$'; + myhostad[i+1] = 0; + } + if (adhostoverride) + krb5_free_string(context, adhostoverride); + + code = krb5_get_default_realm(context, &default_realm); + if (code) { + retval = code; + k5err = gssd_k5_err_msg(context, code); + printerr(1, "%s while getting default realm name\n", k5err); + goto out; + } + + /* + * Get the realm name(s) for the target hostname. + * In reality, this function currently only returns a + * single realm, but we code with the assumption that + * someday it may actually return a list. + */ + code = krb5_get_host_realm(context, targethostname, &realmnames); + if (code) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "ERROR: %s while getting realm(s) for host '%s'\n", + k5err, targethostname); + retval = code; + goto out; + } + + /* + * Make sure the preferred_realm, which may have been explicitly set + * on the command line, is tried first. If nothing is found go on with + * the host and local default realm (if that hasn't already been tried). + */ + i = 0; + realm = realmnames[i]; + + if (preferred_realm && strcmp (realm, preferred_realm) != 0) { + realm = preferred_realm; + /* resetting the realmnames index */ + i = -1; + } + + while (1) { + if (realm == NULL) { + tried_all = 1; + if (!tried_default) + realm = default_realm; + } + if (tried_all && tried_default) + break; + if (strcmp(realm, default_realm) == 0) + tried_default = 1; + for (j = 0; svcnames[j] != NULL; j++) { + char spn[NI_MAXHOST+2]; + + /* + * The special svcname "$" means 'try the active + * directory machine account' + */ + if (strcmp(svcnames[j],"$") == 0) { + snprintf(spn, sizeof(spn), "%s@%s", myhostad, realm); + code = krb5_build_principal_ext(context, &princ, + strlen(realm), + realm, + strlen(myhostad), + myhostad, + NULL); + } else { + if (!myhostname[0]) + continue; + snprintf(spn, sizeof(spn), "%s/%s@%s", + svcnames[j], myhostname, realm); + code = krb5_build_principal_ext(context, &princ, + strlen(realm), + realm, + strlen(svcnames[j]), + svcnames[j], + strlen(myhostname), + myhostname, + NULL); + } + + if (code) { + k5err = gssd_k5_err_msg(context, code); + printerr(1, "%s while building principal for '%s'\n", + k5err, spn); + free(k5err); + k5err = NULL; + continue; + } + code = krb5_kt_get_entry(context, kt, princ, 0, 0, kte); + krb5_free_principal(context, princ); + if (code) { + k5err = gssd_k5_err_msg(context, code); + printerr(3, "%s while getting keytab entry for '%s'\n", + k5err, spn); + free(k5err); + k5err = NULL; + /* + * We tried the active directory machine account + * with the hostname part as-is and failed... + * convert it to uppercase and try again before + * moving on to the svcname + */ + if (strcmp(svcnames[j],"$") == 0 && !tried_upper) { + for (k = 0; myhostad[k] != '$'; ++k) { + myhostad[k] = toupper(myhostad[k]); + } + j--; + tried_upper = 1; + } + } else { + printerr(2, "find_keytab_entry(0x%lx): Success getting keytab entry for '%s'\n",tid, spn); + retval = 0; + goto out; + } + retval = code; + } + /* + * Nothing found with our hostname instance, now look for + * names with any instance (they must have an instance) + */ + for (j = 0; svcnames[j] != NULL; j++) { + int found = 0; + if (strcmp(svcnames[j],"$") == 0) + continue; + code = gssd_search_krb5_keytab(context, kt, realm, + svcnames[j], &found, kte); + if (!code && found) { + printerr(3, "Success getting keytab entry for " + "%s/*@%s\n", svcnames[j], realm); + retval = 0; + goto out; + } + } + if (!tried_all) { + i++; + realm = realmnames[i]; + } + } +out: + if (default_realm) + k5_free_default_realm(context, default_realm); + if (realmnames) + krb5_free_host_realm(context, realmnames); + free(k5err); + return retval; +} + + +static inline int data_is_equal(krb5_data d1, krb5_data d2) +{ + return (d1.length == d2.length + && memcmp(d1.data, d2.data, d1.length) == 0); +} + +static int +check_for_tgt(krb5_context context, krb5_ccache ccache, + krb5_principal principal) +{ + krb5_error_code ret; + krb5_creds creds; + krb5_cc_cursor cur; + int found = 0; + + ret = krb5_cc_start_seq_get(context, ccache, &cur); + if (ret) + return 0; + + while (!found && + (ret = krb5_cc_next_cred(context, ccache, &cur, &creds)) == 0) { + if (creds.server->length == 2 && + data_is_equal(creds.server->realm, + principal->realm) && + creds.server->data[0].length == 6 && + memcmp(creds.server->data[0].data, + "krbtgt", 6) == 0 && + data_is_equal(creds.server->data[1], + principal->realm) && + creds.times.endtime > time(NULL)) + found = 1; + krb5_free_cred_contents(context, &creds); + } + krb5_cc_end_seq_get(context, ccache, &cur); + + return found; +} + +static int +query_krb5_ccache(const char* cred_cache, char **ret_princname, + char **ret_realm) +{ + krb5_error_code ret; + krb5_context context; + krb5_ccache ccache; + krb5_principal principal; + int found = 0; + char *str = NULL; + char *princstring; + + *ret_princname = *ret_realm = NULL; + + ret = krb5_init_context(&context); + if (ret) + return 0; + + if(!cred_cache || krb5_cc_resolve(context, cred_cache, &ccache)) + goto err_cache; + + if (krb5_cc_set_flags(context, ccache, 0)) + goto err_princ; + + ret = krb5_cc_get_principal(context, ccache, &principal); + if (ret) + goto err_princ; + + found = check_for_tgt(context, ccache, principal); + if (found) { + ret = krb5_unparse_name(context, principal, &princstring); + if (ret == 0) { + if ((str = strchr(princstring, '@')) != NULL) { + *str = '\0'; + *ret_princname = strdup(princstring); + *ret_realm = strdup(str+1); + if (!*ret_princname || !*ret_realm) { + free(*ret_princname); + free(*ret_realm); + *ret_princname = NULL; + *ret_realm = NULL; + } + } + k5_free_unparsed_name(context, princstring); + } + } + krb5_free_principal(context, principal); +err_princ: + krb5_cc_set_flags(context, ccache, KRB5_TC_OPENCLOSE); + krb5_cc_close(context, ccache); +err_cache: + krb5_free_context(context); + return (*ret_princname && *ret_realm); +} + +/* + * Obtain (or refresh if necessary) Kerberos machine credentials + * If a ple is passed in, it's reference will be released + */ +static int +gssd_refresh_krb5_machine_credential_internal(char *hostname, + struct gssd_k5_kt_princ *ple, + char *service, char *srchost, + int force_renew) +{ + krb5_error_code code = 0; + krb5_context context; + krb5_keytab kt = NULL;; + int retval = 0; + char *k5err = NULL; + const char *svcnames[] = { "$", "root", "nfs", "host", NULL }; + + /* + * If a specific service name was specified, use it. + * Otherwise, use the default list. + */ + if (service != NULL && strcmp(service, "*") != 0) { + svcnames[0] = service; + svcnames[1] = NULL; + } + if (hostname == NULL && ple == NULL) { + printerr(0, "ERROR: %s: Invalid args\n", __func__); + return EINVAL; + } + code = krb5_init_context(&context); + if (code) { + k5err = gssd_k5_err_msg(NULL, code); + printerr(0, "ERROR: %s: %s while initializing krb5 context\n", + __func__, k5err); + retval = code; + goto out; + } + + if ((code = krb5_kt_resolve(context, keytabfile, &kt))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "ERROR: %s: %s while resolving keytab '%s'\n", + __func__, k5err, keytabfile); + goto out_free_context; + } + + if (ple == NULL) { + krb5_keytab_entry kte; + + code = find_keytab_entry(context, kt, srchost, hostname, + &kte, svcnames); + if (code) { + printerr(0, "ERROR: %s: no usable keytab entry found " + "in keytab %s for connection with host %s\n", + __FUNCTION__, keytabfile, hostname); + retval = code; + goto out_free_kt; + } + + ple = get_ple_by_princ(context, kte.principal); + k5_free_kt_entry(context, &kte); + if (ple == NULL) { + char *pname; + if ((krb5_unparse_name(context, kte.principal, &pname))) { + pname = NULL; + } + printerr(0, "ERROR: %s: Could not locate or create " + "ple struct for principal %s for connection " + "with host %s\n", + __FUNCTION__, pname ? pname : "", + hostname); + if (pname) k5_free_unparsed_name(context, pname); + goto out_free_kt; + } + } + retval = gssd_get_single_krb5_cred(context, kt, ple, force_renew); +out_free_kt: + krb5_kt_close(context, kt); +out_free_context: + if (ple) + release_ple(context, ple); + krb5_free_context(context); +out: + free(k5err); + return retval; +} + +/*==========================*/ +/*=== External routines ===*/ +/*==========================*/ + +/* + * Attempt to find the best match for a credentials cache file + * given only a UID. We really need more information, but we + * do the best we can. + * + * Returns 0 if a ccache was found, or a negative errno otherwise. + */ +int +gssd_setup_krb5_user_gss_ccache(uid_t uid, char *servername, char *dirpattern) +{ + /* dirname + cctype + d_name + NULL */ + char buf[PATH_MAX+5+256+1], dirname[PATH_MAX]; + const char *cctype; + struct dirent *d; + int err, i, j; + u_int maj_stat, min_stat; + + printerr(3, "looking for client creds with uid %u for " + "server %s in %s\n", uid, servername, dirpattern); + + for (i = 0, j = 0; dirpattern[i] != '\0'; i++) { + switch (dirpattern[i]) { + case '%': + switch (dirpattern[i + 1]) { + case '%': + dirname[j++] = dirpattern[i]; + i++; + break; + case 'U': + j += sprintf(dirname + j, "%lu", + (unsigned long) uid); + i++; + break; + } + break; + default: + dirname[j++] = dirpattern[i]; + break; + } + } + dirname[j] = '\0'; + + err = gssd_find_existing_krb5_ccache(uid, dirname, &cctype, &d); + if (err) + return err; + + snprintf(buf, sizeof(buf), "%s:%s/%s", cctype, dirname, d->d_name); + free(d); + + printerr(2, "using %s as credentials cache for client with " + "uid %u for server %s\n", buf, uid, servername); + + printerr(3, "using gss_krb5_ccache_name to select krb5 ccache %s\n", + buf); + maj_stat = gss_krb5_ccache_name(&min_stat, buf, NULL); + if (maj_stat != GSS_S_COMPLETE) { + printerr(0, "ERROR: unable to get user cred cache '%s' " + "failed (%s)\n", buf, error_message(min_stat)); + return maj_stat; + } + return 0; +} + +/* + * Return an array of pointers to names of credential cache files + * which can be used to try to create gss contexts with a server. + * + * Returns: + * 0 => list is attached + * nonzero => error + */ +int +gssd_get_krb5_machine_cred_list(char ***list) +{ + char **l; + int listinc = 10; + int listsize = listinc; + int i = 0; + int retval; + struct gssd_k5_kt_princ *ple; + + /* Assume failure */ + retval = -1; + *list = (char **) NULL; + + if ((l = (char **) malloc(listsize * sizeof(char *))) == NULL) { + retval = ENOMEM; + goto out; + } + + pthread_mutex_lock(&ple_lock); + for (ple = gssd_k5_kt_princ_list; ple; ple = ple->next) { + if (!ple->ccname) + continue; + + /* Take advantage of the fact we only remove the ple + * from the list during shutdown. If it's modified + * concurrently at worst we'll just miss a new entry + * before the current ple + * + * gssd_refresh_krb5_machine_credential_internal() will + * release the ple refcount + */ + ple->refcount++; + pthread_mutex_unlock(&ple_lock); + /* Make sure cred is up-to-date before returning it */ + retval = gssd_refresh_krb5_machine_credential_internal(NULL, ple, + NULL, NULL, 0); + pthread_mutex_lock(&ple_lock); + if (gssd_k5_kt_princ_list == NULL) { + /* Looks like we did shutdown... abort */ + l[i] = NULL; + gssd_free_krb5_machine_cred_list(l); + retval = ENOMEM; + goto out_lock; + } + if (retval) + continue; + if (i + 1 > listsize) { + char **tmplist; + listsize += listinc; + tmplist = (char **) + realloc(l, listsize * sizeof(char *)); + if (tmplist == NULL) { + gssd_free_krb5_machine_cred_list(l); + retval = ENOMEM; + goto out_lock; + } + l = tmplist; + } + if ((l[i++] = strdup(ple->ccname)) == NULL) { + gssd_free_krb5_machine_cred_list(l); + retval = ENOMEM; + goto out_lock; + } + } + if (i > 0) { + l[i] = NULL; + *list = l; + retval = 0; + } else + free((void *)l); +out_lock: + pthread_mutex_unlock(&ple_lock); + out: + return retval; +} + +/* + * Frees the list of names returned in get_krb5_machine_cred_list() + */ +void +gssd_free_krb5_machine_cred_list(char **list) +{ + char **n; + + if (list == NULL) + return; + for (n = list; n && *n; n++) { + free(*n); + } + free(list); +} + +/* + * Called upon exit. Destroys machine credentials. + */ +void +gssd_destroy_krb5_principals(int destroy_machine_creds) +{ + krb5_context context; + krb5_error_code code = 0; + krb5_ccache ccache; + struct gssd_k5_kt_princ *ple; + char *k5err = NULL; + + code = krb5_init_context(&context); + if (code) { + k5err = gssd_k5_err_msg(NULL, code); + printerr(0, "ERROR: %s while initializing krb5\n", k5err); + free(k5err); + return; + } + + pthread_mutex_lock(&ple_lock); + while (gssd_k5_kt_princ_list) { + ple = gssd_k5_kt_princ_list; + gssd_k5_kt_princ_list = ple->next; + + if (destroy_machine_creds && ple->ccname) { + if ((code = krb5_cc_resolve(context, ple->ccname, &ccache))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "WARNING: %s while resolving credential " + "cache '%s' for destruction\n", k5err, + ple->ccname); + free(k5err); + k5err = NULL; + } + + if (!code && (code = krb5_cc_destroy(context, ccache))) { + k5err = gssd_k5_err_msg(context, code); + printerr(0, "WARNING: %s while destroying credential " + "cache '%s'\n", k5err, ple->ccname); + free(k5err); + k5err = NULL; + } + } + + release_ple_locked(context, ple); + } + pthread_mutex_unlock(&ple_lock); + krb5_free_context(context); +} + +/* + * Obtain (or refresh if necessary) Kerberos machine credentials + */ +int +gssd_refresh_krb5_machine_credential(char *hostname, + char *service, char *srchost, + int force_renew) +{ + return gssd_refresh_krb5_machine_credential_internal(hostname, NULL, + service, srchost, + force_renew); +} + +/* + * A common routine for getting the Kerberos error message + */ +char * +gssd_k5_err_msg(krb5_context context, krb5_error_code code) +{ + const char *origmsg; + char *msg = NULL; + +#if HAVE_KRB5_GET_ERROR_MESSAGE + if (context != NULL) { + origmsg = krb5_get_error_message(context, code); + msg = strdup(origmsg); + krb5_free_error_message(context, origmsg); + } +#endif + if (msg != NULL) + return msg; +#if HAVE_KRB5 + return strdup(error_message(code)); +#else + if (context != NULL) + return strdup(krb5_get_err_text(context, code)); + else + return strdup(error_message(code)); +#endif +} + +/* + * Return default Kerberos realm + */ +void +gssd_k5_get_default_realm(char **def_realm) +{ + krb5_context context; + + if (krb5_init_context(&context)) + return; + + krb5_get_default_realm(context, def_realm); + + krb5_free_context(context); +} + +static int +gssd_acquire_krb5_cred(gss_cred_id_t *gss_cred) +{ + OM_uint32 maj_stat, min_stat; + gss_OID_set_desc desired_mechs = { 1, &krb5oid }; + + maj_stat = gss_acquire_cred(&min_stat, GSS_C_NO_NAME, GSS_C_INDEFINITE, + &desired_mechs, GSS_C_INITIATE, + gss_cred, NULL, NULL); + + if (maj_stat != GSS_S_COMPLETE) { + if (get_verbosity() > 0) + pgsserr("gss_acquire_cred", + maj_stat, min_stat, &krb5oid); + return -1; + } + + return 0; +} + +int +gssd_acquire_user_cred(gss_cred_id_t *gss_cred) +{ + OM_uint32 maj_stat, min_stat; + int ret; + + ret = gssd_acquire_krb5_cred(gss_cred); + if (ret) + return ret; + + /* force validation of cred to check for expiry */ + maj_stat = gss_inquire_cred(&min_stat, *gss_cred, + NULL, NULL, NULL, NULL); + if (maj_stat != GSS_S_COMPLETE) { + if (get_verbosity() > 0) + pgsserr("gss_inquire_cred", + maj_stat, min_stat, &krb5oid); + ret = -1; + } + + return ret; +} + +/* Removed a service ticket for nfs/ from the ticket cache + */ +int +gssd_k5_remove_bad_service_cred(char *name) +{ + krb5_creds in_creds, out_creds; + krb5_error_code ret; + krb5_context context; + krb5_ccache cache; + krb5_principal principal; + int retflags = KRB5_TC_MATCH_SRV_NAMEONLY; + char srvname[1024]; + + ret = krb5_init_context(&context); + if (ret) + goto out_cred; + ret = krb5_cc_default(context, &cache); + if (ret) + goto out_free_context; + ret = krb5_cc_get_principal(context, cache, &principal); + if (ret) + goto out_close_cache; + memset(&in_creds, 0, sizeof(in_creds)); + in_creds.client = principal; + sprintf(srvname, "nfs/%s", name); + ret = krb5_parse_name(context, srvname, &in_creds.server); + if (ret) + goto out_free_principal; + ret = krb5_cc_retrieve_cred(context, cache, retflags, &in_creds, &out_creds); + if (ret) + goto out_free_principal; + ret = krb5_cc_remove_cred(context, cache, 0, &out_creds); +out_free_principal: + krb5_free_principal(context, principal); +out_close_cache: + krb5_cc_close(context, cache); +out_free_context: + krb5_free_context(context); +out_cred: + return ret; +} + +#ifdef HAVE_SET_ALLOWABLE_ENCTYPES +/* + * this routine obtains a credentials handle via gss_acquire_cred() + * then calls gss_krb5_set_allowable_enctypes() to limit the encryption + * types negotiated. + * + * XXX Should call some function to determine the enctypes supported + * by the kernel. (Only need to do that once!) + * + * Returns: + * 0 => all went well + * -1 => there was an error + */ + +int +limit_krb5_enctypes(struct rpc_gss_sec *sec) +{ + u_int maj_stat, min_stat; + krb5_enctype enctypes[] = { ENCTYPE_DES_CBC_CRC, + ENCTYPE_DES_CBC_MD5, + ENCTYPE_DES_CBC_MD4 }; + int num_enctypes = sizeof(enctypes) / sizeof(enctypes[0]); + extern int num_krb5_enctypes; + extern krb5_enctype *krb5_enctypes; + int err = -1; + + if (sec->cred == GSS_C_NO_CREDENTIAL) { + err = gssd_acquire_krb5_cred(&sec->cred); + if (err) + return -1; + } + + /* + * If we failed for any reason to produce global + * list of supported enctypes, use local default here. + */ + if (krb5_enctypes == NULL || limit_to_legacy_enctypes) + maj_stat = gss_set_allowable_enctypes(&min_stat, sec->cred, + &krb5oid, num_enctypes, enctypes); + else + maj_stat = gss_set_allowable_enctypes(&min_stat, sec->cred, + &krb5oid, num_krb5_enctypes, krb5_enctypes); + + if (maj_stat != GSS_S_COMPLETE) { + pgsserr("gss_set_allowable_enctypes", + maj_stat, min_stat, &krb5oid); + return -1; + } + + return 0; +} +#endif /* HAVE_SET_ALLOWABLE_ENCTYPES */ -- cgit v1.2.3