Try all host keys by default in vfy_increds
authorGreg Hudson <ghudson@mit.edu>
Thu, 3 May 2012 19:42:43 +0000 (19:42 +0000)
committerGreg Hudson <ghudson@mit.edu>
Thu, 3 May 2012 19:42:43 +0000 (19:42 +0000)
Factor out the core code of krb5_verify_init_creds into a helper, add
new helper functions to retrieve the list of unique host principals
from a keytab, and make krb5_verify_init_creds drive the helper once
per host principal.

Augment the test harness and test cases to better test the new
behavior.  Add a k5test method to retrieve an NFS principal for the
test realm for the sake of the new test cases.

ticket: 7125

git-svn-id: svn://anonsvn.mit.edu/krb5/trunk@25845 dc483132-0cff-0310-8789-dd5450dbe970

src/include/krb5/krb5.hin
src/lib/krb5/krb/t_vfy_increds.c
src/lib/krb5/krb/t_vfy_increds.py
src/lib/krb5/krb/vfy_increds.c
src/util/k5test.py

index 94a78a004c6f3adc4d9f10145cd0aab588a151a1..c267622dbb506e1d9ccd62dd8c95debdf8257e73 100644 (file)
@@ -7056,36 +7056,37 @@ krb5_verify_init_creds_opt_set_ap_req_nofail(krb5_verify_init_creds_opt * k5_vic
  *
  * @param [in]     context      Library context
  * @param [in]     creds        Initial credentials to be verified
- * @param [in]     server_arg   Server principal (or NULL)
- * @param [in]     keytab_arg   Key table (NULL to use default keytab)
- * @param [in,out] ccache_arg   Credential cache for fetched creds (or NULL)
+ * @param [in]     server       Server principal (or NULL)
+ * @param [in]     keytab       Key table (NULL to use default keytab)
+ * @param [in,out] ccache       Credential cache for fetched creds (or NULL)
  * @param [in]     options      Verification options (NULL for default options)
  *
  * This function attempts to verify that @a creds were obtained from a KDC with
- * knowledge of a key in @a keytab_arg.  If @a server_arg is provided, the
- * highest-kvno key entry for that principal name is used to verify the
- * credentials; otherwise, the highest-kvno key entry for the first principal
- * listed in @a keytab_arg is used.
+ * knowledge of a key in @a keytab, or the default keytab if @a keytab is NULL.
+ * If @a server is provided, the highest-kvno key entry for that principal name
+ * is used to verify the credentials; otherwise, all unique "host" service
+ * principals in the keytab are tried.
  *
  * If the specified keytab does not exist, or is empty, or cannot be read, or
- * does not contain an entry for @a server_arg, then credential verification
- * may be skipped unless configuration demands that it succeed.  The caller can
+ * does not contain an entry for @a server, then credential verification may be
+ * skipped unless configuration demands that it succeed.  The caller can
  * control this behavior by providing a verification options structure; see
  * krb5_verify_init_creds_opt_init() and
  * krb5_verify_init_creds_opt_set_ap_req_nofail().
  *
- * If @a ccache_arg is NULL, any additional credentials fetched during the
- * verification process will be destroyed.  If @a ccache_arg points to NULL, a
+ * If @a ccache is NULL, any additional credentials fetched during the
+ * verification process will be destroyed.  If @a ccache points to NULL, a
  * memory ccache will be created for the additional credentials and returned in
- * @a ccache_arg.  If @a ccache_arg points to a valid credential cache handle,
- * the additional credentials will be stored in that cache.
+ * @a ccache.  If @a ccache points to a valid credential cache handle, the
+ * additional credentials will be stored in that cache.
  *
  * @retval 0  Success; otherwise - Kerberos error codes
  */
 krb5_error_code KRB5_CALLCONV
 krb5_verify_init_creds(krb5_context context, krb5_creds *creds,
-                       krb5_principal server_arg, krb5_keytab keytab_arg,
-                       krb5_ccache *ccache_arg, krb5_verify_init_creds_opt *options);
+                       krb5_principal server, krb5_keytab keytab,
+                       krb5_ccache *ccache,
+                       krb5_verify_init_creds_opt *options);
 
 /**
  * Get validated credentials from the KDC.
index e5d68777a04d8d75c316fab029bd5ccb07d8b107..a1280141b2515801f391c693f290a7d6e573ae3a 100644 (file)
 
 #include "k5-int.h"
 
+static void
+check(krb5_error_code code)
+{
+    if (code != 0) {
+        com_err("t_vfy_increds", code, NULL);
+        abort();
+    }
+}
+
 int
 main(int argc, char **argv)
 {
@@ -40,17 +49,28 @@ main(int argc, char **argv)
     krb5_ccache ccache;
     krb5_cc_cursor cursor;
     krb5_creds creds;
+    krb5_principal princ = NULL;
+    krb5_verify_init_creds_opt opt;
+
+    check(krb5_init_context(&context));
 
-    assert(krb5_init_context(&context) == 0);
+    krb5_verify_init_creds_opt_init(&opt);
+    argv++;
+    if (*argv != NULL && strcmp(*argv, "-n") == 0) {
+        argv++;
+        krb5_verify_init_creds_opt_set_ap_req_nofail(&opt, TRUE);
+    }
+    if (*argv != NULL)
+        check(krb5_parse_name(context, *argv, &princ));
 
     /* Fetch the first credential from the default ccache. */
-    assert(krb5_cc_default(context, &ccache) == 0);
-    assert(krb5_cc_start_seq_get(context, ccache, &cursor) == 0);
-    assert(krb5_cc_next_cred(context, ccache, &cursor, &creds) == 0);
-    assert(krb5_cc_end_seq_get(context, ccache, &cursor) == 0);
-    assert(krb5_cc_close(context, ccache) == 0);
+    check(krb5_cc_default(context, &ccache));
+    check(krb5_cc_start_seq_get(context, ccache, &cursor));
+    check(krb5_cc_next_cred(context, ccache, &cursor, &creds));
+    check(krb5_cc_end_seq_get(context, ccache, &cursor));
+    check(krb5_cc_close(context, ccache));
 
-    if (krb5_verify_init_creds(context, &creds, NULL, NULL, NULL, NULL) != 0)
+    if (krb5_verify_init_creds(context, &creds, princ, NULL, NULL, &opt) != 0)
         return 1;
     return 0;
 }
index 6bac646dc01e4e5140597f92dd347dd434a94aac..a06b740fc6d7d8d787e37e7314f23e85c3200c42 100644 (file)
@@ -26,25 +26,72 @@ from k5test import *
 
 realm = K5Realm()
 
-# Verify the default.
+# Verify the default test realm credentials with the default keytab.
 realm.run_as_server(['./t_vfy_increds'])
+realm.run_as_server(['./t_vfy_increds', '-n'])
 
 # Verify after updating the keytab (so the keytab contains an outdated
 # version 1 key followed by an up-to-date version 2 key).
 realm.run_kadminl('ktadd ' + realm.host_princ)
 realm.run_as_server(['./t_vfy_increds'])
+realm.run_as_server(['./t_vfy_increds', '-n'])
 
 # Bump the host key without updating the keytab and make sure that
 # verification fails as we expect it to.
 realm.run_kadminl('change_password -randkey ' + realm.host_princ)
 realm.run_as_server(['./t_vfy_increds'], expected_code=1)
+realm.run_as_server(['./t_vfy_increds', '-n'], expected_code=1)
 
-# Remove the keytab and verify again.  This should succeed because
-# verify_ap_req_nofail is not set.
+# Simulate a system where the hostname has changed and the keytab
+# contains host service principals with a hostname that no longer
+# matches.  Verify after updating the keytab with a host service
+# principal that has hostname that doesn't match the host running the
+# test.  Verify should succeed, with or without nofail.
+realm.run_kadminl('addprinc -randkey host/wrong.hostname')
+realm.run_kadminl('ktadd host/wrong.hostname')
+realm.run_as_server(['./t_vfy_increds'])
+realm.run_as_server(['./t_vfy_increds', '-n'])
+
+# Remove the keytab and verify again.  This should succeed if nofail
+# is not set, and fail if it is set.
 os.remove(realm.keytab)
 realm.run_as_server(['./t_vfy_increds'])
+realm.run_as_server(['./t_vfy_increds', '-n'], expected_code=1)
+
+# Create an empty keytab file and verify again.  This simulates a
+# system where an admin ran "touch krb5.keytab" to work around a
+# Solaris Kerberos bug where krb5_kt_default() fails if the keytab
+# file doesn't exist.  Verification should succeed in nofail is not
+# set.  (An empty keytab file appears as corrupt to keytab calls,
+# causing a KRB5_KEYTAB_BADVNO error, so any tightening of the
+# krb5_verify_init_creds semantics needs to take this into account.)
+open(realm.keytab, 'w').close()
+realm.run_as_server(['./t_vfy_increds'])
+realm.run_as_server(['./t_vfy_increds', '-n'], expected_code=1)
+os.remove(realm.keytab)
+
+# Add an NFS service principal to keytab.  Verify should ignore it by
+# default (succeeding unless nofail is set), but should verify with it
+# when it is specifically requested.
+realm.run_kadminl('addprinc -randkey ' + realm.nfs_princ)
+realm.run_kadminl('ktadd ' + realm.nfs_princ)
+realm.run_as_server(['./t_vfy_increds'])
+realm.run_as_server(['./t_vfy_increds', '-n'], expected_code=1)
+realm.run_as_server(['./t_vfy_increds', realm.nfs_princ])
+realm.run_as_server(['./t_vfy_increds', '-n', realm.nfs_princ])
+
+# Invalidating the NFS keys in the keytab.  We should get the same
+# results with the default principal argument, but verification should
+# now fail if we request it specifically.
+realm.run_kadminl('change_password -randkey ' + realm.nfs_princ)
+realm.run_as_server(['./t_vfy_increds'])
+realm.run_as_server(['./t_vfy_increds', '-n'], expected_code=1)
+realm.run_as_server(['./t_vfy_increds', realm.nfs_princ], expected_code=1)
+realm.run_as_server(['./t_vfy_increds', '-n', realm.nfs_princ],
+                    expected_code=1)
 
-# Try with verify_ap_req_nofail set and no keytab.  This should fail.
+# Spot-check that verify_ap_req_nofail works equivalently to the
+# programmatic nofail option.
 realm.stop()
 conf = { 'server' : { 'libdefaults' : { 'verify_ap_req_nofail' : 'true' } } }
 realm = K5Realm(krb5_conf=conf)
index 207b3094d5b91a1122dbb1ed4773d3b89f9e9aa9..bd993c21f993b2af945e9e0d5cf4682ee39195f1 100644 (file)
@@ -69,77 +69,20 @@ cleanup:
     return(code);
 }
 
-krb5_error_code KRB5_CALLCONV
-krb5_verify_init_creds(krb5_context context,
-                       krb5_creds *creds,
-                       krb5_principal server_arg,
-                       krb5_keytab keytab_arg,
-                       krb5_ccache *ccache_arg,
-                       krb5_verify_init_creds_opt *options)
+static krb5_error_code
+get_vfy_cred(krb5_context context, krb5_creds *creds, krb5_principal server,
+             krb5_keytab keytab, krb5_ccache *ccache_arg)
 {
     krb5_error_code ret;
-    krb5_principal server;
-    krb5_keytab keytab;
     krb5_ccache ccache;
-    krb5_keytab_entry kte;
     krb5_creds in_creds, *out_creds;
     krb5_auth_context authcon;
     krb5_data ap_req;
 
-    /* KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN */
-
-    server = NULL;
-    keytab = NULL;
     ccache = NULL;
     out_creds = NULL;
     authcon = NULL;
     ap_req.data = NULL;
-
-    if (keytab_arg) {
-        keytab = keytab_arg;
-    } else {
-        if ((ret = krb5_kt_default(context, &keytab)))
-            goto cleanup;
-    }
-
-    if (server_arg) {
-        ret = krb5_copy_principal(context, server_arg, &server);
-        if (ret)
-            goto cleanup;
-    } else {
-        /* Use a principal name from the keytab. */
-        ret = k5_kt_get_principal(context, keytab, &server);
-        if (ret) {
-            /* There's no keytab, or it's empty, or we can't read it.
-             * Allow this unless configuration demands verification. */
-            if (!nofail(context, options, creds))
-                ret = 0;
-            goto cleanup;
-        }
-    }
-
-    /* first, check if the server is in the keytab.  If not, there's
-       no reason to continue.  rd_req does all this, but there's
-       no way to know that a given error is caused by a missing
-       keytab or key, and not by some other problem. */
-
-    if (krb5_is_referral_realm(&server->realm)) {
-        krb5_free_data_contents(context, &server->realm);
-        ret = krb5_get_default_realm(context, &server->realm.data);
-        if (ret) goto cleanup;
-        server->realm.length = strlen(server->realm.data);
-    }
-
-    if ((ret = krb5_kt_get_entry(context, keytab, server, 0, 0, &kte))) {
-        /* this means there is no keying material.  This is ok, as long as
-           it is not prohibited by the configuration */
-        if (!nofail(context, options, creds))
-            ret = 0;
-        goto cleanup;
-    }
-
-    krb5_kt_free_entry(context, &kte);
-
     /* If the creds are for the server principal, we're set, just do a mk_req.
      * Otherwise, do a get_credentials first.
      */
@@ -229,10 +172,6 @@ krb5_verify_init_creds(krb5_context context,
      */
 
 cleanup:
-    if ( server)
-        krb5_free_principal(context, server);
-    if (!keytab_arg && keytab)
-        krb5_kt_close(context, keytab);
     if (ccache)
         krb5_cc_destroy(context, ccache);
     if (out_creds)
@@ -244,3 +183,141 @@ cleanup:
 
     return(ret);
 }
+
+/* Free the principals in plist and plist itself. */
+static void
+free_princ_list(krb5_context context, krb5_principal *plist)
+{
+    size_t i;
+
+    if (plist == NULL)
+        return;
+    for (i = 0; plist[i] != NULL; i++)
+        krb5_free_principal(context, plist[i]);
+    free(plist);
+}
+
+/* Add princ to plist if it isn't already there. */
+static krb5_error_code
+add_princ_list(krb5_context context, krb5_const_principal princ,
+               krb5_principal **plist)
+{
+    size_t i;
+    krb5_principal *newlist;
+
+    /* Check if princ is already in plist, and count the elements. */
+    for (i = 0; (*plist) != NULL && (*plist)[i] != NULL; i++) {
+        if (krb5_principal_compare(context, princ, (*plist)[i]))
+            return 0;
+    }
+
+    newlist = realloc(*plist, (i + 2) * sizeof(*newlist));
+    if (newlist == NULL)
+        return ENOMEM;
+    *plist = newlist;
+    newlist[i] = newlist[i + 1] = NULL; /* terminate the list */
+    return krb5_copy_principal(context, princ, &newlist[i]);
+}
+
+/* Return a list of all unique host service princs in keytab. */
+static krb5_error_code
+get_host_princs_from_keytab(krb5_context context, krb5_keytab keytab,
+                            krb5_principal **princ_list_out)
+{
+    krb5_error_code ret;
+    krb5_kt_cursor cursor;
+    krb5_keytab_entry kte;
+    krb5_principal *plist = NULL, p;
+
+    *princ_list_out = NULL;
+
+    ret = krb5_kt_start_seq_get(context, keytab, &cursor);
+    if (ret)
+        goto cleanup;
+
+    while ((ret = krb5_kt_next_entry(context, keytab, &kte, &cursor)) == 0) {
+        p = kte.principal;
+        if (p->length == 2 && data_eq_string(p->data[0], "host"))
+            ret = add_princ_list(context, p, &plist);
+        krb5_kt_free_entry(context, &kte);
+        if (ret)
+            break;
+    }
+    (void)krb5_kt_end_seq_get(context, keytab, &cursor);
+    if (ret == KRB5_KT_END)
+        ret = 0;
+    if (ret)
+        goto cleanup;
+
+    *princ_list_out = plist;
+    plist = NULL;
+
+cleanup:
+    free_princ_list(context, plist);
+    return ret;
+}
+
+krb5_error_code KRB5_CALLCONV
+krb5_verify_init_creds(krb5_context context, krb5_creds *creds,
+                       krb5_principal server, krb5_keytab keytab,
+                       krb5_ccache *ccache,
+                       krb5_verify_init_creds_opt *options)
+{
+    krb5_error_code ret;
+    krb5_principal *host_princs = NULL;
+    krb5_keytab defkeytab = NULL;
+    krb5_keytab_entry kte;
+    krb5_boolean have_keys = FALSE;
+    size_t i;
+
+    if (keytab == NULL) {
+        ret = krb5_kt_default(context, &defkeytab);
+        if (ret)
+            goto cleanup;
+        keytab = defkeytab;
+    }
+
+    if (server != NULL) {
+        /* Check if server exists in keytab first. */
+        ret = krb5_kt_get_entry(context, keytab, server, 0, 0, &kte);
+        if (ret)
+            goto cleanup;
+        krb5_kt_free_entry(context, &kte);
+        have_keys = TRUE;
+        ret = get_vfy_cred(context, creds, server, keytab, ccache);
+    } else {
+        /* Try using the host service principals from the keytab. */
+        if (keytab->ops->start_seq_get == NULL) {
+            ret = EINVAL;
+            goto cleanup;
+        }
+        ret = get_host_princs_from_keytab(context, keytab, &host_princs);
+        if (ret)
+            goto cleanup;
+        if (host_princs == NULL) {
+            ret = KRB5_KT_NOTFOUND;
+            goto cleanup;
+        }
+        have_keys = TRUE;
+
+        /* Try all host principals until one succeeds or they all fail. */
+        for (i = 0; host_princs[i] != NULL; i++) {
+            ret = get_vfy_cred(context, creds, host_princs[i], keytab, ccache);
+            if (ret == 0)
+                break;
+        }
+    }
+
+cleanup:
+    /* If we have no key to verify with, pretend to succeed unless
+     * configuration directs otherwise. */
+    if (!have_keys && !nofail(context, options, creds))
+        ret = 0;
+
+    if (defkeytab != NULL)
+        krb5_kt_close(context, defkeytab);
+    krb5_free_principal(context, server);
+    free_princ_list(context, host_princs);
+
+    return ret;
+}
index 6097c9be9644dcafa487eba37c6eb04296e04356..f2a36b4d3da57819c8306cc5d8041512a256dc23 100644 (file)
@@ -301,6 +301,9 @@ Scripts may use the following realm methods and attributes:
 * realm.host_princ: The name of the host principal for this machine,
   with realm.
 
+* realm.nfs_princ: The name of the nfs principal for this machine,
+  with realm.
+
 * realm.krbtgt_princ: The name of the krbtgt principal for the realm.
 
 * realm.keytab: A keytab file in realm.testdir.  Initially contains a
@@ -698,6 +701,7 @@ class K5Realm(object):
         self.user_princ = 'user@' + self.realm
         self.admin_princ = 'user/admin@' + self.realm
         self.host_princ = 'host/%s@%s' % (hostname, self.realm)
+        self.nfs_princ = 'nfs/%s@%s' % (hostname, self.realm)
         self.krbtgt_princ = 'krbtgt/%s@%s' % (self.realm, self.realm)
         self.keytab = os.path.join(self.testdir, 'keytab')
         self.ccache = os.path.join(self.testdir, 'ccache')