From e33835c4b6c6ce71757e9f659db03afa4bfd9a9a Mon Sep 17 00:00:00 2001
From: Greg Hudson <ghudson@mit.edu>
Date: Fri, 15 Jan 2021 13:51:34 -0500
Subject: [PATCH] Support host-based GSS initiator names

When checking if we can get initial credentials in the GSS krb5 mech,
use krb5_kt_have_match() to support fallback iteration.  When scanning
the ccache or getting initial credentials, rewrite cred->name->princ
to the canonical client name.  When a name check is necessary (such as
when the caller specifies both a name and ccache), use a new internal
API k5_sname_compare() to support fallback iteration.  Add fallback
iteration to krb5_cc_cache_match() to allow host-based names to be
canonicalized against the cache collection.

Create and store the matching principal for acceptor names in
acquire_accept_cred() so that it isn't affected by changes in
cred->name->princ during acquire_init_cred().

ticket: 8978 (new)
(cherry picked from commit c374ab40dd059a5938ffc0440d87457ac5da3a46)
---
 src/include/k5-int.h                     |  9 +++
 src/include/k5-trace.h                   |  3 +
 src/lib/gssapi/krb5/accept_sec_context.c | 15 +---
 src/lib/gssapi/krb5/acquire_cred.c       | 89 ++++++++++++++----------
 src/lib/gssapi/krb5/gssapiP_krb5.h       |  1 +
 src/lib/gssapi/krb5/rel_cred.c           |  1 +
 src/lib/krb5/ccache/cccursor.c           | 57 +++++++++++----
 src/lib/krb5/libkrb5.exports             |  1 +
 src/lib/krb5/os/sn2princ.c               | 23 +++++-
 src/lib/krb5_32.def                      |  1 +
 src/tests/gssapi/t_client_keytab.py      | 44 ++++++++++++
 src/tests/gssapi/t_credstore.py          | 32 +++++++++
 12 files changed, 214 insertions(+), 62 deletions(-)

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