krb5 commit: Add dns_canonicalize_hostname=fallback support

Greg Hudson <[email protected]>
Newsgroups gmane.comp.encryption.kerberos.cvs
Message-ID <[email protected]>
https://github.com/krb5/krb5/commit/6c20cb1c89acaa03db897182a3b28d5f8f284907
commit 6c20cb1c89acaa03db897182a3b28d5f8f284907
Author: Simo Sorce <[email protected]>
Date:   Tue Dec 4 15:22:55 2018 -0500

    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.
    
    [[email protected]: added TGS tests; refactored code; edited commit
    message and documentation]
    
    ticket: 8765 (new)

 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 7b4389f..e9f7e8c 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 6522422..aceae36 100644
--- a/src/include/k5-int.h
+++ b/src/include/k5-int.h
@@ -1158,6 +1158,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;
@@ -1221,7 +1227,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 2aa379b..f3ed6a4 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 69900ad..0a04d68 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 947e504..d263d5c 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 fa810be..a6e48cd 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 634e82d..066d302 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 5932fd9..98d2600 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 b14e4fc..cac524c 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 1ffda51..fe435a2 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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.