krb5 commit: Add client keytab initiation support

Greg Hudson ghudson at MIT.EDU
Mon Jul 2 02:24:59 EDT 2012


https://github.com/krb5/krb5/commit/8651f3339ccc5a623172a8edfb9cf522883acacd
commit 8651f3339ccc5a623172a8edfb9cf522883acacd
Author: Greg Hudson <ghudson at mit.edu>
Date:   Fri Jun 22 12:48:26 2012 -0400

    Add client keytab initiation support
    
    Support acquiring GSSAPI krb5 credentials by fetching initial
    credentials using the client keytab.  Credentials obtained this way
    will be stored in the default ccache or collection, and will be
    refreshed when they are halfway to expiring.
    
    ticket: 7189 (new)

 doc/rst_source/krb_appldev/gssapi.rst  |   62 ++++
 src/appl/gss-sample/t_gss_sample.py    |   23 +-
 src/include/k5-int.h                   |    1 +
 src/lib/gssapi/krb5/acquire_cred.c     |  511 ++++++++++++++++++++++----------
 src/lib/gssapi/krb5/gssapiP_krb5.h     |   10 +
 src/lib/gssapi/krb5/iakerb.c           |   68 ++---
 src/lib/gssapi/krb5/init_sec_context.c |   24 --
 src/lib/gssapi/krb5/rel_cred.c         |    3 +
 src/lib/gssapi/krb5/val_cred.c         |    2 +-
 src/tests/gssapi/Makefile.in           |    8 +-
 src/tests/gssapi/ccinit.c              |   72 +++++
 src/tests/gssapi/ccrefresh.c           |   80 +++++
 src/tests/gssapi/t_ccselect.py         |    2 +-
 src/tests/gssapi/t_client_keytab.py    |  132 ++++++++
 14 files changed, 767 insertions(+), 231 deletions(-)

diff --git a/doc/rst_source/krb_appldev/gssapi.rst b/doc/rst_source/krb_appldev/gssapi.rst
index 4556c14..d22a63f 100644
--- a/doc/rst_source/krb_appldev/gssapi.rst
+++ b/doc/rst_source/krb_appldev/gssapi.rst
@@ -53,6 +53,66 @@ name types are supported by the krb5 mechanism:
   gss_export_name_ call.
 
 
+Initiator credentials
+---------------------
+
+A GSSAPI client application uses gss_init_sec_context_ to establish a
+security context.  The *initiator_cred_handle* parameter determines
+what tickets are used to establish the connection.  An application can
+either pass **GSS_C_NO_CREDENTIAL** to use the default client
+credential, or it can use gss_acquire_cred_ beforehand to acquire an
+initiator credential.  The call to gss_acquire_cred_ may include a
+*desired_name* parameter, or it may pass **GSS_C_NO_NAME** if it does
+not have a specific name preference.
+
+If the desired name for a krb5 initiator credential is a host-based
+name, it is converted to a principal name of the form
+``service/hostname`` in the local realm, where *hostname* is the local
+hostname if not specified.  The hostname will be canonicalized using
+forward name resolution, and possibly also using reverse name
+resolution depending on the value of the **rdns** variable in
+:ref:`libdefaults`.
+
+If a desired name is specified in the call to gss_acquire_cred_, the
+krb5 mechanism will attempt to find existing tickets for that client
+principal name in the default credential cache or collection.  If the
+default cache type does not support a collection, and the default
+cache contains credentials for a different principal than the desired
+name, a **GSS_S_CRED_UNAVAIL** error will be returned with a minor
+code indicating a mismatch.
+
+If no existing tickets are available for the desired name, but the
+name has an entry in the default client :ref:`keytab_definition`, the
+krb5 mechanism will acquire initial tickets for the name using the
+default client keytab.
+
+If no desired name is specified, credential acquisition will be
+deferred until the credential is used in a call to
+gss_init_sec_context_ or gss_inquire_cred_.  If the call is to
+gss_init_sec_context_, the target name will be used to choose a client
+principal name using the credential cache selection facility.  (This
+facility might, for instance, try to choose existing tickets for a
+client principal in the same realm as the target service).  If there
+are no existing tickets for the chosen principal, but it is present in
+the default client keytab, the krb5 mechanism will acquire initial
+tickets using the keytab.
+
+If the target name cannot be used to select a client principal
+(because the credentials are used in a call to gss_inquire_cred_), or
+if the credential cache selection facility cannot choose a principal
+for it, the default credential cache will be selected if it exists and
+contains tickets.
+
+If the default credential cache does exist, but the default keytab
+does exist, the krb5 mechanism will try to acquire initial tickets for
+the first principal in the default client keytab.
+
+If the krb5 mechanism acquires initial tickets using the default
+client keytab, the resulting tickets will be stored in the default
+cache or collection, and will be refreshed by future calls to
+gss_acquire_cred_ as they approach their expire time.
+
+
 Acceptor names
 --------------
 
@@ -108,3 +168,5 @@ allowed to authenticate to that principal in the default keytab.
 .. _gss_acquire_cred: http://tools.ietf.org/html/rfc2744.html#section-5.2
 .. _gss_export_name: http://tools.ietf.org/html/rfc2744.html#section-5.13
 .. _gss_import_name: http://tools.ietf.org/html/rfc2744.html#section-5.16
+.. _gss_init_sec_context: http://tools.ietf.org/html/rfc2744.html#section-5.19
+.. _gss_inquire_cred: http://tools.ietf.org/html/rfc2744.html#section-5.21
diff --git a/src/appl/gss-sample/t_gss_sample.py b/src/appl/gss-sample/t_gss_sample.py
index cfac43d..211da97 100644
--- a/src/appl/gss-sample/t_gss_sample.py
+++ b/src/appl/gss-sample/t_gss_sample.py
@@ -61,11 +61,17 @@ def tgs_test(realm, options):
 
 # Perform a test of the server and client with initial credentials
 # obtained through gss_acquire_cred_with_password().
-def as_test(realm, options):
+def pw_test(realm, options):
     os.remove(realm.ccache)
     server_client_test(realm, options + ['-user', realm.user_princ,
                                          '-pass', password('user')])
 
+# Perform a test of the server and client with initial credentials
+# obtained with the client keytab
+def kt_test(realm, options):
+    os.remove(realm.ccache)
+    server_client_test(realm, options)
+
 for realm in multipass_realms():
     ccache_save(realm)
 
@@ -75,10 +81,15 @@ for realm in multipass_realms():
     # test default (i.e., krb5) mechanism with GSS_C_DCE_STYLE
     tgs_test(realm, ['-dce'])
 
-    as_test(realm, ['-krb5'])
-    as_test(realm, ['-spnego'])
-    as_test(realm, ['-iakerb'])
-    # test default (i.e., krb5) mechanism with GSS_C_DCE_STYLE
-    as_test(realm, ['-dce'])
+    pw_test(realm, ['-krb5'])
+    pw_test(realm, ['-spnego'])
+    pw_test(realm, ['-iakerb'])
+    pw_test(realm, ['-dce'])
+
+    realm.extract_keytab(realm.user_princ, realm.client_keytab)
+    kt_test(realm, ['-krb5'])
+    kt_test(realm, ['-spnego'])
+    kt_test(realm, ['-iakerb'])
+    kt_test(realm, ['-dce'])
 
 success('GSS sample application')
diff --git a/src/include/k5-int.h b/src/include/k5-int.h
index 69d30b3..c426aca 100644
--- a/src/include/k5-int.h
+++ b/src/include/k5-int.h
@@ -280,6 +280,7 @@ typedef INT64_TYPE krb5_int64;
 /* Cache configuration variables */
 #define KRB5_CONF_FAST_AVAIL                  "fast_avail"
 #define KRB5_CONF_PROXY_IMPERSONATOR          "proxy_impersonator"
+#define KRB5_CONF_REFRESH_TIME                "refresh_time"
 
 /* Error codes used in KRB_ERROR protocol messages.
    Return values of library routines are based on a different error table
diff --git a/src/lib/gssapi/krb5/acquire_cred.c b/src/lib/gssapi/krb5/acquire_cred.c
index 8b31990..1972b1e 100644
--- a/src/lib/gssapi/krb5/acquire_cred.c
+++ b/src/lib/gssapi/krb5/acquire_cred.c
@@ -185,7 +185,6 @@ cleanup:
 static OM_uint32
 acquire_accept_cred(krb5_context context,
                     OM_uint32 *minor_status,
-                    krb5_gss_name_t desired_name,
                     krb5_keytab req_keytab,
                     krb5_gss_cred_id_rec *cred)
 {
@@ -223,9 +222,9 @@ acquire_accept_cred(krb5_context context,
         return GSS_S_CRED_UNAVAIL;
     }
 
-    if (desired_name != NULL) {
+    if (cred->name != NULL) {
         /* Make sure we keys matching the desired name in the keytab. */
-        code = check_keytab(context, kt, desired_name);
+        code = check_keytab(context, kt, cred->name);
         if (code) {
             krb5_kt_close(context, kt);
             if (code == KRB5_KT_NOTFOUND) {
@@ -238,15 +237,8 @@ acquire_accept_cred(krb5_context context,
             return GSS_S_CRED_UNAVAIL;
         }
 
-        assert(cred->name == NULL);
-        code = kg_duplicate_name(context, desired_name, &cred->name);
-        if (code) {
-            *minor_status = code;
-            return GSS_S_FAILURE;
-        }
-
         /* Open the replay cache for this principal. */
-        code = krb5_get_server_rcache(context, &desired_name->princ->data[0],
+        code = krb5_get_server_rcache(context, &cred->name->princ->data[0],
                                       &cred->rcache);
         if (code) {
             *minor_status = code;
@@ -307,65 +299,6 @@ get_ccache_leash(krb5_context context, krb5_principal desired_princ,
 }
 #endif /* USE_LEASH */
 
-/* Prepare to acquire credentials into ccache using password at
- * init_sec_context time.  On success, cred takes ownership of ccache. */
-static krb5_error_code
-prep_ccache(krb5_context context, krb5_gss_cred_id_rec *cred,
-            krb5_ccache ccache, krb5_principal desired_princ,
-            gss_buffer_t password)
-{
-    krb5_error_code code;
-    krb5_principal ccache_princ;
-    krb5_data pwdata = make_data(password->value, password->length), pwcopy;
-    krb5_boolean eq;
-    const char *cctype;
-    krb5_ccache newcache = NULL;
-
-    /* Check the ccache principal or initialize a new cache. */
-    code = krb5_cc_get_principal(context, ccache, &ccache_princ);
-    if (code == 0) {
-        eq = krb5_principal_compare(context, ccache_princ, desired_princ);
-        krb5_free_principal(context, ccache_princ);
-        if (!eq) {
-            cctype = krb5_cc_get_type(context, ccache);
-            if (krb5_cc_support_switch(context, cctype)) {
-                /* Make a new ccache within the collection. */
-                code = krb5_cc_new_unique(context, cctype, NULL, &newcache);
-                if (code)
-                    return code;
-            } else
-                return KG_CCACHE_NOMATCH;
-        }
-    } else if (code == KRB5_FCC_NOFILE) {
-        /* Cache file does not exist; create and initialize one. */
-        code = krb5_cc_initialize(context, ccache, desired_princ);
-        if (code)
-            return code;
-    } else
-        return code;
-
-    /* Save the desired principal as the credential name if not already set. */
-    if (!cred->name) {
-        code = kg_init_name(context, desired_princ, NULL, NULL, NULL, 0,
-                            &cred->name);
-        if (code)
-            return code;
-    }
-
-    /* Stash the password for later. */
-    code = krb5int_copy_data_contents_add0(context, &pwdata, &pwcopy);
-    if (code)
-        return code;
-    cred->password = pwcopy.data;
-
-    if (newcache) {
-        krb5_cc_close(context, ccache);
-        cred->ccache = newcache;
-    } else
-        cred->ccache = ccache;
-    return 0;
-}
-
 /* Set fields in cred according to a ccache config entry whose key (in
  * principal form) is config_princ and whose value is value. */
 static krb5_error_code
@@ -386,36 +319,67 @@ scan_cc_config(krb5_context context, krb5_gss_cred_id_rec *cred,
         krb5_free_data_contents(context, &data0);
         if (code)
             return code;
+    } else if (data_eq_string(config_princ->data[1], KRB5_CONF_REFRESH_TIME) &&
+               cred->refresh_time == 0) {
+        code = krb5int_copy_data_contents_add0(context, value, &data0);
+        if (code)
+            return code;
+        cred->refresh_time = atol(data0.data);
+        krb5_free_data_contents(context, &data0);
     }
     return 0;
 }
 
-/* Check ccache and scan it for its expiry time.  On success, cred takes
- * ownership of ccache. */
+/* Return true if it appears that we can non-interactively get initial
+ * tickets for cred. */
+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;
+
+    /* If we don't know the client principal yet, check for any keytab keys. */
+    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;
+}
+
+/* Scan cred->ccache for name, expiry time, impersonator, refresh time. */
 static krb5_error_code
-scan_ccache(krb5_context context, krb5_gss_cred_id_rec *cred,
-            krb5_ccache ccache, krb5_principal desired_princ)
+scan_ccache(krb5_context context, krb5_gss_cred_id_rec *cred)
 {
     krb5_error_code code;
+    krb5_ccache ccache = cred->ccache;
     krb5_principal ccache_princ = NULL, tgt_princ = NULL;
     krb5_data *realm;
     krb5_cc_cursor cursor;
     krb5_creds creds;
     krb5_timestamp endtime;
-    int got_endtime = 0, is_tgt;
+    krb5_boolean is_tgt;
 
     /* Turn off OPENCLOSE mode while extensive frobbing is going on. */
     code = krb5_cc_set_flags(context, ccache, 0);
     if (code)
         return code;
 
+    /* Credentials cache principal must match the initiator name. */
     code = krb5_cc_get_principal(context, ccache, &ccache_princ);
     if (code != 0)
-        return code;
-
-    /* Credentials cache principal must match the initiator name. */
-    if (desired_princ != NULL &&
-        !krb5_principal_compare(context, ccache_princ, desired_princ)) {
+        goto cleanup;
+    if (cred->name != NULL &&
+        !krb5_principal_compare(context, ccache_princ, cred->name->princ)) {
         code = KG_CCACHE_NOMATCH;
         goto cleanup;
     }
@@ -457,109 +421,298 @@ scan_ccache(krb5_context context, krb5_gss_cred_id_rec *cred,
         is_tgt = krb5_principal_compare(context, tgt_princ, creds.server);
         endtime = creds.times.endtime;
         krb5_free_cred_contents(context, &creds);
-        if (is_tgt || !got_endtime)
-            cred->expire = creds.times.endtime;
-        got_endtime = 1;
+        if (is_tgt)
+            cred->have_tgt = TRUE;
+        if (is_tgt || cred->expire == 0)
+            cred->expire = endtime;
     }
     krb5_cc_end_seq_get(context, ccache, &cursor);
     if (code && code != KRB5_CC_END)
         goto cleanup;
     code = 0;
 
-    if (!got_endtime) {         /* ccache is empty. */
+    if (cred->expire == 0 && !can_get_initial_creds(context, cred)) {
         code = KG_EMPTY_CCACHE;
         goto cleanup;
     }
 
     (void)krb5_cc_set_flags(context, ccache, KRB5_TC_OPENCLOSE);
-    cred->ccache = ccache;
 
 cleanup:
+    (void)krb5_cc_set_flags(context, ccache, KRB5_TC_OPENCLOSE);
     krb5_free_principal(context, ccache_princ);
     krb5_free_principal(context, tgt_princ);
     return code;
 }
 
-/* get credentials corresponding to the default credential cache.
-   If successful, set the ccache-specific fields in cred.
-*/
+/* Find an existing or destination ccache for cred->name. */
+static krb5_error_code
+get_cache_for_name(krb5_context context, krb5_gss_cred_id_rec *cred)
+{
+    krb5_error_code code;
+    krb5_boolean can_get, have_collection;
+    krb5_ccache defcc = NULL;
+    krb5_principal princ = NULL;
+    const char *cctype;
+
+    assert(cred->name != NULL && cred->ccache == NULL);
+#ifdef USE_LEASH
+    return get_ccache_leash(context, cred->name->princ, &cred->ccache);
+#else
+    /* Check first whether we can acquire tickets, to avoid overwriting the
+     * extended error message from krb5_cc_cache_match. */
+    can_get = can_get_initial_creds(context, 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);
+    if (code != KRB5_CC_NOTFOUND || !can_get)
+        return code;
+    krb5_clear_error_message(context);
+
+    /* There is no existing ccache, but we can acquire credentials.  Get the
+     * default ccache to help decide where we should put them. */
+    code = krb5_cc_default(context, &defcc);
+    if (code)
+        return code;
+    cctype = krb5_cc_get_type(context, defcc);
+    have_collection = krb5_cc_support_switch(context, cctype);
+
+    /* We can use an empty default ccache if we're using a password or if
+     * there's no collection. */
+    if (cred->password != NULL || !have_collection) {
+        if (krb5_cc_get_principal(context, defcc, &princ) == KRB5_FCC_NOFILE) {
+            cred->ccache = defcc;
+            defcc = NULL;
+        }
+        krb5_clear_error_message(context);
+    }
+
+    /* Otherwise, try to use a new cache in the collection. */
+    if (cred->ccache == NULL) {
+        if (!have_collection) {
+            code = KG_CCACHE_NOMATCH;
+            goto cleanup;
+        }
+        code = krb5_cc_new_unique(context, cctype, NULL, &cred->ccache);
+        if (code)
+            goto cleanup;
+    }
+
+cleanup:
+    krb5_free_principal(context, princ);
+    if (defcc != NULL)
+        krb5_cc_close(context, defcc);
+    return code;
+#endif /* not USE_LEASH */
+}
+
+/* Try to set cred->name using the client keytab. */
+static krb5_error_code
+get_name_from_client_keytab(krb5_context context, krb5_gss_cred_id_rec *cred)
+{
+    krb5_error_code code;
+    krb5_principal princ;
+
+    assert(cred->name == NULL);
+    code = k5_kt_get_principal(context, cred->client_keytab, &princ);
+    if (code)
+        return code;
+    code = kg_init_name(context, princ, NULL, NULL, NULL, KG_INIT_NAME_NO_COPY,
+                        &cred->name);
+    if (code) {
+        krb5_free_principal(context, princ);
+        return code;
+    }
+    return 0;
+}
+
+/* Make a note in ccache that we should attempt to refresh it from the client
+ * keytab at refresh_time. */
+static void
+set_refresh_time(krb5_context context, krb5_ccache ccache,
+                 krb5_timestamp refresh_time)
+{
+    char buf[128];
+    krb5_data d;
+
+    snprintf(buf, sizeof(buf), "%ld", (long)refresh_time);
+    d = string2data(buf);
+    (void)krb5_cc_set_config(context, ccache, NULL, KRB5_CONF_REFRESH_TIME,
+                             &d);
+    krb5_clear_error_message(context);
+}
+
+/* Return true if it's time to refresh cred from the client keytab.  If
+ * returning true, avoid retrying for 30 seconds. */
+krb5_boolean
+kg_cred_time_to_refresh(krb5_context context, krb5_gss_cred_id_rec *cred)
+{
+    krb5_timestamp now;
+
+    if (krb5_timeofday(context, &now))
+        return FALSE;
+    if (cred->refresh_time != 0 && now >= cred->refresh_time) {
+        set_refresh_time(context, cred->ccache, cred->refresh_time + 30);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+/* If appropriate, make a note to refresh cred from the client keytab when it
+ * is halfway to expired. */
+void
+kg_cred_set_initial_refresh(krb5_context context, krb5_gss_cred_id_rec *cred,
+                            krb5_ticket_times *times)
+{
+    krb5_timestamp refresh;
+
+    /* For now, we only mark keytab-acquired credentials for refresh. */
+    if (cred->password != NULL)
+        return;
+
+    /* Make a note to refresh these when they are halfway to expired. */
+    refresh = times->starttime + (times->endtime - times->starttime) / 2;
+    set_refresh_time(context, cred->ccache, refresh);
+}
+
+/* Get initial credentials using the supplied password or client keytab. */
+static krb5_error_code
+get_initial_cred(krb5_context context, krb5_gss_cred_id_rec *cred)
+{
+    krb5_error_code code;
+    krb5_get_init_creds_opt *opt = NULL;
+    krb5_creds creds;
+
+    code = krb5_get_init_creds_opt_alloc(context, &opt);
+    if (code)
+        return code;
+    code = krb5_get_init_creds_opt_set_out_ccache(context, opt, cred->ccache);
+    if (code)
+        goto cleanup;
+    if (cred->password != NULL) {
+        code = krb5_get_init_creds_password(context, &creds, cred->name->princ,
+                                            cred->password, NULL, NULL, 0,
+                                            NULL, opt);
+    } else {
+        code = krb5_get_init_creds_keytab(context, &creds, cred->name->princ,
+                                          cred->client_keytab, 0, NULL, opt);
+    }
+    if (code)
+        goto cleanup;
+    kg_cred_set_initial_refresh(context, cred, &creds.times);
+    cred->have_tgt = TRUE;
+    cred->expire = creds.times.endtime;
+    krb5_free_cred_contents(context, &creds);
+cleanup:
+    krb5_get_init_creds_opt_free(context, opt);
+    return code;
+}
+
+/* Get initial credentials if we ought to and are able to. */
+static krb5_error_code
+maybe_get_initial_cred(krb5_context context, krb5_gss_cred_id_rec *cred)
+{
+    krb5_error_code code;
+
+    /* Don't get creds if we don't know the name or are doing IAKERB. */
+    if (cred->name == NULL || cred->iakerb_mech)
+        return 0;
+
+    /* Get creds if we have none or if it's time to refresh. */
+    if (cred->expire == 0 || kg_cred_time_to_refresh(context, cred)) {
+        code = get_initial_cred(context, cred);
+        /* If we were trying to refresh and failed, we can keep going. */
+        if (code && cred->expire == 0)
+            return code;
+        krb5_clear_error_message(context);
+    }
+    return 0;
+}
 
 static OM_uint32
 acquire_init_cred(krb5_context context,
                   OM_uint32 *minor_status,
                   krb5_ccache req_ccache,
-                  krb5_principal desired_princ,
                   gss_buffer_t password,
                   krb5_gss_cred_id_rec *cred)
 {
     krb5_error_code code;
-    krb5_ccache ccache = NULL;
+    krb5_data pwdata, pwcopy;
     int caller_ccname = 0;
 
-    cred->ccache = NULL;
-
-    /* Load the GSS ccache name, if specified, into the context. */
+    /* Get ccache from caller if available. */
     if (GSS_ERROR(kg_sync_ccache_name(context, minor_status)))
         return GSS_S_FAILURE;
     if (GSS_ERROR(kg_caller_provided_ccache_name(minor_status,
                                                  &caller_ccname)))
         return GSS_S_FAILURE;
-
-    /* Pick a credential cache. */
     if (req_ccache != NULL) {
-        code = krb5_cc_dup(context, req_ccache, &ccache);
+        code = krb5_cc_dup(context, req_ccache, &cred->ccache);
+        if (code)
+            goto error;
     } else if (caller_ccname) {
         /* Caller's ccache name has been set as the context default. */
-        code = krb5int_cc_default(context, &ccache);
-    } else if (desired_princ) {
-        /* Try to find an appropriate ccache for the desired name. */
-#ifdef USE_LEASH
-        code = get_ccache_leash(context, desired_princ, &ccache);
-#else
-        code = krb5_cc_cache_match(context, desired_princ, &ccache);
-        if (code == KRB5_CC_NOTFOUND && password != GSS_C_NO_BUFFER) {
-            /* Grab the default ccache for now; if it's not empty, prep_ccache
-             * will create a new one of the default type or error out. */
-            krb5_clear_error_message(context);
-            code = krb5_cc_default(context, &ccache);
-        }
-#endif
-    } else
-        code = 0;
-    if (code != 0) {
-        *minor_status = code;
-        return GSS_S_CRED_UNAVAIL;
+        code = krb5int_cc_default(context, &cred->ccache);
+        if (code)
+            goto error;
     }
 
-    if (ccache != NULL) {
-        if (password != GSS_C_NO_BUFFER && desired_princ != NULL)
-            code = prep_ccache(context, cred, ccache, desired_princ, password);
-        else
-            code = scan_ccache(context, cred, ccache, desired_princ);
-        if (code != 0) {
-            krb5_cc_close(context, ccache);
-            *minor_status = code;
-            return GSS_S_CRED_UNAVAIL;
+    code = krb5_kt_client_default(context, &cred->client_keytab);
+    if (code)
+        goto error;
+
+    if (password != GSS_C_NO_BUFFER) {
+        pwdata = make_data(password->value, password->length);
+        code = krb5int_copy_data_contents_add0(context, &pwdata, &pwcopy);
+        if (code)
+            goto error;
+        cred->password = pwcopy.data;
+    }
+
+    if (cred->ccache != NULL) {
+        /* The caller specified a ccache; check what's in it. */
+        code = scan_ccache(context, cred);
+        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. */
+            if (cred->name == NULL) {
+                if (!get_name_from_client_keytab(context, cred))
+                    code = 0;
+            } else if (can_get_initial_creds(context, cred)) {
+                code = 0;
+            }
         }
-        cred->ccache = ccache;
-    } else {
-        /* We haven't decided on a ccache or principal yet, but fail now if
-         * there are no krb5 credentials at all. */
+        if (code)
+            goto error;
+    } else if (cred->name != NULL) {
+        /* The caller specified a name but not a ccache; pick a cache. */
+        code = get_cache_for_name(context, cred);
+        if (code)
+            goto error;
+    }
+
+#ifndef USE_LEASH
+    /* If we haven't picked a name, make sure we have or can get any creds,
+     * unless we're using Leash and might be able to get them interactively. */
+    if (cred->name == NULL && !can_get_initial_creds(context, cred)) {
         code = krb5_cccol_have_content(context);
-        if (code != 0) {
-            *minor_status = code;
-            return GSS_S_CRED_UNAVAIL;
-        }
+        if (code)
+            goto error;
     }
+#endif
 
-    /*
-     * If the caller specified no ccache and no desired principal, leave
-     * cred->ccache and cred->name NULL.  They will be resolved later by
-     * kg_cred_resolve(), possibly using the target principal name.
-     */
+    code = maybe_get_initial_cred(context, cred);
+    if (code)
+        goto error;
 
     *minor_status = 0;
     return GSS_S_COMPLETE;
+
+error:
+    *minor_status = code;
+    return GSS_S_CRED_UNAVAIL;
 }
 
 static OM_uint32
@@ -619,13 +772,21 @@ acquire_cred(OM_uint32 *minor_status, gss_name_t desired_name,
         goto error_out;
     }
 
+    if (name != NULL) {
+        code = kg_duplicate_name(context, name, &cred->name);
+        if (code) {
+            *minor_status = code;
+            return GSS_S_FAILURE;
+        }
+    }
+
 #ifndef LEAN_CLIENT
     /*
      * If requested, acquire credentials for accepting. This will fill
      * in cred->name if desired_princ is specified.
      */
     if (cred_usage == GSS_C_ACCEPT || cred_usage == GSS_C_BOTH) {
-        ret = acquire_accept_cred(context, minor_status, name, keytab, cred);
+        ret = acquire_accept_cred(context, minor_status, keytab, cred);
         if (ret != GSS_S_COMPLETE)
             goto error_out;
     }
@@ -636,8 +797,7 @@ acquire_cred(OM_uint32 *minor_status, gss_name_t desired_name,
      * in cred->name if it wasn't set above.
      */
     if (cred_usage == GSS_C_INITIATE || cred_usage == GSS_C_BOTH) {
-        ret = acquire_init_cred(context, minor_status, ccache,
-                                name ? name->princ : NULL, password, cred);
+        ret = acquire_init_cred(context, minor_status, ccache, password, cred);
         if (ret != GSS_S_COMPLETE)
             goto error_out;
     }
@@ -702,8 +862,7 @@ kg_cred_resolve(OM_uint32 *minor_status, krb5_context context,
     krb5_error_code code;
     krb5_gss_cred_id_t cred = (krb5_gss_cred_id_t)cred_handle;
     krb5_gss_name_t tname = (krb5_gss_name_t)target_name;
-    krb5_ccache ccache = NULL;
-    krb5_principal client_princ = NULL;
+    krb5_principal client_princ;
 
     *minor_status = 0;
 
@@ -712,37 +871,73 @@ kg_cred_resolve(OM_uint32 *minor_status, krb5_context context,
         return maj;
     k5_mutex_assert_locked(&cred->lock);
 
-    if (cred->ccache != NULL || cred->usage == GSS_C_ACCEPT)
+    if (cred->usage == GSS_C_ACCEPT || cred->name != NULL)
         return GSS_S_COMPLETE;
+    /* acquire_init_cred should have set both name and ccache, or neither. */
+    assert(cred->ccache == NULL);
 
-    /* Pick a credential cache. */
     if (tname != NULL) {
-        code = krb5_cc_select(context, tname->princ, &ccache, &client_princ);
+        /* Use the target name to select an existing ccache or a principal. */
+        code = krb5_cc_select(context, tname->princ, &cred->ccache,
+                              &client_princ);
         if (code && code != KRB5_CC_NOTFOUND)
             goto kerr;
+        if (client_princ != NULL) {
+            code = kg_init_name(context, client_princ, NULL, NULL, NULL,
+                                KG_INIT_NAME_NO_COPY, &cred->name);
+            if (code) {
+                krb5_free_principal(context, client_princ);
+                goto kerr;
+            }
+        }
+        if (cred->ccache != NULL) {
+            code = scan_ccache(context, cred);
+            if (code)
+                goto kerr;
+        }
     }
-    if (ccache == NULL) {
-        /*
-         * Ideally we would get credentials for client_princ if it is set.  At
-         * the moment, we just get the default ccache (obtaining credentials if
-         * the platform supports it) and check it against client_princ below.
-         */
-        code = krb5int_cc_default(context, &ccache);
+
+    /* If we still haven't picked a client principal, try using an existing
+     * default ccache.  (On Windows, this may acquire initial creds.) */
+    if (cred->name == NULL) {
+        code = krb5int_cc_default(context, &cred->ccache);
         if (code)
             goto kerr;
+        code = scan_ccache(context, cred);
+        if (code == KRB5_FCC_NOFILE) {
+            /* Default ccache doesn't exist; fall through to client keytab. */
+            krb5_cc_close(context, cred->ccache);
+            cred->ccache = NULL;
+        } else if (code) {
+            goto kerr;
+        }
     }
 
-    code = scan_ccache(context, cred, ccache, client_princ);
-    if (code) {
-        krb5_cc_close(context, ccache);
-        goto kerr;
+    /* If that didn't work, try getting a name from the client keytab. */
+    if (cred->name == NULL) {
+        code = get_name_from_client_keytab(context, cred);
+        if (code) {
+            code = KG_EMPTY_CCACHE;
+            goto kerr;
+        }
     }
 
-    krb5_free_principal(context, client_princ);
+    if (cred->name != NULL && cred->ccache == NULL) {
+        /* Pick a cache for the name we chose (from krb5_cc_select or from the
+         * client keytab). */
+        code = get_cache_for_name(context, cred);
+        if (code)
+            goto kerr;
+    }
+
+    /* Resolve name to ccache and possibly get initial credentials. */
+    code = maybe_get_initial_cred(context, cred);
+    if (code)
+        goto kerr;
+
     return GSS_S_COMPLETE;
 
 kerr:
-    krb5_free_principal(context, client_princ);
     k5_mutex_unlock(&cred->lock);
     save_error_info(code, context);
     *minor_status = code;
diff --git a/src/lib/gssapi/krb5/gssapiP_krb5.h b/src/lib/gssapi/krb5/gssapiP_krb5.h
index 5621dbd..9b0d6cc 100644
--- a/src/lib/gssapi/krb5/gssapiP_krb5.h
+++ b/src/lib/gssapi/krb5/gssapiP_krb5.h
@@ -183,7 +183,10 @@ typedef struct _krb5_gss_cred_id_rec {
 
     /* ccache (init) data */
     krb5_ccache ccache;
+    krb5_keytab client_keytab;
+    krb5_boolean have_tgt;
     krb5_timestamp expire;
+    krb5_timestamp refresh_time;
     krb5_enctype *req_enctypes;  /* limit negotiated enctypes to this list */
     char *password;
 } krb5_gss_cred_id_rec, *krb5_gss_cred_id_t;
@@ -476,6 +479,13 @@ krb5_to_gss_cred(krb5_context context,
                  krb5_creds *creds,
                  krb5_gss_cred_id_t *out_cred);
 
+krb5_boolean
+kg_cred_time_to_refresh(krb5_context context, krb5_gss_cred_id_rec *cred);
+
+void
+kg_cred_set_initial_refresh(krb5_context context, krb5_gss_cred_id_rec *cred,
+                            krb5_ticket_times *times);
+
 OM_uint32
 kg_cred_resolve(OM_uint32 *minor_status, krb5_context context,
                 gss_cred_id_t cred_handle, gss_name_t target_name);
diff --git a/src/lib/gssapi/krb5/iakerb.c b/src/lib/gssapi/krb5/iakerb.c
index 1b3236e..1d73e2d 100644
--- a/src/lib/gssapi/krb5/iakerb.c
+++ b/src/lib/gssapi/krb5/iakerb.c
@@ -416,7 +416,7 @@ iakerb_init_creds_ctx(iakerb_ctx_id_t ctx,
 {
     krb5_error_code code;
 
-    if (cred->iakerb_mech == 0 || cred->password == NULL) {
+    if (cred->iakerb_mech == 0) {
         code = EINVAL;
         goto cleanup;
     }
@@ -446,7 +446,13 @@ iakerb_init_creds_ctx(iakerb_ctx_id_t ctx,
     if (code != 0)
         goto cleanup;
 
-    code = krb5_init_creds_set_password(ctx->k5c, ctx->icc, cred->password);
+    if (cred->password != NULL) {
+        code = krb5_init_creds_set_password(ctx->k5c, ctx->icc,
+                                            cred->password);
+    } else {
+        code = krb5_init_creds_set_keytab(ctx->k5c, ctx->icc,
+                                          cred->client_keytab);
+    }
     if (code != 0)
         goto cleanup;
 
@@ -547,10 +553,17 @@ iakerb_initiator_step(iakerb_ctx_id_t ctx,
 
         code = krb5_init_creds_step(ctx->k5c, ctx->icc, &in, &out, &realm,
                                     &flags);
-        if (code != 0)
-            goto cleanup;
-        if (!(flags & KRB5_INIT_CREDS_STEP_FLAG_CONTINUE)) {
+        if (code != 0) {
+            if (cred->have_tgt) {
+                /* We were trying to refresh; keep going with current creds. */
+                ctx->state = IAKERB_TGS_REQ;
+                krb5_clear_error_message(ctx->k5c);
+            } else {
+                goto cleanup;
+            }
+        } else if (!(flags & KRB5_INIT_CREDS_STEP_FLAG_CONTINUE)) {
             krb5_init_creds_get_times(ctx->k5c, ctx->icc, &times);
+            kg_cred_set_initial_refresh(ctx->k5c, cred, &times);
             cred->expire = times.endtime;
 
             krb5_init_creds_free(ctx->k5c, ctx->icc);
@@ -650,43 +663,18 @@ iakerb_get_initial_state(iakerb_ctx_id_t ctx,
         in_creds.times.endtime = now + time_req;
     }
 
-    code = krb5_get_credentials(ctx->k5c, KRB5_GC_CACHED,
-                                cred->ccache,
+    /* Make an AS request if we have no creds or it's time to refresh them. */
+    if (cred->expire == 0 || kg_cred_time_to_refresh(ctx->k5c, cred)) {
+        *state = IAKERB_AS_REQ;
+        code = 0;
+        goto cleanup;
+    }
+
+    code = krb5_get_credentials(ctx->k5c, KRB5_GC_CACHED, cred->ccache,
                                 &in_creds, &out_creds);
     if (code == KRB5_CC_NOTFOUND || code == KRB5_CC_NOT_KTYPE) {
-        krb5_principal tgs;
-        krb5_data *realm = krb5_princ_realm(ctx->k5c, in_creds.client);
-
-        /* If we have a TGT for the client realm, can proceed to TGS-REQ. */
-        code = krb5_build_principal_ext(ctx->k5c,
-                                        &tgs,
-                                        realm->length,
-                                        realm->data,
-                                        KRB5_TGS_NAME_SIZE,
-                                        KRB5_TGS_NAME,
-                                        realm->length,
-                                        realm->data,
-                                        NULL);
-        if (code != 0)
-            goto cleanup;
-
-        in_creds.server = tgs;
-
-        /* It would be nice if we could return KRB5KRB_AP_ERR_TKT_EXPIRED if
-         * the TGT is expired, for consistency with the krb5 mech.  As it
-         * stands, we won't see the expired TGT and will return
-         * KRB5_CC_NOTFOUND. */
-        code = krb5_get_credentials(ctx->k5c, KRB5_GC_CACHED,
-                                    cred->ccache,
-                                    &in_creds, &out_creds);
-        if (code == KRB5_CC_NOTFOUND && cred->password != NULL) {
-            *state = IAKERB_AS_REQ;
-            code = 0;
-        } else if (code == 0) {
-            *state = IAKERB_TGS_REQ;
-            krb5_free_creds(ctx->k5c, out_creds);
-        }
-        krb5_free_principal(ctx->k5c, tgs);
+        *state = cred->have_tgt ? IAKERB_TGS_REQ : IAKERB_AS_REQ;
+        code = 0;
     } else if (code == 0) {
         *state = IAKERB_AP_REQ;
         krb5_free_creds(ctx->k5c, out_creds);
diff --git a/src/lib/gssapi/krb5/init_sec_context.c b/src/lib/gssapi/krb5/init_sec_context.c
index d0eda5f..d4c987a 100644
--- a/src/lib/gssapi/krb5/init_sec_context.c
+++ b/src/lib/gssapi/krb5/init_sec_context.c
@@ -194,30 +194,6 @@ static krb5_error_code get_credentials(context, cred, server, now,
 
     code = krb5_get_credentials(context, flags, cred->ccache,
                                 &in_creds, &result_creds);
-    if (code == KRB5_CC_NOTFOUND && cred->password != NULL &&
-        !cred->iakerb_mech) {
-        krb5_creds tgt_creds;
-
-        memset(&tgt_creds, 0, sizeof(tgt_creds));
-
-        /* No TGT in the ccache, but we can get one with the password. */
-        code = krb5_get_init_creds_password(context, &tgt_creds,
-                                            in_creds.client, cred->password,
-                                            NULL, NULL, 0, NULL, NULL);
-        if (code)
-            goto cleanup;
-
-        code = krb5_cc_store_cred(context, cred->ccache, &tgt_creds);
-        if (code) {
-            krb5_free_cred_contents(context, &tgt_creds);
-            goto cleanup;
-        }
-        cred->expire = tgt_creds.times.endtime;
-        krb5_free_cred_contents(context, &tgt_creds);
-
-        code = krb5_get_credentials(context, flags, cred->ccache,
-                                    &in_creds, &result_creds);
-    }
     if (code)
         goto cleanup;
 
diff --git a/src/lib/gssapi/krb5/rel_cred.c b/src/lib/gssapi/krb5/rel_cred.c
index a69fb19..8db7450 100644
--- a/src/lib/gssapi/krb5/rel_cred.c
+++ b/src/lib/gssapi/krb5/rel_cred.c
@@ -57,6 +57,9 @@ krb5_gss_release_cred(minor_status, cred_handle)
     } else
         code1 = 0;
 
+    if (cred->client_keytab)
+        krb5_kt_close(context, cred->client_keytab);
+
 #ifndef LEAN_CLIENT
     if (cred->keytab)
         code2 = krb5_kt_close(context, cred->keytab);
diff --git a/src/lib/gssapi/krb5/val_cred.c b/src/lib/gssapi/krb5/val_cred.c
index 46a9ae1..234cf69 100644
--- a/src/lib/gssapi/krb5/val_cred.c
+++ b/src/lib/gssapi/krb5/val_cred.c
@@ -44,7 +44,7 @@ krb5_gss_validate_cred_1(OM_uint32 *minor_status, gss_cred_id_t cred_handle,
         return GSS_S_FAILURE;
     }
 
-    if (cred->ccache) {
+    if (cred->ccache && cred->expire != 0) {
         if ((code = krb5_cc_get_principal(context, cred->ccache, &princ))) {
             k5_mutex_unlock(&cred->lock);
             *minor_status = code;
diff --git a/src/tests/gssapi/Makefile.in b/src/tests/gssapi/Makefile.in
index 2719212..4ddd9c9 100644
--- a/src/tests/gssapi/Makefile.in
+++ b/src/tests/gssapi/Makefile.in
@@ -14,11 +14,17 @@ OBJS=	t_accname.o t_ccselect.o t_imp_cred.o t_imp_name.o t_s4u.o \
 all:: t_accname t_ccselect t_imp_cred t_imp_name t_s4u t_s4u2proxy_krb5 \
 	t_namingexts t_gssexts t_spnego t_saslname
 
-check-pytests:: t_accname t_ccselect t_imp_cred t_spnego t_s4u2proxy_krb5 t_s4u
+check-pytests:: t_accname t_ccselect t_imp_cred t_spnego t_s4u2proxy_krb5 \
+	t_s4u ccinit ccrefresh
 	$(RUNPYTEST) $(srcdir)/t_gssapi.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_ccselect.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_s4u.py $(PYTESTFLAGS)
+	$(RUNPYTEST) $(srcdir)/t_client_keytab.py $(PYTESTFLAGS)
 
+ccinit: ccinit.o $(KRB5_BASE_DEPLIBS)
+	$(CC_LINK) -o ccinit ccinit.o $(KRB5_BASE_LIBS)
+ccrefresh: ccrefresh.o $(KRB5_BASE_DEPLIBS)
+	$(CC_LINK) -o ccrefresh ccrefresh.o $(KRB5_BASE_LIBS)
 t_accname: t_accname.o $(GSS_DEPLIBS) $(KRB5_BASE_DEPLIBS)
 	$(CC_LINK) -o t_accname t_accname.o $(GSS_LIBS) $(KRB5_BASE_LIBS)
 t_ccselect: t_ccselect.o $(GSS_DEPLIBS) $(KRB5_BASE_DEPLIBS)
diff --git a/src/tests/gssapi/ccinit.c b/src/tests/gssapi/ccinit.c
new file mode 100644
index 0000000..b06f044
--- /dev/null
+++ b/src/tests/gssapi/ccinit.c
@@ -0,0 +1,72 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* tests/gssapi/ccinit.c - Initialize an empty ccache */
+/*
+ * Copyright (C) 2012 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in
+ *   the documentation and/or other materials provided with the
+ *   distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This program initializes a ccache without attempting to get credentials in
+ * it.  It is used to test some finer points of gss_acquire_cred behavior.
+ */
+
+#include "k5-int.h"
+
+static void
+check(krb5_error_code code)
+{
+    if (code != 0) {
+        com_err("ccinit", code, NULL);
+        abort();
+    }
+}
+
+int
+main(int argc, char **argv)
+{
+    const char *ccname, *princname;
+    krb5_context context;
+    krb5_principal princ;
+    krb5_ccache ccache;
+
+    if (argc != 3) {
+        fprintf(stderr, "Usage: %s ccname princname\n", argv[0]);
+        return 1;
+    }
+    ccname = argv[1];
+    princname = argv[2];
+
+    check(krb5_init_context(&context));
+    check(krb5_parse_name(context, princname, &princ));
+    check(krb5_cc_resolve(context, ccname, &ccache));
+    check(krb5_cc_initialize(context, ccache, princ));
+    krb5_cc_close(context, ccache);
+    krb5_free_principal(context, princ);
+    krb5_free_context(context);
+    return 0;
+}
diff --git a/src/tests/gssapi/ccrefresh.c b/src/tests/gssapi/ccrefresh.c
new file mode 100644
index 0000000..bff299e
--- /dev/null
+++ b/src/tests/gssapi/ccrefresh.c
@@ -0,0 +1,80 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* tests/gssapi/ccrefresh.c - Get or set refresh time on a ccache */
+/*
+ * Copyright (C) 2012 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in
+ *   the documentation and/or other materials provided with the
+ *   distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This program sets the refresh time of an existing ccache to 1, forcing a
+ * refresh.
+ */
+
+#include "k5-int.h"
+
+static void
+check(krb5_error_code code)
+{
+    if (code != 0) {
+        com_err("ccrefresh", code, NULL);
+        abort();
+    }
+}
+
+int
+main(int argc, char **argv)
+{
+    const char *ccname, *value = NULL;
+    krb5_context context;
+    krb5_ccache ccache;
+    krb5_data d;
+
+    if (argc != 2 && argc != 3) {
+        fprintf(stderr, "Usage: %s ccname [value]\n", argv[0]);
+        return 1;
+    }
+    ccname = argv[1];
+    if (argc == 3)
+        value = argv[2];
+
+    check(krb5_init_context(&context));
+    check(krb5_cc_resolve(context, ccname, &ccache));
+    if (value != NULL) {
+        d = string2data((char *)value);
+        check(krb5_cc_set_config(context, ccache, NULL, KRB5_CONF_REFRESH_TIME,
+                                 &d));
+    } else {
+        check(krb5_cc_get_config(context, ccache, NULL, KRB5_CONF_REFRESH_TIME,
+                                 &d));
+        printf("%.*s\n", (int)d.length, d.data);
+        krb5_free_data_contents(context, &d);
+    }
+    krb5_cc_close(context, ccache);
+    krb5_free_context(context);
+    return 0;
+}
diff --git a/src/tests/gssapi/t_ccselect.py b/src/tests/gssapi/t_ccselect.py
index 5350d92..ce25dfb 100644
--- a/src/tests/gssapi/t_ccselect.py
+++ b/src/tests/gssapi/t_ccselect.py
@@ -123,7 +123,7 @@ if output != (bob + '\n'):
     fail('bob not chosen via primary cache when no .k5identity line matches.')
 output = r1.run_as_client(['./t_ccselect', 'gss:bogus@' + hostname],
                           expected_code=1)
-if 'does not match desired' not in output:
+if 'Can\'t find client principal noprinc' not in output:
     fail('Expected error not seen when k5identity selects bad principal.')
 
 success('GSSAPI credential selection tests')
diff --git a/src/tests/gssapi/t_client_keytab.py b/src/tests/gssapi/t_client_keytab.py
new file mode 100644
index 0000000..71cb89e
--- /dev/null
+++ b/src/tests/gssapi/t_client_keytab.py
@@ -0,0 +1,132 @@
+#!/usr/bin/python
+from k5test import *
+
+# Set up a basic realm and a client keytab containing two user principals.
+# Point HOME at realm.testdir for tests using .k5identity.
+realm = K5Realm(get_creds=False)
+bob = 'bob@' + realm.realm
+gssserver = 'gss:host@' + hostname
+realm.env_client['HOME'] = realm.testdir
+realm.addprinc(bob, password('bob'))
+realm.extract_keytab(realm.user_princ, realm.client_keytab)
+realm.extract_keytab(bob, realm.client_keytab)
+
+# Test 1: no name/cache specified, pick first principal from client keytab
+out = realm.run_as_client(['./t_ccselect', realm.host_princ])
+if realm.user_princ not in out:
+    fail('Authenticated as wrong principal')
+realm.run_as_client([kdestroy])
+
+# Test 2: no name/cache specified, pick principal from k5identity
+k5idname = os.path.join(realm.testdir, '.k5identity')
+k5id = open(k5idname, 'w')
+k5id.write('%s service=host host=%s\n' % (bob, hostname))
+k5id.close()
+out = realm.run_as_client(['./t_ccselect', gssserver])
+if bob not in out:
+    fail('Authenticated as wrong principal')
+os.remove(k5idname)
+realm.run_as_client([kdestroy])
+
+# Test 3: no name/cache specified, default ccache has name but no creds
+realm.run_as_client(['./ccinit', realm.ccache, bob])
+out = realm.run_as_client(['./t_ccselect', realm.host_princ])
+if bob not in out:
+    fail('Authenticated as wrong principal')
+# Leave tickets for next test.
+
+# Test 4: name specified, non-collectable default cache doesn't match
+out = realm.run_as_client(['./t_ccselect', realm.host_princ, realm.user_princ],
+                          expected_code=1)
+if 'Principal in credential cache does not match desired name' not in out:
+    fail('Expected error not seen')
+realm.run_as_client([kdestroy])
+
+# Test 5: name specified, nonexistent default cache
+out = realm.run_as_client(['./t_ccselect', realm.host_princ, bob])
+if bob not in out:
+    fail('Authenticated as wrong principal')
+# Leave tickets for next test.
+
+# Test 6: name specified, matches default cache, time to refresh
+realm.run_as_client(['./ccrefresh', realm.ccache, '1'])
+out = realm.run_as_client(['./t_ccselect', realm.host_princ, bob])
+if bob not in out:
+    fail('Authenticated as wrong principal')
+out = realm.run_as_client(['./ccrefresh', realm.ccache])
+if int(out) < 1000:
+    fail('Credentials apparently not refreshed')
+realm.run_as_client([kdestroy])
+
+# Test 7: empty ccache specified, pick first principal from client keytab
+realm.run_as_client(['./t_imp_cred', realm.host_princ])
+realm.klist(realm.user_princ)
+realm.run_as_client([kdestroy])
+
+# Test 8: ccache specified with name but no creds; name not in client keytab
+realm.run_as_client(['./ccinit', realm.ccache, realm.host_princ])
+out = realm.run_as_client(['./t_imp_cred', realm.host_princ], expected_code=1)
+if 'Credential cache is empty' not in out:
+    fail('Expected error not seen')
+realm.run_as_client([kdestroy])
+
+# Test 9: ccache specified with name but no creds; name in client keytab
+realm.run_as_client(['./ccinit', realm.ccache, bob])
+realm.run_as_client(['./t_imp_cred', realm.host_princ])
+realm.klist(bob)
+# Leave tickets for next test.
+
+# Test 10: ccache specified with creds, time to refresh
+realm.run_as_client(['./ccrefresh', realm.ccache, '1'])
+realm.run_as_client(['./t_imp_cred', realm.host_princ])
+realm.klist(bob)
+out = realm.run_as_client(['./ccrefresh', realm.ccache])
+if int(out) < 1000:
+    fail('Credentials apparently not refreshed')
+realm.run_as_client([kdestroy])
+
+# Use a cache collection for the remaining tests.
+ccdir = os.path.join(realm.testdir, 'cc')
+ccname = 'DIR:' + ccdir
+os.mkdir(ccdir)
+realm.env_client['KRB5CCNAME'] = ccname
+
+# Test 11: name specified, matching cache in collection with no creds
+bobcache = os.path.join(ccdir, 'tktbob')
+realm.run_as_client(['./ccinit', bobcache, bob])
+out = realm.run_as_client(['./t_ccselect', realm.host_princ, bob])
+if bob not in out:
+    fail('Authenticated as wrong principal')
+# Leave tickets for next test.
+
+# Test 12: name specified, matching cache in collection, time to refresh
+realm.run_as_client(['./ccrefresh', bobcache, '1'])
+out = realm.run_as_client(['./t_ccselect', realm.host_princ, bob])
+if bob not in out:
+    fail('Authenticated as wrong principal')
+out = realm.run_as_client(['./ccrefresh', bobcache])
+if int(out) < 1000:
+    fail('Credentials apparently not refreshed')
+realm.run_as_client([kdestroy, '-A'])
+
+# Test 13: name specified, collection has default for different principal
+realm.kinit(realm.user_princ, password('user'))
+out = realm.run_as_client(['./t_ccselect', realm.host_princ, bob])
+if bob not in out:
+    fail('Authenticated as wrong principal')
+out = realm.run_as_client([klist])
+if 'Default principal: %s\n' % realm.user_princ not in out:
+    fail('Default cache overwritten by acquire_cred')
+realm.run_as_client([kdestroy, '-A'])
+
+# Test 14: name specified, collection has no default cache
+out = realm.run_as_client(['./t_ccselect', realm.host_princ, bob])
+if bob not in out:
+    fail('Authenticated as wrong principal')
+# Make sure the tickets we acquired didn't become the default
+out = realm.run_as_client([klist], expected_code=1)
+if 'No credentials cache found' not in out:
+    fail('Expected error not seen')
+realm.run_as_client([kdestroy, '-A'])
+
+success('Client keytab tests')


More information about the cvs-krb5 mailing list