hdb: Namespace referrals

Add a new method for issuing referrals for entire namespaces of hostnames.

An alias of the form WELLKNOWN/HOSTBASED-NAMESPACE/service/namespace-fqdn@REALM
will cause all requests for host-based principals in the given namespace to be
referred to the given realm.
This commit is contained in:
Nicolas Williams
2021-11-14 16:24:48 -06:00
committed by Luke Howard
parent be708ca3cf
commit d833ce4cbc
4 changed files with 203 additions and 38 deletions

View File

@@ -982,11 +982,6 @@ derive_keys(krb5_context context,
if (!h_is_namespace && !h->entry.flags.virtual_keys)
return 0;
h->entry.flags.virtual = 1;
if (h_is_namespace) {
/* Set the entry's principal name */
free_Principal(h->entry.principal);
ret = copy_Principal(princ, h->entry.principal);
}
kr.len = 0;
kr.val = 0;
@@ -1019,7 +1014,7 @@ derive_keys(krb5_context context,
}
/* The principal name will be used in key derivation and error messages */
if (ret == 0 && h_is_namespace)
if (ret == 0)
ret = krb5_unparse_name(context, princ, &p);
/* Sanity check key rotations, determine current & last kr */
@@ -1153,6 +1148,10 @@ derive_keys(krb5_context context,
}
/*
* Pick a best kvno for the given principal at the given time.
*
* Implements the [hdb] new_service_key_delay configuration parameter.
*
* In order for disparate keytab provisioning systems such as OSKT and our own
* kadmin ext_keytab and httpkadmind's get-keys to coexist, we need to be able
* to force keys set by the former to not become current keys until users of
@@ -1163,9 +1162,9 @@ derive_keys(krb5_context context,
* The context is that OSKT's krb5_keytab is very happy to change keys in a way
* that requires all members of a cluster to rekey together. If one also
* wishes to have cluster members that opt out of this and just fetch current,
* past, and future keys periodically, then the keys set by OSKT need to not
* come into effect until all the opt-out members have had a chance to fetch
* the new keys.
* past, and future keys periodically, then the keys set by OSKT must not come
* into effect until all the opt-out members have had a chance to fetch the new
* keys.
*
* The assumption is that services will fetch new keys periodically, say, every
* four hours. Then one can set `[hdb] new_service_key_delay = 8h' in the
@@ -1175,12 +1174,12 @@ derive_keys(krb5_context context,
* Naturally, this applies only to concrete principals with concrete keys.
*/
static krb5_error_code
fix_keys(krb5_context context,
HDB *db,
unsigned flags,
krb5_timestamp now,
krb5uint32 kvno,
hdb_entry_ex *h)
pick_kvno(krb5_context context,
HDB *db,
unsigned flags,
krb5_timestamp now,
krb5uint32 kvno,
hdb_entry_ex *h)
{
HDB_extension *ext;
HDB_Ext_KeySet keys;
@@ -1296,7 +1295,7 @@ make_namespace_princ(krb5_context context,
/* First go around, need a namespace princ. Make it! */
ret = krb5_build_principal(context, namespace, strlen(realm),
realm, "WELLKNOWN",
realm, KRB5_WELLKNOWN_NAME,
HDB_WK_NAMESPACE, comp0, NULL);
if (ret == 0)
ret = krb5_principal_set_comp_string(context, *namespace, 3, comp1);
@@ -1307,6 +1306,138 @@ make_namespace_princ(krb5_context context,
return ret;
}
static int
is_namespace_princ_p(krb5_context context,
krb5_const_principal princ)
{
return
krb5_principal_get_num_comp(context, princ) >= 4
&& strcmp(krb5_principal_get_comp_string(context, princ, 0),
KRB5_WELLKNOWN_NAME) == 0
&& strcmp(krb5_principal_get_comp_string(context, princ, 1),
HDB_WK_NAMESPACE) == 0;
}
/* See call site */
static krb5_error_code
rewrite_hostname(krb5_context context,
krb5_const_principal wanted_princ,
krb5_const_principal ns_princ,
krb5_const_principal found_ns_princ,
char **s)
{
const char *ns_host_part, *wanted_host_part, *found_host_part;
const char *p, *r;
size_t ns_host_part_len, wanted_host_part_len;
wanted_host_part = krb5_principal_get_comp_string(context, wanted_princ, 1);
wanted_host_part_len = strlen(wanted_host_part);
if (wanted_host_part_len > 256) {
krb5_set_error_message(context, HDB_ERR_NOENTRY,
"Aliases of host-based principals longer than "
"256 bytes not supported");
return HDB_ERR_NOENTRY;
}
ns_host_part = krb5_principal_get_comp_string(context, ns_princ, 3);
ns_host_part_len = strlen(ns_host_part);
/* Find `ns_host_part' as the tail of `wanted_host_part' */
for (r = p = strstr(wanted_host_part, ns_host_part);
r && strnlen(r, ns_host_part_len + 1) > ns_host_part_len;
p = (r = strstr(r, ns_host_part)) ? r : p)
;
if (!p || strnlen(p, ns_host_part_len + 1) != ns_host_part_len)
return HDB_ERR_NOENTRY; /* Can't happen */
if (p == wanted_host_part || p[-1] != '.')
return HDB_ERR_NOENTRY;
found_host_part =
krb5_principal_get_comp_string(context, found_ns_princ, 3);
return
asprintf(s, "%.*s%s", (int)(p - wanted_host_part), wanted_host_part,
found_host_part) < 0 ||
*s == NULL ? krb5_enomem(context) : 0;
}
/*
* Fix `h->entry.principal' to match the desired `princ' in the namespace
* `nsprinc' (which is either the same as `h->entry.principal' or an alias
* of it).
*/
static krb5_error_code
fix_princ_name(krb5_context context,
krb5_const_principal princ,
krb5_const_principal nsprinc,
hdb_entry_ex *h)
{
krb5_error_code ret = 0;
char *s = NULL;
if (!nsprinc)
return 0;
if (krb5_principal_get_num_comp(context, princ) < 2)
return HDB_ERR_NOENTRY;
/* `nsprinc' must be a namespace principal */
if (krb5_principal_compare(context, nsprinc, h->entry.principal)) {
/*
* `h' is the HDB entry for `nsprinc', and `nsprinc' is its canonical
* name.
*
* Set the entry's principal name to the desired name. The keys will
* be fixed next (upstairs, but don't forget to!).
*/
free_Principal(h->entry.principal);
return copy_Principal(princ, h->entry.principal);
}
if (!is_namespace_princ_p(context, h->entry.principal)) {
/*
* The alias is a namespace, but the canonical name is not. WAT.
*
* Well, the KDC will just issue a referral anyways, so we can leave
* `h->entry.principal' as is...
*
* Remove all of `h->entry's keys just in case, and leave
* `h->entry.principal' as-is.
*/
free_Keys(&h->entry.keys);
(void) hdb_entry_clear_password(context, &h->entry);
return hdb_clear_extension(context, &h->entry,
choice_HDB_extension_data_hist_keys);
}
/*
* A namespace alias of a namespace entry.
*
* We'll want to rewrite the original principal accordingly.
*
* E.g., if the caller wanted host/foo.ns.test.h5l.se and we
* found WELLKNOWN/HOSTBASED-NAMESPACE/ns.test.h5l.se is an
* alias of WELLKNOWN/HOSTBASED-NAMESPACE/ns.example.org, then
* we'll want to treat host/foo.ns.test.h5l.se as an alias of
* host/foo.ns.example.org.
*/
if (krb5_principal_get_num_comp(context, h->entry.principal) !=
2 + krb5_principal_get_num_comp(context, princ))
ret = HDB_ERR_NOENTRY; /* Only host-based services for now */
if (ret == 0)
ret = rewrite_hostname(context, princ, nsprinc, h->entry.principal, &s);
if (ret == 0) {
krb5_free_principal(context, h->entry.principal);
h->entry.principal = NULL;
ret = krb5_make_principal(context, &h->entry.principal,
krb5_principal_get_realm(context, princ),
krb5_principal_get_comp_string(context,
princ, 0),
s,
NULL);
}
return ret;
}
/* Wrapper around db->hdb_fetch_kvno() that implements virtual princs/keys */
static krb5_error_code
fetch_it(krb5_context context,
@@ -1319,7 +1450,7 @@ fetch_it(krb5_context context,
hdb_entry_ex *ent)
{
krb5_const_principal tmpprinc = princ;
krb5_principal baseprinc = NULL;
krb5_principal nsprinc = NULL;
krb5_error_code ret = 0;
const char *comp0 = krb5_principal_get_comp_string(context, princ, 0);
const char *comp1 = krb5_principal_get_comp_string(context, princ, 1);
@@ -1331,7 +1462,7 @@ fetch_it(krb5_context context,
int do_search = 0;
if (db->enable_virtual_hostbased_princs && comp1 &&
strcmp("krbtgt", comp0) != 0 && strcmp("WELLKNOWN", comp0) != 0) {
strcmp("krbtgt", comp0) != 0 && strcmp(KRB5_WELLKNOWN_NAME, comp0) != 0) {
char *htmp;
if ((host = strdup(comp1)) == NULL)
@@ -1358,7 +1489,7 @@ fetch_it(krb5_context context,
}
tmp = host ? host : comp1;
for (ret = HDB_ERR_NOENTRY; ret == HDB_ERR_NOENTRY; tmpprinc = baseprinc) {
for (ret = HDB_ERR_NOENTRY; ret == HDB_ERR_NOENTRY; tmpprinc = nsprinc) {
krb5_error_code ret2 = 0;
/*
@@ -1376,7 +1507,7 @@ fetch_it(krb5_context context,
ret = db->hdb_fetch_kvno(context, db, tmpprinc, flags, kvno, ent);
if (ret != HDB_ERR_NOENTRY || hdots == 0 || hdots < mindots || !tmp ||
!do_search)
break;
break;
/*
* Breadcrumb:
@@ -1403,12 +1534,13 @@ fetch_it(krb5_context context,
hdots--;
}
if (baseprinc == NULL)
if (nsprinc == NULL)
/* First go around, need a namespace princ. Make it! */
ret2 = make_namespace_princ(context, db, tmpprinc, &baseprinc);
/* Update the hostname component */
ret2 = make_namespace_princ(context, db, tmpprinc, &nsprinc);
/* Update the hostname component of the namespace principal */
if (ret2 == 0)
ret2 = krb5_principal_set_comp_string(context, baseprinc, 3, tmp);
ret2 = krb5_principal_set_comp_string(context, nsprinc, 3, tmp);
if (ret2)
ret = ret2;
@@ -1425,14 +1557,20 @@ fetch_it(krb5_context context,
* key derivation to do, but that's decided in derive_keys().
*/
if (ret == 0) {
ret = derive_keys(context, flags, princ, !!baseprinc, t, etype, kvno,
ent);
/* Fix the principal name if namespaced */
ret = fix_princ_name(context, princ, nsprinc, ent);
/* Derive keys if namespaced or virtual */
if (ret == 0)
ret = fix_keys(context, db, flags, t, kvno, ent);
if (ret)
hdb_free_entry(context, ent);
ret = derive_keys(context, flags, princ, !!nsprinc, t, etype, kvno,
ent);
/* Pick the best kvno for this principal at the given time */
if (ret == 0)
ret = pick_kvno(context, db, flags, t, kvno, ent);
}
krb5_free_principal(context, baseprinc);
if (ret)
hdb_free_entry(context, ent);
krb5_free_principal(context, nsprinc);
free(host);
return ret;
}