changeset 13084:8edea3c8c280

6909129 krb5 keytab management API should be simplified to easily merge keys from different realms
author Shawn Emery <Shawn.Emery@Sun.COM>
date Wed, 11 Aug 2010 13:08:50 -0600
parents ec9d94ce123b
children 6b4a9cc90eaa
files usr/src/lib/gss_mechs/mech_krb5/Makefile.com usr/src/lib/gss_mechs/mech_krb5/krb5/keytab/kt_solaris.c usr/src/lib/gss_mechs/mech_krb5/krb5/keytab/kt_solaris.h usr/src/lib/gss_mechs/mech_krb5/mapfile-vers
diffstat 4 files changed, 551 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/usr/src/lib/gss_mechs/mech_krb5/Makefile.com	Wed Aug 11 11:18:58 2010 -0700
+++ b/usr/src/lib/gss_mechs/mech_krb5/Makefile.com	Wed Aug 11 13:08:50 2010 -0600
@@ -115,7 +115,8 @@
 
 # krb5/keytab
 K5_KT=	ktadd.o ktbase.o ktdefault.o ktfr_entry.o \
-	ktremove.o read_servi.o kt_file.o kt_srvtab.o ktfns.o kt_findrealm.o
+	ktremove.o read_servi.o kt_file.o kt_srvtab.o ktfns.o kt_findrealm.o \
+	kt_solaris.o
 
 K5_KRB= addr_comp.o  addr_order.o  addr_srch.o \
 	auth_con.o  bld_pr_ext.o  bld_princ.o  chk_trans.o \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/lib/gss_mechs/mech_krb5/krb5/keytab/kt_solaris.c	Wed Aug 11 13:08:50 2010 -0600
@@ -0,0 +1,505 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
+ */
+
+#include <krb5.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
+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
+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:
+
+	krb5_xfree(salt.data);
+	krb5_free_keyblock_contents(ctx, &key);
+
+	return (code);
+}
+
+/*
+ * krb5_error_code krb5_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 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  KRB5_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
+ *
+ * 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
+ * KRB5_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
+krb5_kt_add_ad_entries(krb5_context ctx, char **sprincs_str, char *domain,
+    krb5_kvno kvno, uint_t flags, char *password)
+{
+	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 (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);
+	/*
+	 * 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 = 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 & KRB5_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 = kt_add_entry(ctx, kt, princ, salt, *tenctype,
+			    kvno, password);
+			if (code != 0)
+				goto cleanup;
+
+			penctype = *tenctype;
+		}
+
+		krb5_free_principal(ctx, princ);
+		princ = 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_ktypes(ctx, enctypes);
+	if (princ != NULL)
+		krb5_free_principal(ctx, princ);
+
+	return (code);
+}
+
+#define	PRINCIPAL	0
+#define	REALM		1
+
+static krb5_error_code
+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 = 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 krb5_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
+krb5_kt_remove_by_realm(krb5_context ctx, char *realm)
+{
+
+	return (kt_remove_by_key(ctx, realm, REALM));
+}
+
+/*
+ * krb5_error_code krb5_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
+krb5_kt_remove_by_svcprinc(krb5_context ctx, char *sprinc_str)
+{
+
+	return (kt_remove_by_key(ctx, sprinc_str, PRINCIPAL));
+}
+
+/*
+ * krb5_error_code krb5_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 KRB5_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
+krb5_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, *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 & KRB5_KT_FLAG_AES_SUPPORT;
+
+	if ((code = krb5_parse_name(ctx, sprinc_str, &svc_princ)) != 0)
+		goto cleanup;
+
+	if ((code = 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_ktypes(ctx, enctypes);
+
+	return (code);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/lib/gss_mechs/mech_krb5/krb5/keytab/kt_solaris.h	Wed Aug 11 13:08:50 2010 -0600
@@ -0,0 +1,40 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
+ */
+
+#ifndef	_KT_SOLARIS_H
+#define	_KT_SOLARIS_H
+
+#define	KRB5_KT_FLAG_AES_SUPPORT	1
+
+krb5_error_code krb5_kt_add_ad_entries(krb5_context, char **, char *, krb5_kvno,
+    uint_t, char *);
+
+krb5_error_code krb5_kt_remove_by_realm(krb5_context, char *);
+
+krb5_error_code krb5_kt_remove_by_svcprinc(krb5_context, char *);
+
+krb5_error_code krb5_kt_ad_validate(krb5_context, char *, uint_t, boolean_t *);
+
+#endif /* _KT_SOLARIS_H */
--- a/usr/src/lib/gss_mechs/mech_krb5/mapfile-vers	Wed Aug 11 11:18:58 2010 -0700
+++ b/usr/src/lib/gss_mechs/mech_krb5/mapfile-vers	Wed Aug 11 13:08:50 2010 -0600
@@ -500,6 +500,7 @@
 	krb5_is_referral_realm;
 	krb5_is_thread_safe;
 	krb5_kdc_rep_decrypt_proc;
+	krb5_kt_add_ad_entries;
 	krb5_kt_add_entry;
 	krb5_kt_close;
 	krb5_kt_default;
@@ -514,9 +515,12 @@
 	krb5_kt_next_entry;
 	krb5_kt_read_service_key;
 	krb5_kt_register;
+	krb5_kt_remove_by_realm;
+	krb5_kt_remove_by_svcprinc;
 	krb5_kt_remove_entry;
 	krb5_kt_resolve;
 	krb5_kt_start_seq_get;
+	krb5_kt_ad_validate;
 	krb5_ktf_ops;
 	krb5_ktf_writable_ops;
 	krb5_ktfile_add;