c4016b4
From 818a777822658d44ce647fe975011a5ea25e8250 Mon Sep 17 00:00:00 2001
0dd40e4
From: Greg Hudson <ghudson@mit.edu>
0dd40e4
Date: Fri, 15 Jan 2021 13:51:34 -0500
0dd40e4
Subject: [PATCH] Support host-based GSS initiator names
0dd40e4
0dd40e4
When checking if we can get initial credentials in the GSS krb5 mech,
0dd40e4
use krb5_kt_have_match() to support fallback iteration.  When scanning
0dd40e4
the ccache or getting initial credentials, rewrite cred->name->princ
0dd40e4
to the canonical client name.  When a name check is necessary (such as
0dd40e4
when the caller specifies both a name and ccache), use a new internal
0dd40e4
API k5_sname_compare() to support fallback iteration.  Add fallback
0dd40e4
iteration to krb5_cc_cache_match() to allow host-based names to be
0dd40e4
canonicalized against the cache collection.
0dd40e4
0dd40e4
Create and store the matching principal for acceptor names in
0dd40e4
acquire_accept_cred() so that it isn't affected by changes in
0dd40e4
cred->name->princ during acquire_init_cred().
0dd40e4
0dd40e4
ticket: 8978 (new)
0dd40e4
(cherry picked from commit c374ab40dd059a5938ffc0440d87457ac5da3a46)
0dd40e4
---
0dd40e4
 src/include/k5-int.h                     |  9 +++
0dd40e4
 src/include/k5-trace.h                   |  3 +
0dd40e4
 src/lib/gssapi/krb5/accept_sec_context.c | 15 +---
0dd40e4
 src/lib/gssapi/krb5/acquire_cred.c       | 89 ++++++++++++++----------
0dd40e4
 src/lib/gssapi/krb5/gssapiP_krb5.h       |  1 +
0dd40e4
 src/lib/gssapi/krb5/rel_cred.c           |  1 +
0dd40e4
 src/lib/krb5/ccache/cccursor.c           | 57 +++++++++++----
0dd40e4
 src/lib/krb5/libkrb5.exports             |  1 +
0dd40e4
 src/lib/krb5/os/sn2princ.c               | 23 +++++-
0dd40e4
 src/lib/krb5_32.def                      |  1 +
0dd40e4
 src/tests/gssapi/t_client_keytab.py      | 44 ++++++++++++
0dd40e4
 src/tests/gssapi/t_credstore.py          | 32 +++++++++
0dd40e4
 12 files changed, 214 insertions(+), 62 deletions(-)
0dd40e4
0dd40e4
diff --git a/src/include/k5-int.h b/src/include/k5-int.h
0dd40e4
index efb523689..46f2ce2d3 100644
0dd40e4
--- a/src/include/k5-int.h
0dd40e4
+++ b/src/include/k5-int.h
0dd40e4
@@ -2411,4 +2411,13 @@ void k5_change_error_message_code(krb5_context ctx, krb5_error_code oldcode,
0dd40e4
 #define k5_prependmsg krb5_prepend_error_message
0dd40e4
 #define k5_wrapmsg krb5_wrap_error_message
0dd40e4
 
0dd40e4
+/*
0dd40e4
+ * Like krb5_principal_compare(), but with canonicalization of sname if
0dd40e4
+ * fallback is enabled.  This function should be avoided if multiple matches
0dd40e4
+ * are required, since repeated canonicalization is inefficient.
0dd40e4
+ */
0dd40e4
+krb5_boolean
0dd40e4
+k5_sname_compare(krb5_context context, krb5_const_principal sname,
0dd40e4
+                 krb5_const_principal princ);
0dd40e4
+
0dd40e4
 #endif /* _KRB5_INT_H */
0dd40e4
diff --git a/src/include/k5-trace.h b/src/include/k5-trace.h
0dd40e4
index b3e039dc8..79b5a7a85 100644
0dd40e4
--- a/src/include/k5-trace.h
0dd40e4
+++ b/src/include/k5-trace.h
0dd40e4
@@ -105,6 +105,9 @@ void krb5int_trace(krb5_context context, const char *fmt, ...);
0dd40e4
 
0dd40e4
 #endif /* DISABLE_TRACING */
0dd40e4
 
0dd40e4
+#define TRACE_CC_CACHE_MATCH(c, princ, ret)                             \
0dd40e4
+    TRACE(c, "Matching {princ} in collection with result: {kerr}",      \
0dd40e4
+          princ, ret)
0dd40e4
 #define TRACE_CC_DESTROY(c, cache)                      \
0dd40e4
     TRACE(c, "Destroying ccache {ccache}", cache)
0dd40e4
 #define TRACE_CC_GEN_NEW(c, cache)                                      \
0dd40e4
diff --git a/src/lib/gssapi/krb5/accept_sec_context.c b/src/lib/gssapi/krb5/accept_sec_context.c
0dd40e4
index fcf2c2152..a1d7e0d96 100644
0dd40e4
--- a/src/lib/gssapi/krb5/accept_sec_context.c
0dd40e4
+++ b/src/lib/gssapi/krb5/accept_sec_context.c
0dd40e4
@@ -683,7 +683,6 @@ kg_accept_krb5(minor_status, context_handle,
0dd40e4
     krb5_flags ap_req_options = 0;
0dd40e4
     krb5_enctype negotiated_etype;
0dd40e4
     krb5_authdata_context ad_context = NULL;
0dd40e4
-    krb5_principal accprinc = NULL;
0dd40e4
     krb5_ap_req *request = NULL;
0dd40e4
 
0dd40e4
     code = krb5int_accessor (&kaccess, KRB5INT_ACCESS_VERSION);
0dd40e4
@@ -849,17 +848,9 @@ kg_accept_krb5(minor_status, context_handle,
0dd40e4
         }
0dd40e4
     }
0dd40e4
 
0dd40e4
-    if (!cred->default_identity) {
0dd40e4
-        if ((code = kg_acceptor_princ(context, cred->name, &accprinc))) {
0dd40e4
-            major_status = GSS_S_FAILURE;
0dd40e4
-            goto fail;
0dd40e4
-        }
0dd40e4
-    }
0dd40e4
-
0dd40e4
-    code = krb5_rd_req_decoded(context, &auth_context, request, accprinc,
0dd40e4
-                               cred->keytab, &ap_req_options, NULL);
0dd40e4
-
0dd40e4
-    krb5_free_principal(context, accprinc);
0dd40e4
+    code = krb5_rd_req_decoded(context, &auth_context, request,
0dd40e4
+                               cred->acceptor_mprinc, cred->keytab,
0dd40e4
+                               &ap_req_options, NULL);
0dd40e4
     if (code) {
0dd40e4
         major_status = GSS_S_FAILURE;
0dd40e4
         goto fail;
0dd40e4
diff --git a/src/lib/gssapi/krb5/acquire_cred.c b/src/lib/gssapi/krb5/acquire_cred.c
0dd40e4
index 632ee7def..e226a0269 100644
0dd40e4
--- a/src/lib/gssapi/krb5/acquire_cred.c
0dd40e4
+++ b/src/lib/gssapi/krb5/acquire_cred.c
0dd40e4
@@ -123,11 +123,11 @@ gss_krb5int_register_acceptor_identity(OM_uint32 *minor_status,
0dd40e4
 /* Try to verify that keytab contains at least one entry for name.  Return 0 if
0dd40e4
  * it does, KRB5_KT_NOTFOUND if it doesn't, or another error as appropriate. */
0dd40e4
 static krb5_error_code
0dd40e4
-check_keytab(krb5_context context, krb5_keytab kt, krb5_gss_name_t name)
0dd40e4
+check_keytab(krb5_context context, krb5_keytab kt, krb5_gss_name_t name,
0dd40e4
+             krb5_principal mprinc)
0dd40e4
 {
0dd40e4
     krb5_error_code code;
0dd40e4
     krb5_keytab_entry ent;
0dd40e4
-    krb5_principal accprinc = NULL;
0dd40e4
     char *princname;
0dd40e4
 
0dd40e4
     if (name->service == NULL) {
0dd40e4
@@ -141,21 +141,15 @@ check_keytab(krb5_context context, krb5_keytab kt, krb5_gss_name_t name)
0dd40e4
     if (kt->ops->start_seq_get == NULL)
0dd40e4
         return 0;
0dd40e4
 
0dd40e4
-    /* Get the partial principal for the acceptor name. */
0dd40e4
-    code = kg_acceptor_princ(context, name, &accprinc);
0dd40e4
-    if (code)
0dd40e4
-        return code;
0dd40e4
-
0dd40e4
-    /* Scan the keytab for host-based entries matching accprinc. */
0dd40e4
-    code = k5_kt_have_match(context, kt, accprinc);
0dd40e4
+    /* Scan the keytab for host-based entries matching mprinc. */
0dd40e4
+    code = k5_kt_have_match(context, kt, mprinc);
0dd40e4
     if (code == KRB5_KT_NOTFOUND) {
0dd40e4
-        if (krb5_unparse_name(context, accprinc, &princname) == 0) {
0dd40e4
+        if (krb5_unparse_name(context, mprinc, &princname) == 0) {
0dd40e4
             k5_setmsg(context, code, _("No key table entry found matching %s"),
0dd40e4
                       princname);
0dd40e4
             free(princname);
0dd40e4
         }
0dd40e4
     }
0dd40e4
-    krb5_free_principal(context, accprinc);
0dd40e4
     return code;
0dd40e4
 }
0dd40e4
 
0dd40e4
@@ -202,8 +196,14 @@ acquire_accept_cred(krb5_context context, OM_uint32 *minor_status,
0dd40e4
     }
0dd40e4
 
0dd40e4
     if (cred->name != NULL) {
0dd40e4
+        code = kg_acceptor_princ(context, cred->name, &cred->acceptor_mprinc);
0dd40e4
+        if (code) {
0dd40e4
+            major = GSS_S_FAILURE;
0dd40e4
+            goto cleanup;
0dd40e4
+        }
0dd40e4
+
0dd40e4
         /* Make sure we have keys matching the desired name in the keytab. */
0dd40e4
-        code = check_keytab(context, kt, cred->name);
0dd40e4
+        code = check_keytab(context, kt, cred->name, cred->acceptor_mprinc);
0dd40e4
         if (code) {
0dd40e4
             if (code == KRB5_KT_NOTFOUND) {
0dd40e4
                 k5_change_error_message_code(context, code, KG_KEYTAB_NOMATCH);
0dd40e4
@@ -324,7 +324,6 @@ static krb5_boolean
0dd40e4
 can_get_initial_creds(krb5_context context, krb5_gss_cred_id_rec *cred)
0dd40e4
 {
0dd40e4
     krb5_error_code code;
0dd40e4
-    krb5_keytab_entry entry;
0dd40e4
 
0dd40e4
     if (cred->password != NULL)
0dd40e4
         return TRUE;
0dd40e4
@@ -336,20 +335,21 @@ can_get_initial_creds(krb5_context context, krb5_gss_cred_id_rec *cred)
0dd40e4
     if (cred->name == NULL)
0dd40e4
         return !krb5_kt_have_content(context, cred->client_keytab);
0dd40e4
 
0dd40e4
-    /* Check if we have a keytab key for the client principal. */
0dd40e4
-    code = krb5_kt_get_entry(context, cred->client_keytab, cred->name->princ,
0dd40e4
-                             0, 0, &entry);
0dd40e4
-    if (code) {
0dd40e4
-        krb5_clear_error_message(context);
0dd40e4
-        return FALSE;
0dd40e4
-    }
0dd40e4
-    krb5_free_keytab_entry_contents(context, &entry);
0dd40e4
-    return TRUE;
0dd40e4
+    /*
0dd40e4
+     * Check if we have a keytab key for the client principal.  This is a bit
0dd40e4
+     * more permissive than we really want because krb5_kt_have_match()
0dd40e4
+     * supports wildcarding and obeys ignore_acceptor_hostname, but that should
0dd40e4
+     * generally be harmless.
0dd40e4
+     */
0dd40e4
+    code = k5_kt_have_match(context, cred->client_keytab, cred->name->princ);
0dd40e4
+    return code == 0;
0dd40e4
 }
0dd40e4
 
0dd40e4
-/* Scan cred->ccache for name, expiry time, impersonator, refresh time. */
0dd40e4
+/* Scan cred->ccache for name, expiry time, impersonator, refresh time.  If
0dd40e4
+ * check_name is true, verify the cache name against the credential name. */
0dd40e4
 static krb5_error_code
0dd40e4
-scan_ccache(krb5_context context, krb5_gss_cred_id_rec *cred)
0dd40e4
+scan_ccache(krb5_context context, krb5_gss_cred_id_rec *cred,
0dd40e4
+            krb5_boolean check_name)
0dd40e4
 {
0dd40e4
     krb5_error_code code;
0dd40e4
     krb5_ccache ccache = cred->ccache;
0dd40e4
@@ -365,23 +365,31 @@ scan_ccache(krb5_context context, krb5_gss_cred_id_rec *cred)
0dd40e4
     if (code)
0dd40e4
         return code;
0dd40e4
 
0dd40e4
-    /* Credentials cache principal must match the initiator name. */
0dd40e4
     code = krb5_cc_get_principal(context, ccache, &ccache_princ);
0dd40e4
     if (code != 0)
0dd40e4
         goto cleanup;
0dd40e4
-    if (cred->name != NULL &&
0dd40e4
-        !krb5_principal_compare(context, ccache_princ, cred->name->princ)) {
0dd40e4
-        code = KG_CCACHE_NOMATCH;
0dd40e4
-        goto cleanup;
0dd40e4
-    }
0dd40e4
 
0dd40e4
-    /* Save the ccache principal as the credential name if not already set. */
0dd40e4
-    if (!cred->name) {
0dd40e4
+    if (cred->name == NULL) {
0dd40e4
+        /* Save the ccache principal as the credential name. */
0dd40e4
         code = kg_init_name(context, ccache_princ, NULL, NULL, NULL,
0dd40e4
                             KG_INIT_NAME_NO_COPY, &cred->name);
0dd40e4
         if (code)
0dd40e4
             goto cleanup;
0dd40e4
         ccache_princ = NULL;
0dd40e4
+    } else {
0dd40e4
+        /* Check against the desired name if needed. */
0dd40e4
+        if (check_name) {
0dd40e4
+            if (!k5_sname_compare(context, cred->name->princ, ccache_princ)) {
0dd40e4
+                code = KG_CCACHE_NOMATCH;
0dd40e4
+                goto cleanup;
0dd40e4
+            }
0dd40e4
+        }
0dd40e4
+
0dd40e4
+        /* Replace the credential name principal with the canonical client
0dd40e4
+         * principal, retaining acceptor_mprinc if set. */
0dd40e4
+        krb5_free_principal(context, cred->name->princ);
0dd40e4
+        cred->name->princ = ccache_princ;
0dd40e4
+        ccache_princ = NULL;
0dd40e4
     }
0dd40e4
 
0dd40e4
     assert(cred->name->princ != NULL);
0dd40e4
@@ -447,7 +455,7 @@ get_cache_for_name(krb5_context context, krb5_gss_cred_id_rec *cred)
0dd40e4
     assert(cred->name != NULL && cred->ccache == NULL);
0dd40e4
 #ifdef USE_LEASH
0dd40e4
     code = get_ccache_leash(context, cred->name->princ, &cred->ccache);
0dd40e4
-    return code ? code : scan_ccache(context, cred);
0dd40e4
+    return code ? code : scan_ccache(context, cred, TRUE);
0dd40e4
 #else
0dd40e4
     /* Check first whether we can acquire tickets, to avoid overwriting the
0dd40e4
      * extended error message from krb5_cc_cache_match. */
0dd40e4
@@ -456,7 +464,7 @@ get_cache_for_name(krb5_context context, krb5_gss_cred_id_rec *cred)
0dd40e4
     /* Look for an existing cache for the client principal. */
0dd40e4
     code = krb5_cc_cache_match(context, cred->name->princ, &cred->ccache);
0dd40e4
     if (code == 0)
0dd40e4
-        return scan_ccache(context, cred);
0dd40e4
+        return scan_ccache(context, cred, FALSE);
0dd40e4
     if (code != KRB5_CC_NOTFOUND || !can_get)
0dd40e4
         return code;
0dd40e4
     krb5_clear_error_message(context);
0dd40e4
@@ -633,6 +641,13 @@ get_initial_cred(krb5_context context, const struct verify_params *verify,
0dd40e4
     kg_cred_set_initial_refresh(context, cred, &creds.times);
0dd40e4
     cred->have_tgt = TRUE;
0dd40e4
     cred->expire = creds.times.endtime;
0dd40e4
+
0dd40e4
+    /* Steal the canonical client principal name from creds and save it in the
0dd40e4
+     * credential name, retaining acceptor_mprinc if set. */
0dd40e4
+    krb5_free_principal(context, cred->name->princ);
0dd40e4
+    cred->name->princ = creds.client;
0dd40e4
+    creds.client = NULL;
0dd40e4
+
0dd40e4
     krb5_free_cred_contents(context, &creds);
0dd40e4
 cleanup:
0dd40e4
     krb5_get_init_creds_opt_free(context, opt);
0dd40e4
@@ -721,7 +736,7 @@ acquire_init_cred(krb5_context context, OM_uint32 *minor_status,
0dd40e4
 
0dd40e4
     if (cred->ccache != NULL) {
0dd40e4
         /* The caller specified a ccache; check what's in it. */
0dd40e4
-        code = scan_ccache(context, cred);
0dd40e4
+        code = scan_ccache(context, cred, TRUE);
0dd40e4
         if (code == KRB5_FCC_NOFILE) {
0dd40e4
             /* See if we can get initial creds.  If the caller didn't specify
0dd40e4
              * a name, pick one from the client keytab. */
0dd40e4
@@ -984,7 +999,7 @@ kg_cred_resolve(OM_uint32 *minor_status, krb5_context context,
0dd40e4
             }
0dd40e4
         }
0dd40e4
         if (cred->ccache != NULL) {
0dd40e4
-            code = scan_ccache(context, cred);
0dd40e4
+            code = scan_ccache(context, cred, FALSE);
0dd40e4
             if (code)
0dd40e4
                 goto kerr;
0dd40e4
         }
0dd40e4
@@ -996,7 +1011,7 @@ kg_cred_resolve(OM_uint32 *minor_status, krb5_context context,
0dd40e4
         code = krb5int_cc_default(context, &cred->ccache);
0dd40e4
         if (code)
0dd40e4
             goto kerr;
0dd40e4
-        code = scan_ccache(context, cred);
0dd40e4
+        code = scan_ccache(context, cred, FALSE);
0dd40e4
         if (code == KRB5_FCC_NOFILE) {
0dd40e4
             /* Default ccache doesn't exist; fall through to client keytab. */
0dd40e4
             krb5_cc_close(context, cred->ccache);
0dd40e4
diff --git a/src/lib/gssapi/krb5/gssapiP_krb5.h b/src/lib/gssapi/krb5/gssapiP_krb5.h
0dd40e4
index 3bacdcd35..fd7abbd77 100644
0dd40e4
--- a/src/lib/gssapi/krb5/gssapiP_krb5.h
0dd40e4
+++ b/src/lib/gssapi/krb5/gssapiP_krb5.h
0dd40e4
@@ -175,6 +175,7 @@ typedef struct _krb5_gss_cred_id_rec {
0dd40e4
     /* name/type of credential */
0dd40e4
     gss_cred_usage_t usage;
0dd40e4
     krb5_gss_name_t name;
0dd40e4
+    krb5_principal acceptor_mprinc;
0dd40e4
     krb5_principal impersonator;
0dd40e4
     unsigned int default_identity : 1;
0dd40e4
     unsigned int iakerb_mech : 1;
0dd40e4
diff --git a/src/lib/gssapi/krb5/rel_cred.c b/src/lib/gssapi/krb5/rel_cred.c
0dd40e4
index a9515daf7..0da6c1b95 100644
0dd40e4
--- a/src/lib/gssapi/krb5/rel_cred.c
0dd40e4
+++ b/src/lib/gssapi/krb5/rel_cred.c
0dd40e4
@@ -72,6 +72,7 @@ krb5_gss_release_cred(minor_status, cred_handle)
0dd40e4
     if (cred->name)
0dd40e4
         kg_release_name(context, &cred->name);
0dd40e4
 
0dd40e4
+    krb5_free_principal(context, cred->acceptor_mprinc);
0dd40e4
     krb5_free_principal(context, cred->impersonator);
0dd40e4
 
0dd40e4
     if (cred->req_enctypes)
0dd40e4
diff --git a/src/lib/krb5/ccache/cccursor.c b/src/lib/krb5/ccache/cccursor.c
0dd40e4
index 8f5872116..760216d05 100644
0dd40e4
--- a/src/lib/krb5/ccache/cccursor.c
0dd40e4
+++ b/src/lib/krb5/ccache/cccursor.c
0dd40e4
@@ -30,6 +30,7 @@
0dd40e4
 
0dd40e4
 #include "cc-int.h"
0dd40e4
 #include "../krb/int-proto.h"
0dd40e4
+#include "../os/os-proto.h"
0dd40e4
 
0dd40e4
 #include <assert.h>
0dd40e4
 
0dd40e4
@@ -141,18 +142,18 @@ krb5_cccol_cursor_free(krb5_context context,
0dd40e4
     return 0;
0dd40e4
 }
0dd40e4
 
0dd40e4
-krb5_error_code KRB5_CALLCONV
0dd40e4
-krb5_cc_cache_match(krb5_context context, krb5_principal client,
0dd40e4
-                    krb5_ccache *cache_out)
0dd40e4
+static krb5_error_code
0dd40e4
+match_caches(krb5_context context, krb5_const_principal client,
0dd40e4
+             krb5_ccache *cache_out)
0dd40e4
 {
0dd40e4
     krb5_error_code ret;
0dd40e4
     krb5_cccol_cursor cursor;
0dd40e4
     krb5_ccache cache = NULL;
0dd40e4
     krb5_principal princ;
0dd40e4
-    char *name;
0dd40e4
     krb5_boolean eq;
0dd40e4
 
0dd40e4
     *cache_out = NULL;
0dd40e4
+
0dd40e4
     ret = krb5_cccol_cursor_new(context, &cursor);
0dd40e4
     if (ret)
0dd40e4
         return ret;
0dd40e4
@@ -169,20 +170,52 @@ krb5_cc_cache_match(krb5_context context, krb5_principal client,
0dd40e4
         krb5_cc_close(context, cache);
0dd40e4
     }
0dd40e4
     krb5_cccol_cursor_free(context, &cursor);
0dd40e4
+
0dd40e4
     if (ret)
0dd40e4
         return ret;
0dd40e4
-    if (cache == NULL) {
0dd40e4
-        ret = krb5_unparse_name(context, client, &name);
0dd40e4
-        if (ret == 0) {
0dd40e4
-            k5_setmsg(context, KRB5_CC_NOTFOUND,
0dd40e4
+    if (cache == NULL)
0dd40e4
+        return KRB5_CC_NOTFOUND;
0dd40e4
+
0dd40e4
+    *cache_out = cache;
0dd40e4
+    return 0;
0dd40e4
+}
0dd40e4
+
0dd40e4
+krb5_error_code KRB5_CALLCONV
0dd40e4
+krb5_cc_cache_match(krb5_context context, krb5_principal client,
0dd40e4
+                    krb5_ccache *cache_out)
0dd40e4
+{
0dd40e4
+    krb5_error_code ret;
0dd40e4
+    struct canonprinc iter = { client, .subst_defrealm = TRUE };
0dd40e4
+    krb5_const_principal canonprinc = NULL;
0dd40e4
+    krb5_ccache cache = NULL;
0dd40e4
+    char *name;
0dd40e4
+
0dd40e4
+    *cache_out = NULL;
0dd40e4
+
0dd40e4
+    while ((ret = k5_canonprinc(context, &iter, &canonprinc)) == 0 &&
0dd40e4
+           canonprinc != NULL) {
0dd40e4
+        ret = match_caches(context, canonprinc, &cache);
0dd40e4
+        if (ret != KRB5_CC_NOTFOUND)
0dd40e4
+            break;
0dd40e4
+    }
0dd40e4
+    free_canonprinc(&iter);
0dd40e4
+
0dd40e4
+    if (ret == 0 && canonprinc == NULL) {
0dd40e4
+        ret = KRB5_CC_NOTFOUND;
0dd40e4
+        if (krb5_unparse_name(context, client, &name) == 0) {
0dd40e4
+            k5_setmsg(context, ret,
0dd40e4
                       _("Can't find client principal %s in cache collection"),
0dd40e4
                       name);
0dd40e4
             krb5_free_unparsed_name(context, name);
0dd40e4
         }
0dd40e4
-        ret = KRB5_CC_NOTFOUND;
0dd40e4
-    } else
0dd40e4
-        *cache_out = cache;
0dd40e4
-    return ret;
0dd40e4
+    }
0dd40e4
+
0dd40e4
+    TRACE_CC_CACHE_MATCH(context, client, ret);
0dd40e4
+    if (ret)
0dd40e4
+        return ret;
0dd40e4
+
0dd40e4
+    *cache_out = cache;
0dd40e4
+    return 0;
0dd40e4
 }
0dd40e4
 
0dd40e4
 /* Store the error state for code from context into errsave, but only if code
0dd40e4
diff --git a/src/lib/krb5/libkrb5.exports b/src/lib/krb5/libkrb5.exports
3faaf11
index adbfa332b..df6e2ffbe 100644
0dd40e4
--- a/src/lib/krb5/libkrb5.exports
0dd40e4
+++ b/src/lib/krb5/libkrb5.exports
0dd40e4
@@ -181,6 +181,7 @@ k5_size_authdata_context
0dd40e4
 k5_size_context
0dd40e4
 k5_size_keyblock
0dd40e4
 k5_size_principal
0dd40e4
+k5_sname_compare
0dd40e4
 k5_unmarshal_cred
0dd40e4
 k5_unmarshal_princ
0dd40e4
 k5_unwrap_cammac_svc
0dd40e4
diff --git a/src/lib/krb5/os/sn2princ.c b/src/lib/krb5/os/sn2princ.c
0dd40e4
index 8b7214189..c99b7da17 100644
0dd40e4
--- a/src/lib/krb5/os/sn2princ.c
0dd40e4
+++ b/src/lib/krb5/os/sn2princ.c
0dd40e4
@@ -277,7 +277,8 @@ k5_canonprinc(krb5_context context, struct canonprinc *iter,
0dd40e4
 
0dd40e4
     /* If we're not doing fallback, the input principal is canonical. */
0dd40e4
     if (context->dns_canonicalize_hostname != CANONHOST_FALLBACK ||
0dd40e4
-        iter->princ->type != KRB5_NT_SRV_HST || iter->princ->length != 2) {
0dd40e4
+        iter->princ->type != KRB5_NT_SRV_HST || iter->princ->length != 2 ||
0dd40e4
+        iter->princ->data[1].length == 0) {
0dd40e4
         *princ_out = (step == 1) ? iter->princ : NULL;
0dd40e4
         return 0;
0dd40e4
     }
0dd40e4
@@ -288,6 +289,26 @@ k5_canonprinc(krb5_context context, struct canonprinc *iter,
0dd40e4
     return canonicalize_princ(context, iter, step == 2, princ_out);
0dd40e4
 }
0dd40e4
 
0dd40e4
+krb5_boolean
0dd40e4
+k5_sname_compare(krb5_context context, krb5_const_principal sname,
0dd40e4
+                 krb5_const_principal princ)
0dd40e4
+{
0dd40e4
+    krb5_error_code ret;
0dd40e4
+    struct canonprinc iter = { sname, .subst_defrealm = TRUE };
0dd40e4
+    krb5_const_principal canonprinc = NULL;
0dd40e4
+    krb5_boolean match = FALSE;
0dd40e4
+
0dd40e4
+    while ((ret = k5_canonprinc(context, &iter, &canonprinc)) == 0 &&
0dd40e4
+           canonprinc != NULL) {
0dd40e4
+        if (krb5_principal_compare(context, canonprinc, princ)) {
0dd40e4
+            match = TRUE;
0dd40e4
+            break;
0dd40e4
+        }
0dd40e4
+    }
0dd40e4
+    free_canonprinc(&iter);
0dd40e4
+    return match;
0dd40e4
+}
0dd40e4
+
0dd40e4
 krb5_error_code KRB5_CALLCONV
0dd40e4
 krb5_sname_to_principal(krb5_context context, const char *hostname,
0dd40e4
                         const char *sname, krb5_int32 type,
0dd40e4
diff --git a/src/lib/krb5_32.def b/src/lib/krb5_32.def
0dd40e4
index 60b8dd311..cf690dbe4 100644
0dd40e4
--- a/src/lib/krb5_32.def
0dd40e4
+++ b/src/lib/krb5_32.def
0dd40e4
@@ -507,3 +507,4 @@ EXPORTS
0dd40e4
 ; new in 1.20
0dd40e4
 	krb5_marshal_credentials			@472
0dd40e4
 	krb5_unmarshal_credentials			@473
0dd40e4
+	k5_sname_compare				@474 ; PRIVATE GSSAPI
0dd40e4
diff --git a/src/tests/gssapi/t_client_keytab.py b/src/tests/gssapi/t_client_keytab.py
0dd40e4
index 7847b3ecd..9a61d53b8 100755
0dd40e4
--- a/src/tests/gssapi/t_client_keytab.py
0dd40e4
+++ b/src/tests/gssapi/t_client_keytab.py
0dd40e4
@@ -141,5 +141,49 @@ msgs = ('Getting initial credentials for user/admin@KRBTEST.COM',
0dd40e4
         '/Matching credential not found')
0dd40e4
 realm.run(['./t_ccselect', phost], expected_code=1,
0dd40e4
           expected_msg='Ticket expired', expected_trace=msgs)
0dd40e4
+realm.run([kdestroy, '-A'])
0dd40e4
+
0dd40e4
+# Test 19: host-based initiator name
0dd40e4
+mark('host-based initiator name')
0dd40e4
+hsvc = 'h:svc@' + hostname
0dd40e4
+svcprinc = 'svc/%s@%s' % (hostname, realm.realm)
0dd40e4
+realm.addprinc(svcprinc)
0dd40e4
+realm.extract_keytab(svcprinc, realm.client_keytab)
0dd40e4
+# On the first run we match against the keytab while getting tickets,
0dd40e4
+# substituting the default realm.
0dd40e4
+msgs = ('/Can\'t find client principal svc/%s@ in' % hostname,
0dd40e4
+        'Getting initial credentials for svc/%s@' % hostname,
0dd40e4
+        'Found entries for %s in keytab' % svcprinc,
0dd40e4
+        'Retrieving %s from FILE:%s' % (svcprinc, realm.client_keytab),
0dd40e4
+        'Storing %s -> %s in' % (svcprinc, realm.krbtgt_princ),
0dd40e4
+        'Retrieving %s -> %s from' % (svcprinc, realm.krbtgt_princ),
0dd40e4
+        'authenticator for %s -> %s' % (svcprinc, realm.host_princ))
0dd40e4
+realm.run(['./t_ccselect', phost, hsvc], expected_trace=msgs)
0dd40e4
+# On the second run we match against the collection.
0dd40e4
+msgs = ('Matching svc/%s@ in collection with result: 0' % hostname,
0dd40e4
+        'Getting credentials %s -> %s' % (svcprinc, realm.host_princ),
0dd40e4
+        'authenticator for %s -> %s' % (svcprinc, realm.host_princ))
0dd40e4
+realm.run(['./t_ccselect', phost, hsvc], expected_trace=msgs)
0dd40e4
+realm.run([kdestroy, '-A'])
0dd40e4
+
0dd40e4
+# Test 20: host-based initiator name with fallback
0dd40e4
+mark('host-based fallback initiator name')
0dd40e4
+canonname = canonicalize_hostname(hostname)
0dd40e4
+if canonname != hostname:
0dd40e4
+    hfsvc = 'h:fsvc@' + hostname
0dd40e4
+    canonprinc = 'fsvc/%s@%s' % (canonname, realm.realm)
0dd40e4
+    realm.addprinc(canonprinc)
0dd40e4
+    realm.extract_keytab(canonprinc, realm.client_keytab)
0dd40e4
+    msgs = ('/Can\'t find client principal fsvc/%s@ in' % hostname,
0dd40e4
+            'Found entries for %s in keytab' % canonprinc,
0dd40e4
+            'authenticator for %s -> %s' % (canonprinc, realm.host_princ))
0dd40e4
+    realm.run(['./t_ccselect', phost, hfsvc], expected_trace=msgs)
0dd40e4
+    msgs = ('Matching fsvc/%s@ in collection with result: 0' % hostname,
0dd40e4
+            'Getting credentials %s -> %s' % (canonprinc, realm.host_princ))
0dd40e4
+    realm.run(['./t_ccselect', phost, hfsvc], expected_trace=msgs)
0dd40e4
+    realm.run([kdestroy, '-A'])
0dd40e4
+else:
0dd40e4
+    skipped('GSS initiator name fallback test',
0dd40e4
+            '%s does not canonicalize to a different name' % hostname)
0dd40e4
 
0dd40e4
 success('Client keytab tests')
0dd40e4
diff --git a/src/tests/gssapi/t_credstore.py b/src/tests/gssapi/t_credstore.py
0dd40e4
index c11975bf5..9be57bb82 100644
0dd40e4
--- a/src/tests/gssapi/t_credstore.py
0dd40e4
+++ b/src/tests/gssapi/t_credstore.py
0dd40e4
@@ -15,6 +15,38 @@ msgs = ('Storing %s -> %s in %s' % (service_cs, realm.krbtgt_princ,
0dd40e4
 realm.run(['./t_credstore', '-s', 'p:' + service_cs, 'ccache', storagecache,
0dd40e4
            'keytab', servicekeytab], expected_trace=msgs)
0dd40e4
 
0dd40e4
+mark('matching')
0dd40e4
+scc = 'FILE:' + os.path.join(realm.testdir, 'service_cache')
0dd40e4
+realm.kinit(realm.host_princ, flags=['-k', '-c', scc])
0dd40e4
+realm.run(['./t_credstore', '-i', 'p:' + realm.host_princ, 'ccache', scc])
0dd40e4
+realm.run(['./t_credstore', '-i', 'h:host', 'ccache', scc])
0dd40e4
+realm.run(['./t_credstore', '-i', 'h:host@' + hostname, 'ccache', scc])
0dd40e4
+realm.run(['./t_credstore', '-i', 'p:wrong', 'ccache', scc],
0dd40e4
+          expected_code=1, expected_msg='does not match desired name')
0dd40e4
+realm.run(['./t_credstore', '-i', 'h:host@-nomatch-', 'ccache', scc],
0dd40e4
+          expected_code=1, expected_msg='does not match desired name')
0dd40e4
+realm.run(['./t_credstore', '-i', 'h:svc', 'ccache', scc],
0dd40e4
+          expected_code=1, expected_msg='does not match desired name')
0dd40e4
+
0dd40e4
+mark('matching (fallback)')
0dd40e4
+canonname = canonicalize_hostname(hostname)
0dd40e4
+if canonname != hostname:
0dd40e4
+    canonprinc = 'host/%s@%s' % (canonname, realm.realm)
0dd40e4
+    realm.addprinc(canonprinc)
0dd40e4
+    realm.extract_keytab(canonprinc, realm.keytab)
0dd40e4
+    realm.kinit(canonprinc, flags=['-k', '-c', scc])
0dd40e4
+    realm.run(['./t_credstore', '-i', 'h:host', 'ccache', scc])
0dd40e4
+    realm.run(['./t_credstore', '-i', 'h:host@' + hostname, 'ccache', scc])
0dd40e4
+    realm.run(['./t_credstore', '-i', 'h:host@' + canonname, 'ccache', scc])
0dd40e4
+    realm.run(['./t_credstore', '-i', 'p:' + canonprinc, 'ccache', scc])
0dd40e4
+    realm.run(['./t_credstore', '-i', 'p:' + realm.host_princ, 'ccache', scc],
0dd40e4
+              expected_code=1, expected_msg='does not match desired name')
0dd40e4
+    realm.run(['./t_credstore', '-i', 'h:host@-nomatch-', 'ccache', scc],
0dd40e4
+              expected_code=1, expected_msg='does not match desired name')
0dd40e4
+else:
0dd40e4
+    skipped('fallback matching test',
0dd40e4
+            '%s does not canonicalize to a different name' % hostname)
0dd40e4
+
0dd40e4
 mark('rcache')
0dd40e4
 # t_credstore -r should produce a replay error normally, but not with
0dd40e4
 # rcache set to "none:".