components/krb5/Solaris/kt_solaris.c
author Rich Burridge <rich.burridge@oracle.com>
Thu, 13 Apr 2017 13:20:29 -0700
changeset 7864 f11e8d81786a
parent 5490 9bf0bc57423a
permissions -rw-r--r--
PSARC 2017/057 pcre2 25783625 Add pcre2 10.23 to Userland because vte 0.47.90 requires it

/*
 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
 */

/*
 * kt_solaris.c is to provide set of keytab interfaces contracted with SMB team.
 */

#include "k5-int.h"
#include <errno.h>
#include <netdb.h>
#include <strings.h>
#include <stdio.h>
#include <assert.h>
#include <ctype.h>
#include "kt_solaris.h"

#define	AES128		ENCTYPE_AES128_CTS_HMAC_SHA1_96
#define	AES256		ENCTYPE_AES256_CTS_HMAC_SHA1_96
#define	DES3		ENCTYPE_DES3_CBC_SHA1
#define	AES_ENTRIES	2
#define	HOST_TRUNC	15
#define	SVC_ENTRIES	4

static krb5_error_code
k5_kt_open(krb5_context ctx, krb5_keytab *kt)
{
	krb5_error_code code;
	char		buf[MAX_KEYTAB_NAME_LEN], ktstr[MAX_KEYTAB_NAME_LEN];

	memset(buf, 0, sizeof (buf));
	memset(ktstr, 0, sizeof (ktstr));

	if ((code = krb5_kt_default_name(ctx, buf, sizeof (buf))) != 0)
		return (code);

	/*
	 * The default is file type w/o the write.  If it's anything besides
	 * FILE or WRFILE then we bail as quickly as possible.
	 */
	if (strncmp(buf, "FILE:", strlen("FILE:")) == 0)
		(void) snprintf(ktstr, sizeof (ktstr), "WR%s", buf);
	else if (strncmp(buf, "WRFILE:", strlen("WRFILE:")) == 0)
		(void) snprintf(ktstr, sizeof (ktstr), "%s", buf);
	else
		return (EINVAL);

	return (krb5_kt_resolve(ctx, ktstr, kt));
}

static krb5_error_code
k5_kt_add_entry(krb5_context ctx, krb5_keytab kt, const krb5_principal princ,
    const krb5_principal svc_princ, krb5_enctype enctype, krb5_kvno kvno,
    const char *pw)
{
	krb5_keytab_entry entry;
	krb5_data password, salt;
	krb5_keyblock key;
	krb5_error_code code;

	memset(&entry, 0, sizeof (entry));
	memset(&key, 0, sizeof (krb5_keyblock));

	password.length = strlen(pw);
	password.data = (char *)pw;

	if ((code = krb5_principal2salt(ctx, svc_princ, &salt)) != 0) {
		return (code);
	}

	if ((krb5_c_string_to_key(ctx, enctype, &password, &salt, &key)) != 0)
		goto cleanup;

	entry.key = key;
	entry.vno = kvno;
	entry.principal = princ;

	code = krb5_kt_add_entry(ctx, kt, &entry);

cleanup:

	free(salt.data);
	krb5_free_keyblock_contents(ctx, &key);

	return (code);
}

/*
 * krb5_error_code k5_kt_add_ad_entries(krb5_context ctx, char **sprincs_str,
 * krb5_kvno kvno, uint_t flags, char *password)
 *
 * Adds keys to the keytab file for a default set of service principals for
 * the specified host (or local host) in an Active Directory environment.
 *
 * where ctx is the pointer passed back from krb5_init_context
 * where sprincs_str is an array of service principal names to be added
 * to the keytab file, terminated by a NULL pointer
 * where domain is the domain used to fully qualify the hostname for
 * constructing the salt in the string-to-key function.
 * where kvno is the key version number of the set of service principal
 * keys to be added
 * where flags is the set of conditions that affects the key table entries
 * current set of defined flags:
 *
 * 	encryption type
 * 	---------------
 *  	0x00000001  K5_KT_FLAG_AES_SUPPORT (core set + AES-256-128 keys added)
 *
 * where password is the password that will be used to derive the key for
 * the associated service principals in the keytab file
 * where hostname is the unqualified hostname of the system that will be used to
 * derive the key salt. If NULL is specified, this function will use the hostname
 * of the local system.
 *
 * Note: this function is used for adding service principals to the
 * local /etc/krb5/krb5.keytab (unless KRB5_KTNAME has been set to something
 * different, see krb5envvar(5)) file when the client belongs to an AD domain.
 * The keytab file is populated differently for an AD domain as the various
 * service principals share the same key material, unlike MIT based
 * implementations.
 *
 * Note: For encryption types; the union of the enc type flag and the
 * capabilities of the client is used to determine the enc type set to
 * populate the keytab file.
 *
 * Note: The keys are not created for any AES enctypes UNLESS the
 * K5_KT_FLAG_AES_SUPPORT flag is set and permitted_enctypes has the AES
 * enctypes enabled.
 *
 * Note: In Active Directory environments the salt is constructed by truncating
 * the host name to 15 characters and only use the host svc princ as the salt,
 * e.g. host/<str15>.<domain>@<realm>.  The realm name is determined by parsing
 * sprincs_str.  The local host name to construct is determined by calling
 * gethostname(3C).  If AD environments construct salts differently in the
 * future or this function is expanded outside of AD environments one could
 * derive the salt by sending an initial authentication exchange.
 *
 * Note: The kvno was previously determined by performing an LDAP query of the
 * computer account's msDS-KeyVersionNumber attribute.  If the schema changes
 * in the future or this function is expanded outside of AD environments then
 * one could derive the principal's kvno by requesting a service ticket.
 */
krb5_error_code
k5_kt_add_ad_entries(krb5_context ctx, char **sprincs_str, char *domain,
    krb5_kvno kvno, uint_t flags, char *password, char *hostname)
{
	krb5_principal	princ = NULL, salt = NULL, f_princ = NULL;
	krb5_keytab	kt = NULL;
	krb5_enctype	*enctypes = NULL, *tenctype, penctype = 0;
	char		**tprinc, *ptr, *token, *t_host = NULL, *realm;
	char		localname[MAXHOSTNAMELEN];
	krb5_error_code	code;
	krb5_boolean	similar;
	uint_t		t_len;

	assert(ctx != NULL && sprincs_str != NULL && *sprincs_str != NULL);
	assert(password != NULL && domain != NULL);

	if ((code = krb5_parse_name(ctx, *sprincs_str, &f_princ)) != 0)
		return (code);
	if (krb5_princ_realm(ctx, f_princ)->length == 0) {
		code = EINVAL;
		goto cleanup;
	}
	realm = krb5_princ_realm(ctx, f_princ)->data;

	if (hostname == NULL) {
		if (gethostname(localname, MAXHOSTNAMELEN) != 0) {
			code = errno;
			goto cleanup;
		}
		token = localname;

		/*
		 * Local host name could be fully qualified and/or in upper
		 * case, but usually and appropriately not.
		 */
		if ((ptr = strchr(token, '.')) != NULL)
			ptr = '\0';
		for (ptr = token; *ptr; ptr++)
			*ptr = tolower(*ptr);
	} else {
		token = hostname;
	}

	/*
	 * Windows servers currently truncate the host name to 15 characters
	 * and only use the host svc princ as the salt, e.g.
	 * host/str15.domain@realm
	 */
	t_len = snprintf(NULL, 0, "host/%.*s.%s@%s", HOST_TRUNC, token, domain,
	    realm) + 1;
	if ((t_host = malloc(t_len)) == NULL) {
		code = ENOMEM;
		goto cleanup;
	}
	(void) snprintf(t_host, t_len, "host/%.*s.%s@%s", HOST_TRUNC, token,
	    domain, realm);

	if ((code = krb5_parse_name(ctx, t_host, &salt)) != 0)
		goto cleanup;

	if ((code = k5_kt_open(ctx, &kt)) != 0)
		goto cleanup;

	code = krb5_get_permitted_enctypes(ctx, &enctypes);
	if (code != 0 || *enctypes == NULL)
		goto cleanup;

	for (tprinc = sprincs_str; *tprinc; tprinc++) {

		if ((code = krb5_parse_name(ctx, *tprinc, &princ)) != 0)
			goto cleanup;

		for (tenctype = enctypes; *tenctype; tenctype++) {
			if ((!(flags & K5_KT_FLAG_AES_SUPPORT) &&
			    (*tenctype == AES128 || *tenctype == AES256)) ||
			    (*tenctype == DES3)) {
				continue;
			}

			if (penctype) {
				code = krb5_c_enctype_compare(ctx, *tenctype,
				    penctype, &similar);
				if (code != 0)
					goto cleanup;
				else if (similar)
					continue;
			}

			code = k5_kt_add_entry(ctx, kt, princ, salt, *tenctype,
			    kvno, password);
			if (code != 0)
				goto cleanup;

			penctype = *tenctype;
		}

		krb5_free_principal(ctx, princ);
		princ = NULL;
		penctype = NULL;
	}

cleanup:

	if (f_princ != NULL)
		krb5_free_principal(ctx, f_princ);
	if (salt != NULL)
		krb5_free_principal(ctx, salt);
	if (t_host != NULL)
		free(t_host);
	if (kt != NULL)
		(void) krb5_kt_close(ctx, kt);
	if (enctypes != NULL)
		krb5_free_enctypes(ctx, enctypes);
	if (princ != NULL)
		krb5_free_principal(ctx, princ);

	return (code);
}

#define	PRINCIPAL	0
#define	REALM		1

static krb5_error_code
k5_kt_remove_by_key(krb5_context ctx, char *key, uint_t type)
{
	krb5_error_code		code;
	krb5_kt_cursor		cursor;
	krb5_keytab_entry	entry;
	krb5_keytab		kt = NULL;
	krb5_principal		svc_princ = NULL;
	krb5_principal_data	realm_data;
	boolean_t		found = FALSE;

	assert(ctx != NULL && key != NULL);

	if (type == REALM) {
		krb5_princ_realm(ctx, &realm_data)->length = strlen(key);
		krb5_princ_realm(ctx, &realm_data)->data = key;
	} else if (type == PRINCIPAL) {
		if ((code = krb5_parse_name(ctx, key, &svc_princ)) != 0)
			goto cleanup;
	} else
		return (EINVAL);

	if ((code = k5_kt_open(ctx, &kt)) != 0)
		goto cleanup;

	if ((code = krb5_kt_start_seq_get(ctx, kt, &cursor)) != 0)
		goto cleanup;

	while ((code = krb5_kt_next_entry(ctx, kt, &entry, &cursor)) == 0) {
		if (type == PRINCIPAL && krb5_principal_compare(ctx, svc_princ,
		    entry.principal)) {
			found = TRUE;
		} else if (type == REALM && krb5_realm_compare(ctx, &realm_data,
		    entry.principal)) {
			found = TRUE;
		}

		if (found == TRUE) {
			code = krb5_kt_end_seq_get(ctx, kt, &cursor);
			if (code != 0) {
				krb5_kt_free_entry(ctx, &entry);
				goto cleanup;
			}

			code = krb5_kt_remove_entry(ctx, kt, &entry);
			if (code != 0) {
				krb5_kt_free_entry(ctx, &entry);
				goto cleanup;
			}

			code = krb5_kt_start_seq_get(ctx, kt, &cursor);
			if (code != 0) {
				krb5_kt_free_entry(ctx, &entry);
				goto cleanup;
			}

			found = FALSE;
		}

		krb5_kt_free_entry(ctx, &entry);
	}

	if (code && code != KRB5_KT_END)
		goto cleanup;

	code = krb5_kt_end_seq_get(ctx, kt, &cursor);

cleanup:

	if (svc_princ != NULL)
		krb5_free_principal(ctx, svc_princ);
	if (kt != NULL)
		(void) krb5_kt_close(ctx, kt);

	return (code);
}

/*
 * krb5_error_code k5_kt_remove_by_realm(krb5_context ctx, char *realm)
 *
 * Removes all key entries in the keytab file that match the exact realm name
 * specified.
 *
 * where ctx is the pointer passed back from krb5_init_context
 * where realm is the realm name that is matched for any keytab entries
 * to be removed
 *
 * Note: if there are no entries matching realm then 0 (success) is returned
 */
krb5_error_code
k5_kt_remove_by_realm(krb5_context ctx, char *realm)
{

	return (k5_kt_remove_by_key(ctx, realm, REALM));
}

/*
 * krb5_error_code k5_kt_remove_by_svcprinc(krb5_context ctx, char *sprinc_str)
 *
 * Removes all key entries in the keytab file that match the exact service
 * principal name specified.
 *
 * where ctx is the pointer passed back from krb5_init_context
 * where sprinc_str is the service principal name that is matched for any
 * keytab entries to be removed
 *
 * Note: if there are no entries matching sprinc_str then 0 (success) is
 * returned
 */
krb5_error_code
k5_kt_remove_by_svcprinc(krb5_context ctx, char *sprinc_str)
{

	return (k5_kt_remove_by_key(ctx, sprinc_str, PRINCIPAL));
}

/*
 * krb5_error_code k5_kt_validate(krb5_context ctx, char *sprinc_str,
 * uint_t flags, boolean_t *valid)
 *
 * The validate function determines that the service principal exists and that
 * it has a valid set of encryption types for said principal.
 *
 * where ctx is the pointer passed back from krb5_init_context
 * where sprinc_str is the principal to be validated in the keytab file
 * where flags is the set of conditions that affects the key table entries
 * that the function considers valid
 * 	current set of defined flags:
 *
 *	encryption type
 *	---------------
 *	0x00000001 K5_KT_FLAG_AES_SUPPORT (core set + AES-256-128 keys are
 *		valid)
 *
 * where valid is a boolean that is set if the sprinc_str is correctly
 * populated in the keytab file based on the flags set else valid is unset.
 *
 * Note: The validate function assumes that only one set of keys exists for
 * a corresponding service principal, of key version number (kvno) n.  It would
 * consider more than one kvno set as invalid.  This is from the fact that AD
 * clients will attempt to refresh credential caches if KRB5KRB_AP_ERR_MODIFIED
 * is returned by the acceptor when the requested kvno is not found within the
 * keytab file.
 */
krb5_error_code
k5_kt_ad_validate(krb5_context ctx, char *sprinc_str, uint_t flags,
    boolean_t *valid)
{
	krb5_error_code		code;
	krb5_kt_cursor		cursor;
	krb5_keytab_entry	entry;
	krb5_keytab		kt = NULL;
	krb5_principal		svc_princ = NULL;
	krb5_enctype		*enctypes = NULL, *tenctype, penctype = 0;
	boolean_t		ck_aes = FALSE;
	uint_t			aes_count = 0, kt_entries = 0;
	krb5_boolean		similar;

	assert(ctx != NULL && sprinc_str != NULL && valid != NULL);

	*valid = FALSE;
	ck_aes = flags & K5_KT_FLAG_AES_SUPPORT;

	if ((code = krb5_parse_name(ctx, sprinc_str, &svc_princ)) != 0)
		goto cleanup;

	if ((code = k5_kt_open(ctx, &kt)) != 0)
		goto cleanup;

	code = krb5_get_permitted_enctypes(ctx, &enctypes);
	if (code != 0 || *enctypes == NULL)
		goto cleanup;

	if ((code = krb5_kt_start_seq_get(ctx, kt, &cursor)) != 0)
		goto cleanup;

	while ((code = krb5_kt_next_entry(ctx, kt, &entry, &cursor)) == 0) {
		if (krb5_principal_compare(ctx, svc_princ, entry.principal)) {

			for (tenctype = enctypes; *tenctype; tenctype++) {
				if (penctype) {
					code = krb5_c_enctype_compare(ctx,
					    *tenctype, penctype, &similar);
					if (code != 0) {
						krb5_kt_free_entry(ctx, &entry);
						goto cleanup;
					} else if (similar)
						continue;
				}

				if ((*tenctype != DES3) &&
				    (entry.key.enctype == *tenctype)) {
					kt_entries++;
				}

				penctype = *tenctype;
			}

			if ((entry.key.enctype == AES128) ||
			    (entry.key.enctype == AES256)) {
				aes_count++;
			}
		}

		krb5_kt_free_entry(ctx, &entry);
	}

	if (code && code != KRB5_KT_END)
		goto cleanup;

	if ((code = krb5_kt_end_seq_get(ctx, kt, &cursor)))
		goto cleanup;

	if (ck_aes == TRUE) {
		if ((kt_entries != SVC_ENTRIES) || (aes_count != AES_ENTRIES))
			goto cleanup;
	} else if (kt_entries != (SVC_ENTRIES - AES_ENTRIES))
		goto cleanup;

	*valid = TRUE;

cleanup:

	if (svc_princ != NULL)
		krb5_free_principal(ctx, svc_princ);
	if (kt != NULL)
		(void) krb5_kt_close(ctx, kt);
	if (enctypes != NULL)
		krb5_free_enctypes(ctx, enctypes);

	return (code);
}