Make cross-TGT key rollover work from AD to MIT
authorGreg Hudson <ghudson@mit.edu>
Mon, 2 Apr 2012 17:49:56 +0000 (17:49 +0000)
committerGreg Hudson <ghudson@mit.edu>
Mon, 2 Apr 2012 17:49:56 +0000 (17:49 +0000)
Active Directory always issues cross-realm tickets without a kvno,
which we see as kvno 0.  When we see that, try the highest kvno (as we
already do) and then a few preceding kvnos so that key rollover of the
AD->MIT cross TGT can work.

Add new helpers kdc_rd_ap_req, which takes the place of a couple of
steps from kdc_process_tgs_req, and find_server_key, which takes the
place of some of the end steps of kdc_get_server_key.

Code changes by Nicolas Williams.  Test cases by me.

ticket: 7109

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

src/kdc/kdc_util.c
src/tests/t_keyrollover.py

index 039a06ac54f3bc63387621a3ef907047dabc0473..2f4af733d1727d51e3acd6f970a0e66aaa4a59f7 100644 (file)
@@ -68,6 +68,15 @@ const int vague_errors = 1;
 const int vague_errors = 0;
 #endif
 
+static krb5_error_code kdc_rd_ap_req(krb5_ap_req *apreq,
+                                     krb5_auth_context auth_context,
+                                     krb5_db_entry **server,
+                                     krb5_keyblock **tgskey,
+                                     krb5_ticket **ticket);
+static krb5_error_code find_server_key(krb5_db_entry *, krb5_enctype,
+                                       krb5_kvno, krb5_keyblock **,
+                                       krb5_kvno *);
+
 /*
  * concatenate first two authdata arrays, returning an allocated replacement.
  * The replacement should be freed with krb5_free_authdata().
@@ -208,7 +217,6 @@ kdc_process_tgs_req(krb5_kdc_req *request, const krb5_fulladdr *from,
     krb5_auth_context     auth_context = NULL;
     krb5_authenticator  * authenticator = NULL;
     krb5_checksum       * his_cksum = NULL;
-    krb5_kvno             kvno = 0;
     krb5_db_entry       * krbtgt = NULL;
 
     *krbtgt_ptr = NULL;
@@ -253,22 +261,10 @@ kdc_process_tgs_req(krb5_kdc_req *request, const krb5_fulladdr *from,
                                          from->address)) )
         goto cleanup_auth_context;
 
-    if ((retval = kdc_get_server_key(apreq->ticket, 0, foreign_server,
-                                     &krbtgt, tgskey, &kvno)))
-        goto cleanup_auth_context;
-    /*
-     * We do not use the KDB keytab because other parts of the TGS need the TGT key.
-     */
-    retval = krb5_auth_con_setuseruserkey(kdc_context, auth_context, *tgskey);
+    retval = kdc_rd_ap_req(apreq, auth_context, &krbtgt, tgskey, ticket);
     if (retval)
         goto cleanup_auth_context;
 
-    if ((retval = krb5_rd_req_decoded_anyflag(kdc_context, &auth_context, apreq,
-                                              apreq->ticket->server,
-                                              kdc_active_realm->realm_keytab,
-                                              NULL, ticket)))
-        goto cleanup_auth_context;
-
     /* "invalid flag" tickets can must be used to validate */
     if (isflagset((*ticket)->enc_part2->flags, TKT_FLG_INVALID)
         && !isflagset(request->kdc_options, KDC_OPT_VALIDATE)) {
@@ -356,12 +352,77 @@ cleanup:
     return retval;
 }
 
-/* XXX This function should no longer be necessary.
- * The KDC should take the keytab associated with the realm and pass that to
- * the krb5_rd_req_decode(). --proven
+/*
+ * This is a KDC wrapper around krb5_rd_req_decoded_anyflag().
  *
- * It's actually still used by do_tgs_req() for u2u auth, and not too
- * much else. -- tlyu
+ * We can't depend on KDB-as-keytab for handling the AP-REQ here for
+ * optimization reasons: we want to minimize the number of KDB lookups.  We'll
+ * need the KDB entry for the TGS principal, and the TGS key used to decrypt
+ * the TGT, elsewhere in the TGS code.
+ *
+ * This function also implements key rollover support for kvno 0 cross-realm
+ * TGTs issued by AD.
+ */
+static
+krb5_error_code
+kdc_rd_ap_req(krb5_ap_req *apreq, krb5_auth_context auth_context,
+              krb5_db_entry **server, krb5_keyblock **tgskey,
+              krb5_ticket **ticket)
+{
+    krb5_error_code     retval;
+    krb5_enctype        search_enctype = apreq->ticket->enc_part.enctype;
+    krb5_boolean        match_enctype = 1;
+    krb5_kvno           kvno;
+    size_t              tries = 3;
+
+    /*
+     * When we issue tickets we use the first key in the principals' highest
+     * kvno keyset.  For non-cross-realm krbtgt principals we want to only
+     * allow the use of the first key of the principal's keyset that matches
+     * the given kvno.
+     */
+    if (krb5_is_tgs_principal(apreq->ticket->server) &&
+        !is_cross_tgs_principal(apreq->ticket->server)) {
+        search_enctype = -1;
+        match_enctype = 0;
+    }
+
+    retval = kdc_get_server_key(apreq->ticket, 0, match_enctype, server, NULL,
+                                NULL);
+    if (retval)
+        return retval;
+
+    *tgskey = NULL;
+    kvno = apreq->ticket->enc_part.kvno;
+    do {
+        krb5_free_keyblock(kdc_context, *tgskey);
+        retval = find_server_key(*server, search_enctype, kvno, tgskey, &kvno);
+        if (retval)
+            continue;
+
+        /* Make the TGS key available to krb5_rd_req_decoded_anyflag() */
+        retval = krb5_auth_con_setuseruserkey(kdc_context, auth_context,
+                                              *tgskey);
+        if (retval)
+            return retval;
+
+        retval = krb5_rd_req_decoded_anyflag(kdc_context, &auth_context, apreq,
+                                             apreq->ticket->server,
+                                             kdc_active_realm->realm_keytab,
+                                             NULL, ticket);
+    } while (retval && apreq->ticket->enc_part.kvno == 0 && kvno-- > 1 &&
+             --tries > 0);
+
+    return retval;
+}
+
+/*
+ * The KDC should take the keytab associated with the realm and pass
+ * that to the krb5_rd_req_decoded_anyflag(), but we still need to use
+ * the service (TGS, here) key elsewhere.  This approach is faster than
+ * the KDB keytab approach too.
+ *
+ * This is also used by do_tgs_req() for u2u auth.
  */
 krb5_error_code
 kdc_get_server_key(krb5_ticket *ticket, unsigned int flags,
@@ -369,9 +430,14 @@ kdc_get_server_key(krb5_ticket *ticket, unsigned int flags,
                    krb5_keyblock **key, krb5_kvno *kvno)
 {
     krb5_error_code       retval;
-    krb5_boolean          similar;
-    krb5_key_data       * server_key;
     krb5_db_entry       * server = NULL;
+    krb5_enctype          search_enctype = -1;
+    krb5_kvno             search_kvno = -1;
+
+    if (match_enctype)
+        search_enctype = ticket->enc_part.enctype;
+    if (ticket->enc_part.kvno)
+        search_kvno = ticket->enc_part.kvno;
 
     *server_ptr = NULL;
 
@@ -394,38 +460,67 @@ kdc_get_server_key(krb5_ticket *ticket, unsigned int flags,
         goto errout;
     }
 
-    retval = krb5_dbe_find_enctype(kdc_context, server,
-                                   match_enctype ? ticket->enc_part.enctype : -1,
-                                   -1, (krb5_int32)ticket->enc_part.kvno,
-                                   &server_key);
-    if (retval)
-        goto errout;
-    if (!server_key) {
-        retval = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN;
-        goto errout;
-    }
-    if ((*key = (krb5_keyblock *)malloc(sizeof **key))) {
-        retval = krb5_dbe_decrypt_key_data(kdc_context, NULL, server_key,
-                                           *key, NULL);
-    } else
-        retval = ENOMEM;
-    retval = krb5_c_enctype_compare(kdc_context, ticket->enc_part.enctype,
-                                    (*key)->enctype, &similar);
-    if (retval)
-        goto errout;
-    if (!similar) {
-        retval = KRB5_KDB_NO_PERMITTED_KEY;
-        goto errout;
+    if (key) {
+        retval = find_server_key(server, search_enctype, search_kvno, key, kvno);
+        if (retval)
+            goto errout;
     }
-    (*key)->enctype = ticket->enc_part.enctype;
-    *kvno = server_key->key_data_kvno;
     *server_ptr = server;
     server = NULL;
+    return 0;
+
 errout:
     krb5_db_free_principal(kdc_context, server);
     return retval;
 }
 
+/*
+ * A utility function to get the right key from a KDB entry.  Used in handling
+ * of kvno 0 TGTs, for example.
+ */
+static
+krb5_error_code
+find_server_key(krb5_db_entry *server, krb5_enctype enctype, krb5_kvno kvno,
+                krb5_keyblock **key_out, krb5_kvno *kvno_out)
+{
+    krb5_error_code       retval;
+    krb5_key_data       * server_key;
+    krb5_keyblock       * key;
+
+    *key_out = NULL;
+    retval = krb5_dbe_find_enctype(kdc_context, server, enctype, -1,
+                                   kvno ? (krb5_int32)kvno : -1, &server_key);
+    if (retval)
+        return retval;
+    if (!server_key)
+        return KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN;
+    if ((key = (krb5_keyblock *)malloc(sizeof *key)) == NULL)
+        return ENOMEM;
+    retval = krb5_dbe_decrypt_key_data(kdc_context, NULL, server_key,
+                                       key, NULL);
+    if (retval)
+        goto errout;
+    if (enctype != -1) {
+        krb5_boolean similar;
+        retval = krb5_c_enctype_compare(kdc_context, enctype, key->enctype,
+                                        &similar);
+        if (retval)
+            goto errout;
+        if (!similar) {
+            retval = KRB5_KDB_NO_PERMITTED_KEY;
+            goto errout;
+        }
+        key->enctype = enctype;
+    }
+    *key_out = key;
+    key = NULL;
+    if (kvno_out)
+        *kvno_out = server_key->key_data_kvno;
+errout:
+    krb5_free_keyblock(kdc_context, key);
+    return retval;
+}
+
 /* This probably wants to be updated if you support last_req stuff */
 
 static krb5_last_req_entry nolrentry = { KV5M_LAST_REQ_ENTRY, KRB5_LRQ_NONE, 0 };
index 4af76ae9a5371d8fc36ac119c5793fea3b44b2a1..af38b8e188afd716edba7be752f623a7529bd8c9 100644 (file)
@@ -43,4 +43,39 @@ expected = 'krbtgt/%s@%s\n\tEtype (skey, tkt): ' \
 if expected not in output:
     fail('keyrollover: expected TGS enctype not found after change')
 
+# Test that the KDC only accepts the first enctype for a kvno, for a
+# local-realm TGS request.  To set this up, we abuse an edge-case
+# behavior of modprinc -kvno.  First, set up a DES3 krbtgt entry at
+# kvno 1 and cache a krbtgt ticket.
+realm.run_kadminl('cpw -randkey -e des3-cbc-sha1:normal krbtgt/%s' %
+                  realm.realm)
+realm.run_kadminl('modprinc -kvno 1 krbtgt/%s' % realm.realm)
+realm.kinit(realm.user_princ, password('user'))
+# Add an AES krbtgt entry at kvno 2, and then reset it to kvno 1
+# (modprinc -kvno sets the kvno on all entries without deleting any).
+realm.run_kadminl('cpw -randkey -keepold -e aes256-cts:normal krbtgt/%s' %
+                  realm.realm)
+realm.run_kadminl('modprinc -kvno 1 krbtgt/%s' % realm.realm)
+output = realm.run_kadminl('getprinc krbtgt/%s' % realm.realm)
+if 'vno 1, aes256' not in output or 'vno 1, des3' not in output:
+    fail('keyrollover: setup for TGS enctype test failed')
+# Now present the DES3 ticket to the KDC and make sure it's rejected.
+realm.run_as_client([kvno, realm.host_princ], expected_code=1)
+
+realm.stop()
+
+# Test a cross-realm TGT key rollover scenario where realm 1 mimics
+# the Active Directory behavior of always using kvno 0 when issuing
+# cross-realm TGTs.  The first kvno invocation caches a cross-realm
+# TGT with the old key, and the second kvno invocation sends it to
+# r2's KDC with no kvno to identify it, forcing the KDC to try
+# multiple keys.
+r1, r2 = cross_realms(2, start_kadmind=False)
+r1.run_kadminl('modprinc -kvno 0 krbtgt/%s' % r2.realm)
+r1.run_as_client([kvno, r2.host_princ])
+r2.run_kadminl('cpw -pw newcross -keepold krbtgt/%s@%s' % (r2.realm, r1.realm))
+r1.run_kadminl('cpw -pw newcross krbtgt/%s' % r2.realm)
+r1.run_kadminl('modprinc -kvno 0 krbtgt/%s' % r2.realm)
+r1.run_as_client([kvno, r2.user_princ])
+
 success('keyrollover')