From 1723d5cf07693d8fb249956ee73ca9f4436f95da Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Tue, 4 Dec 2018 15:22:55 -0500 Subject: [PATCH] Add dns_canonicalize_hostname=fallback support Turn dns_canonicalize_hostname into a tristate variable, allowing the value "fallback" as well as the true/false booleans. If it is set to fallback, delay DNS canonicalization and attempt it only in krb5_get_credentials() if the KDC responds that the requested server principal name is unknown. [ghudson@mit.edu: added TGS tests; refactored code; edited commit message and documentation] ticket: 8765 (new) (cherry picked from commit 6c20cb1c89acaa03db897182a3b28d5f8f284907) --- doc/admin/conf_files/krb5_conf.rst | 4 ++ src/include/k5-int.h | 8 ++- src/include/k5-trace.h | 3 ++ src/lib/krb5/krb/get_creds.c | 79 ++++++++++++++++++++++++++---- src/lib/krb5/krb/init_ctx.c | 27 +++++++++- src/lib/krb5/krb/t_copy_context.c | 2 +- src/lib/krb5/os/os-proto.h | 4 ++ src/lib/krb5/os/sn2princ.c | 19 +++++-- src/tests/gcred.c | 5 +- src/tests/t_sn2princ.py | 34 ++++++++++++- 10 files changed, 167 insertions(+), 18 deletions(-) diff --git a/doc/admin/conf_files/krb5_conf.rst b/doc/admin/conf_files/krb5_conf.rst index 7b4389f6b..e9f7e8c59 100644 --- a/doc/admin/conf_files/krb5_conf.rst +++ b/doc/admin/conf_files/krb5_conf.rst @@ -201,6 +201,10 @@ The libdefaults section may contain any of the following relations: means that short hostnames will not be canonicalized to fully-qualified hostnames. The default value is true. + If this option is set to ``fallback`` (new in release 1.18), DNS + canonicalization will only be performed the server hostname is not + found with the original name when requesting credentials. + **dns_lookup_kdc** Indicate whether DNS SRV records should be used to locate the KDCs and other servers for a realm, if they are not listed in the diff --git a/src/include/k5-int.h b/src/include/k5-int.h index 255cee822..1e6a739e9 100644 --- a/src/include/k5-int.h +++ b/src/include/k5-int.h @@ -1159,6 +1159,12 @@ k5_plugin_register_dyn(krb5_context context, int interface_id, void k5_plugin_free_context(krb5_context context); +enum dns_canonhost { + CANONHOST_FALSE = 0, + CANONHOST_TRUE = 1, + CANONHOST_FALLBACK = 2 +}; + struct _kdb5_dal_handle; /* private, in kdb5.h */ typedef struct _kdb5_dal_handle kdb5_dal_handle; struct _kdb_log_context; @@ -1222,7 +1228,7 @@ struct _krb5_context { krb5_boolean allow_weak_crypto; krb5_boolean ignore_acceptor_hostname; - krb5_boolean dns_canonicalize_hostname; + enum dns_canonhost dns_canonicalize_hostname; krb5_trace_callback trace_callback; void *trace_callback_data; diff --git a/src/include/k5-trace.h b/src/include/k5-trace.h index 2aa379b76..f3ed6a45d 100644 --- a/src/include/k5-trace.h +++ b/src/include/k5-trace.h @@ -191,6 +191,9 @@ void krb5int_trace(krb5_context context, const char *fmt, ...); #define TRACE_FAST_REQUIRED(c) \ TRACE(c, "Using FAST due to KRB5_FAST_REQUIRED flag") +#define TRACE_GET_CREDS_FALLBACK(c, hostname) \ + TRACE(c, "Falling back to canonicalized server hostname {str}", hostname) + #define TRACE_GIC_PWD_CHANGED(c) \ TRACE(c, "Getting initial TGT with changed password") #define TRACE_GIC_PWD_CHANGEPW(c, tries) \ diff --git a/src/lib/krb5/krb/get_creds.c b/src/lib/krb5/krb/get_creds.c index 69900adfa..0a04d68b9 100644 --- a/src/lib/krb5/krb/get_creds.c +++ b/src/lib/krb5/krb/get_creds.c @@ -39,6 +39,7 @@ #include "k5-int.h" #include "int-proto.h" +#include "os-proto.h" #include "fast.h" /* @@ -1249,6 +1250,26 @@ krb5_tkt_creds_step(krb5_context context, krb5_tkt_creds_context ctx, return EINVAL; } +static krb5_error_code +try_get_creds(krb5_context context, krb5_flags options, krb5_ccache ccache, + krb5_creds *in_creds, krb5_creds *creds_out) +{ + krb5_error_code code; + krb5_tkt_creds_context ctx = NULL; + + code = krb5_tkt_creds_init(context, ccache, in_creds, options, &ctx); + if (code) + goto cleanup; + code = krb5_tkt_creds_get(context, ctx); + if (code) + goto cleanup; + code = krb5_tkt_creds_get_creds(context, ctx, creds_out); + +cleanup: + krb5_tkt_creds_free(context, ctx); + return code; +} + krb5_error_code KRB5_CALLCONV krb5_get_credentials(krb5_context context, krb5_flags options, krb5_ccache ccache, krb5_creds *in_creds, @@ -1256,7 +1277,10 @@ krb5_get_credentials(krb5_context context, krb5_flags options, { krb5_error_code code; krb5_creds *ncreds = NULL; - krb5_tkt_creds_context ctx = NULL; + krb5_creds canon_creds, store_creds; + krb5_principal_data canon_server; + krb5_data canon_components[2]; + char *hostname = NULL, *canon_hostname = NULL; *out_creds = NULL; @@ -1265,22 +1289,59 @@ krb5_get_credentials(krb5_context context, krb5_flags options, if (ncreds == NULL) goto cleanup; - /* Make and execute a krb5_tkt_creds context to get the credential. */ - code = krb5_tkt_creds_init(context, ccache, in_creds, options, &ctx); - if (code != 0) + code = try_get_creds(context, options, ccache, in_creds, ncreds); + if (!code) { + *out_creds = ncreds; + return 0; + } + + /* Possibly try again with the canonicalized hostname, if the server is + * host-based and we are configured for fallback canonicalization. */ + if (code != KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN) goto cleanup; - code = krb5_tkt_creds_get(context, ctx); - if (code != 0) + if (context->dns_canonicalize_hostname != CANONHOST_FALLBACK) goto cleanup; - code = krb5_tkt_creds_get_creds(context, ctx, ncreds); - if (code != 0) + if (in_creds->server->type != KRB5_NT_SRV_HST || + in_creds->server->length != 2) goto cleanup; + hostname = k5memdup0(in_creds->server->data[1].data, + in_creds->server->data[1].length, &code); + if (hostname == NULL) + goto cleanup; + code = k5_expand_hostname(context, hostname, TRUE, &canon_hostname); + if (code) + goto cleanup; + + TRACE_GET_CREDS_FALLBACK(context, canon_hostname); + + /* Make shallow copies of in_creds and its server to alter the hostname. */ + canon_components[0] = in_creds->server->data[0]; + canon_components[1] = string2data(canon_hostname); + canon_server = *in_creds->server; + canon_server.data = canon_components; + canon_creds = *in_creds; + canon_creds.server = &canon_server; + + code = try_get_creds(context, options | KRB5_GC_NO_STORE, ccache, + &canon_creds, ncreds); + if (code) + goto cleanup; + + if (!(options & KRB5_GC_NO_STORE)) { + /* Store the creds under the originally requested server name. The + * ccache layer will also store them under the ticket server name. */ + store_creds = *ncreds; + store_creds.server = in_creds->server; + (void)krb5_cc_store_cred(context, ccache, &store_creds); + } + *out_creds = ncreds; ncreds = NULL; cleanup: + free(hostname); + free(canon_hostname); krb5_free_creds(context, ncreds); - krb5_tkt_creds_free(context, ctx); return code; } diff --git a/src/lib/krb5/krb/init_ctx.c b/src/lib/krb5/krb/init_ctx.c index 947e50400..d263d5cc5 100644 --- a/src/lib/krb5/krb/init_ctx.c +++ b/src/lib/krb5/krb/init_ctx.c @@ -101,6 +101,30 @@ get_boolean(krb5_context ctx, const char *name, int def_val, int *boolean_out) return retval; } +static krb5_error_code +get_tristate(krb5_context ctx, const char *name, const char *third_option, + int third_option_val, int def_val, int *val_out) +{ + krb5_error_code retval; + char *str; + int match; + + retval = profile_get_boolean(ctx->profile, KRB5_CONF_LIBDEFAULTS, name, + NULL, def_val, val_out); + if (retval != PROF_BAD_BOOLEAN) + return retval; + retval = profile_get_string(ctx->profile, KRB5_CONF_LIBDEFAULTS, name, + NULL, NULL, &str); + if (retval) + return retval; + match = (strcasecmp(third_option, str) == 0); + free(str); + if (!match) + return EINVAL; + *val_out = third_option_val; + return 0; +} + krb5_error_code KRB5_CALLCONV krb5_init_context(krb5_context *context) { @@ -213,7 +237,8 @@ krb5_init_context_profile(profile_t profile, krb5_flags flags, goto cleanup; ctx->ignore_acceptor_hostname = tmp; - retval = get_boolean(ctx, KRB5_CONF_DNS_CANONICALIZE_HOSTNAME, 1, &tmp); + retval = get_tristate(ctx, KRB5_CONF_DNS_CANONICALIZE_HOSTNAME, "fallback", + CANONHOST_FALLBACK, 1, &tmp); if (retval) goto cleanup; ctx->dns_canonicalize_hostname = tmp; diff --git a/src/lib/krb5/krb/t_copy_context.c b/src/lib/krb5/krb/t_copy_context.c index fa810be8a..a6e48cd25 100644 --- a/src/lib/krb5/krb/t_copy_context.c +++ b/src/lib/krb5/krb/t_copy_context.c @@ -145,7 +145,7 @@ main(int argc, char **argv) ctx->udp_pref_limit = 2345; ctx->use_conf_ktypes = TRUE; ctx->ignore_acceptor_hostname = TRUE; - ctx->dns_canonicalize_hostname = FALSE; + ctx->dns_canonicalize_hostname = CANONHOST_FALSE; free(ctx->plugin_base_dir); check((ctx->plugin_base_dir = strdup("/a/b/c/d")) != NULL); diff --git a/src/lib/krb5/os/os-proto.h b/src/lib/krb5/os/os-proto.h index 634e82d70..066d30221 100644 --- a/src/lib/krb5/os/os-proto.h +++ b/src/lib/krb5/os/os-proto.h @@ -83,6 +83,10 @@ struct sendto_callback_info { void *data; }; +krb5_error_code k5_expand_hostname(krb5_context context, const char *host, + krb5_boolean is_fallback, + char **canonhost_out); + krb5_error_code k5_locate_server(krb5_context, const krb5_data *realm, struct serverlist *serverlist, enum locate_service_type svc, diff --git a/src/lib/krb5/os/sn2princ.c b/src/lib/krb5/os/sn2princ.c index 5932fd9b3..98d2600aa 100644 --- a/src/lib/krb5/os/sn2princ.c +++ b/src/lib/krb5/os/sn2princ.c @@ -53,19 +53,23 @@ use_reverse_dns(krb5_context context) return value; } -krb5_error_code KRB5_CALLCONV -krb5_expand_hostname(krb5_context context, const char *host, - char **canonhost_out) +krb5_error_code +k5_expand_hostname(krb5_context context, const char *host, + krb5_boolean is_fallback, char **canonhost_out) { struct addrinfo *ai = NULL, hint; char namebuf[NI_MAXHOST], *copy, *p; int err; const char *canonhost; + krb5_boolean use_dns; *canonhost_out = NULL; canonhost = host; - if (context->dns_canonicalize_hostname) { + use_dns = (context->dns_canonicalize_hostname == CANONHOST_TRUE || + (is_fallback && + context->dns_canonicalize_hostname == CANONHOST_FALLBACK)); + if (use_dns) { /* Try a forward lookup of the hostname. */ memset(&hint, 0, sizeof(hint)); hint.ai_flags = AI_CANONNAME; @@ -112,6 +116,13 @@ cleanup: return (*canonhost_out == NULL) ? ENOMEM : 0; } +krb5_error_code KRB5_CALLCONV +krb5_expand_hostname(krb5_context context, const char *host, + char **canonhost_out) +{ + return k5_expand_hostname(context, host, FALSE, canonhost_out); +} + /* If hostname appears to have a :port or :instance trailer (used in MSSQLSvc * principals), return a pointer to the separator. Otherwise return NULL. */ static const char * diff --git a/src/tests/gcred.c b/src/tests/gcred.c index b14e4fc9a..cac524c51 100644 --- a/src/tests/gcred.c +++ b/src/tests/gcred.c @@ -66,6 +66,7 @@ main(int argc, char **argv) krb5_principal client, server; krb5_ccache ccache; krb5_creds in_creds, *creds; + krb5_ticket *ticket; krb5_flags options = 0; char *name; int c; @@ -102,9 +103,11 @@ main(int argc, char **argv) in_creds.client = client; in_creds.server = server; check(krb5_get_credentials(ctx, options, ccache, &in_creds, &creds)); - check(krb5_unparse_name(ctx, creds->server, &name)); + check(krb5_decode_ticket(&creds->ticket, &ticket)); + check(krb5_unparse_name(ctx, ticket->server, &name)); printf("%s\n", name); + krb5_free_ticket(ctx, ticket); krb5_free_unparsed_name(ctx, name); krb5_free_creds(ctx, creds); krb5_free_principal(ctx, client); diff --git a/src/tests/t_sn2princ.py b/src/tests/t_sn2princ.py index 1ffda51f4..fe435a2d5 100755 --- a/src/tests/t_sn2princ.py +++ b/src/tests/t_sn2princ.py @@ -7,10 +7,15 @@ conf = {'domain_realm': {'kerberos.org': 'R1', 'mit.edu': 'R3'}} no_rdns_conf = {'libdefaults': {'rdns': 'false'}} no_canon_conf = {'libdefaults': {'dns_canonicalize_hostname': 'false'}} +fallback_canon_conf = {'libdefaults': + {'rdns': 'false', + 'dns_canonicalize_hostname': 'fallback'}} -realm = K5Realm(create_kdb=False, krb5_conf=conf) +realm = K5Realm(realm='R1', create_host=False, krb5_conf=conf) no_rdns = realm.special_env('no_rdns', False, krb5_conf=no_rdns_conf) no_canon = realm.special_env('no_canon', False, krb5_conf=no_canon_conf) +fallback_canon = realm.special_env('fallback_canon', False, + krb5_conf=fallback_canon_conf) def testbase(host, nametype, princhost, princrealm, env=None): # Run the sn2princ harness with a specified host and name type and @@ -37,6 +42,10 @@ def testu(host, princhost, princrealm): # Test with the unknown name type. testbase(host, 'unknown', princhost, princrealm) +def testfc(host, princhost, princrealm): + # Test with the host-based name type with canonicalization fallback. + testbase(host, 'srv-hst', princhost, princrealm, env=fallback_canon) + # With the unknown principal type, we do not canonicalize or downcase, # but we do remove a trailing period and look up the realm. mark('unknown type') @@ -71,6 +80,29 @@ if offline: oname = 'ptr-mismatch.kerberos.org' fname = 'www.kerberos.org' +# Test fallback canonicalization krb5_sname_to_principal() results +# (same as dns_canonicalize_hostname=false). +mark('dns_canonicalize_host=fallback') +testfc(oname, oname, 'R1') + +# Test fallback canonicalization in krb5_get_credentials(). +oprinc = 'host/' + oname +fprinc = 'host/' + fname +shutil.copy(realm.ccache, realm.ccache + '.save') +realm.addprinc(fprinc) +# oprinc doesn't exist, so we get the canonicalized fprinc as a fallback. +msgs = ('Falling back to canonicalized server hostname ' + fname,) +realm.run(['./gcred', 'srv-hst', oprinc], env=fallback_canon, + expected_msg=fprinc, expected_trace=msgs) +realm.addprinc(oprinc) +# oprinc now exists, but we still get the fprinc ticket from the cache. +realm.run(['./gcred', 'srv-hst', oprinc], env=fallback_canon, + expected_msg=fprinc) +# Without the cached result, we sould get oprinc in preference to fprinc. +os.rename(realm.ccache + '.save', realm.ccache) +realm.run(['./gcred', 'srv-hst', oprinc], env=fallback_canon, + expected_msg=oprinc) + # Verify forward resolution before testing for it. try: ai = socket.getaddrinfo(oname, None, 0, 0, 0, socket.AI_CANONNAME)