Allow preauth mechs to work with clock skew
authorGreg Hudson <ghudson@mit.edu>
Tue, 17 Apr 2012 04:07:34 +0000 (04:07 +0000)
committerGreg Hudson <ghudson@mit.edu>
Tue, 17 Apr 2012 04:07:34 +0000 (04:07 +0000)
Add a clpreauth callback which gets the time of day using an offset
determined by the preauth-required error, and use it in encrypted
timestamp and encrypted challenge.  This timestamp is not necessarily
authenticated, but the security consequences for those preauth mechs
are minor (and can be mitigated by turning off kdc_timesync on
clients).

Based on a patch from Stef Walter.

ticket: 7114

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

src/include/k5-int.h
src/include/krb5/preauth_plugin.h
src/lib/krb5/krb/get_in_tkt.c
src/lib/krb5/krb/preauth2.c
src/lib/krb5/krb/preauth_ec.c
src/lib/krb5/krb/preauth_encts.c
src/lib/krb5/os/ustime.c
src/tests/t_skew.py

index 7ef421d2d9aba3ac89741aa70b1b166e7e8761c3..752b40efc0db74af0e599eda43449961ef398577 100644 (file)
@@ -665,6 +665,12 @@ typedef struct _krb5_os_context {
     char *                  default_ccname;
 } *krb5_os_context;
 
+/* Get the current time of day plus a specified offset. */
+krb5_error_code k5_time_with_offset(krb5_timestamp offset,
+                                    krb5_int32 offset_usec,
+                                    krb5_timestamp *time_out,
+                                    krb5_int32 *usec_out);
+
 /*
  * Flags for the os_flags field
  *
@@ -753,6 +759,11 @@ struct krb5_clpreauth_rock_st {
     krb5_principal client;
     krb5_prompter_fct prompter;
     void *prompter_data;
+
+    /* Discovered offset of server time during preauth */
+    krb5_timestamp pa_offset;
+    krb5_int32 pa_offset_usec;
+    enum { NO_OFFSET = 0, UNAUTH_OFFSET, AUTH_OFFSET } pa_offset_state;
 };
 
 typedef struct _krb5_pa_enc_ts {
index f732b947d2b57179b9ef318f312982684e8ad34f..72fd92d26febe0b212bc110768f75b0083d15c28 100644 (file)
@@ -176,6 +176,24 @@ typedef struct krb5_clpreauth_callbacks_st {
                                   const krb5_keyblock *keyblock);
 
     /* End of version 1 clpreauth callbacks. */
+
+    /*
+     * Get the current time for use in a preauth response.  If
+     * allow_unauth_time is true and the library has been configured to allow
+     * it, the current time will be offset using unauthenticated timestamp
+     * information received from the KDC in the preauth-required error, if one
+     * has been received.  Otherwise, the timestamp in a preauth-required error
+     * will only be used if it is protected by a FAST channel.  Only set
+     * allow_unauth_time if using an unauthenticated time offset would not
+     * create a security issue.
+     */
+    krb5_error_code (*get_preauth_time)(krb5_context context,
+                                        krb5_clpreauth_rock rock,
+                                        krb5_boolean allow_unauth_time,
+                                        krb5_timestamp *time_out,
+                                        krb5_int32 *usec_out);
+
+    /* End of version 2 clpreauth callbacks (added in 1.11). */
 } *krb5_clpreauth_callbacks;
 
 /*
index fc8df83dfe96ec8ae95e120ac5b1d7fae32f161c..aaabc4e2141f4ef5971a332e2caa17c9635f6054 100644 (file)
@@ -1281,6 +1281,23 @@ check_reply_enctype(krb5_init_creds_context ctx)
     return KRB5_CONFIG_ETYPE_NOSUPP;
 }
 
+/* Note the difference between the KDC's time, as reported to us in a
+ * preauth-required error, and the current time. */
+static void
+note_req_timestamp(krb5_context kcontext, krb5_clpreauth_rock rock,
+                   krb5_timestamp kdc_time, krb5_int32 kdc_usec)
+{
+    krb5_timestamp now;
+    krb5_int32 usec;
+
+    if (k5_time_with_offset(0, 0, &now, &usec) != 0)
+        return;
+    rock->pa_offset = kdc_time - now;
+    rock->pa_offset_usec = kdc_usec - usec;
+    rock->pa_offset_state = (rock->fast_state->armor_key != NULL) ?
+        AUTH_OFFSET : UNAUTH_OFFSET;
+}
+
 static krb5_error_code
 init_creds_step_reply(krb5_context context,
                       krb5_init_creds_context ctx,
@@ -1328,6 +1345,8 @@ init_creds_step_reply(krb5_context context,
             krb5_free_pa_data(context, ctx->preauth_to_use);
             ctx->preauth_to_use = ctx->err_padata;
             ctx->err_padata = NULL;
+            note_req_timestamp(context, &ctx->preauth_rock,
+                               ctx->err_reply->stime, ctx->err_reply->susec);
             /* this will trigger a new call to krb5_do_preauth() */
             krb5_free_error(context, ctx->err_reply);
             ctx->err_reply = NULL;
index 0c8ead5fe70766bbd7ecd626d04f01dd008958bb..06a135a36137427f9f1607ab05d3b9d2878b1c7e 100644 (file)
@@ -412,12 +412,31 @@ set_as_key(krb5_context context, krb5_clpreauth_rock rock,
     return krb5_copy_keyblock_contents(context, keyblock, rock->as_key);
 }
 
+static krb5_error_code
+get_preauth_time(krb5_context context, krb5_clpreauth_rock rock,
+                 krb5_boolean allow_unauth_time, krb5_timestamp *time_out,
+                 krb5_int32 *usec_out)
+{
+    if (rock->pa_offset_state != NO_OFFSET &&
+        (allow_unauth_time || rock->pa_offset_state == AUTH_OFFSET) &&
+        (context->library_options & KRB5_LIBOPT_SYNC_KDCTIME)) {
+        /* Use the offset we got from the preauth-required error. */
+        return k5_time_with_offset(rock->pa_offset, rock->pa_offset_usec,
+                                   time_out, usec_out);
+
+    } else {
+        /* Use the time offset from the context, or no offset. */
+        return krb5_us_timeofday(context, time_out, usec_out);
+    }
+}
+
 static struct krb5_clpreauth_callbacks_st callbacks = {
-    1,
+    2,
     get_etype,
     fast_armor,
     get_as_key,
-    set_as_key
+    set_as_key,
+    get_preauth_time
 };
 
 /* Tweak the request body, for now adding any enctypes which the module claims
index 7e7565b6f303ffaccf07055da5b6453fb9701080..48a4a17bf418154d39da48cd67b14cf96fcb1733 100644 (file)
@@ -92,7 +92,10 @@ ec_process(krb5_context context, krb5_clpreauth_moddata moddata,
         krb5_data *encoded_ts = NULL;
         krb5_pa_enc_ts ts;
         enc.ciphertext.data = NULL;
-        retval = krb5_us_timeofday(context, &ts.patimestamp, &ts.pausec);
+        /* Use the timestamp from the preauth-required error if possible.
+         * This time should always be secured by the FAST channel. */
+        retval = cb->get_preauth_time(context, rock, FALSE, &ts.patimestamp,
+                                      &ts.pausec);
         if (retval == 0)
             retval = encode_krb5_pa_enc_ts(&ts, &encoded_ts);
         if (retval == 0)
index 63e4259ebcc3e3c16a5e7a8afedbc9945192494e..559c6700f9fe208ca2a565adaa40f958e713cd07 100644 (file)
@@ -58,8 +58,15 @@ encts_process(krb5_context context, krb5_clpreauth_moddata moddata,
         goto cleanup;
     TRACE_PREAUTH_ENC_TS_KEY_GAK(context, as_key);
 
-    /* now get the time of day, and encrypt it accordingly */
-    ret = krb5_us_timeofday(context, &pa_enc.patimestamp, &pa_enc.pausec);
+    /*
+     * Try and use the timestamp of the preauth request, even if it's
+     * unauthenticated.  We could be fooled into making a preauth response for
+     * a future time, but that has no security consequences other than the
+     * KDC's audit logs.  If kdc_timesync is not configured, then this will
+     * just use local time.
+     */
+    ret = cb->get_preauth_time(context, rock, TRUE, &pa_enc.patimestamp,
+                               &pa_enc.pausec);
     if (ret)
         goto cleanup;
 
index be94a82185d7c121293c250907e4633c19004631..90fa4a6e25d6eb1cd08c3f28aed4a2a09317ddc1 100644 (file)
 
 #include "k5-int.h"
 
-krb5_error_code KRB5_CALLCONV
-krb5_us_timeofday(krb5_context context, krb5_timestamp *seconds, krb5_int32 *microseconds)
+krb5_error_code
+k5_time_with_offset(krb5_timestamp offset, krb5_int32 offset_usec,
+                    krb5_timestamp *time_out, krb5_int32 *usec_out)
 {
-    krb5_os_context os_ctx = &context->os_context;
     krb5_int32 sec, usec;
     krb5_error_code retval;
 
-    if (os_ctx->os_flags & KRB5_OS_TOFFSET_TIME) {
-        *seconds = os_ctx->time_offset;
-        *microseconds = os_ctx->usec_offset;
-        return 0;
-    }
     retval = krb5_crypto_us_timeofday(&sec, &usec);
     if (retval)
         return retval;
-    if (os_ctx->os_flags & KRB5_OS_TOFFSET_VALID) {
-        usec += os_ctx->usec_offset;
-        if (usec > 1000000) {
-            usec -= 1000000;
-            sec++;
-        }
-        if (usec < 0) {
-            usec += 1000000;
-            sec--;
-        }
-        sec += os_ctx->time_offset;
+    usec += offset_usec;
+    if (usec > 1000000) {
+        usec -= 1000000;
+        sec++;
+    }
+    if (usec < 0) {
+        usec += 1000000;
+        sec--;
     }
-    *seconds = sec;
-    *microseconds = usec;
+    sec += offset;
+
+    *time_out = sec;
+    *usec_out = usec;
     return 0;
 }
+
+krb5_error_code KRB5_CALLCONV
+krb5_us_timeofday(krb5_context context, krb5_timestamp *seconds,
+                  krb5_int32 *microseconds)
+{
+    krb5_os_context os_ctx = &context->os_context;
+
+    if (os_ctx->os_flags & KRB5_OS_TOFFSET_TIME) {
+        *seconds = os_ctx->time_offset;
+        *microseconds = os_ctx->usec_offset;
+        return 0;
+    } else if (os_ctx->os_flags & KRB5_OS_TOFFSET_VALID) {
+        return k5_time_with_offset(os_ctx->time_offset, os_ctx->usec_offset,
+                                   seconds, microseconds);
+    } else {
+        return krb5_crypto_us_timeofday(seconds, microseconds);
+    }
+}
index f00c2f92004f8e7af403c4eea43b66ea4b1bcfd7..f831035ac735fc1ede471ed8038378c55d73a8fa 100644 (file)
@@ -13,9 +13,13 @@ realm.kinit(realm.user_princ, password('user'), flags=['-T', realm.ccache])
 realm.run_as_client([kvno, realm.host_princ])
 realm.run_as_client([kdestroy])
 
-# kinit (with preauth) should fail.
+# kinit (with preauth) should work, with or without FAST.
 realm.run_kadminl('modprinc +requires_preauth user')
-realm.kinit(realm.user_princ, password('user'), expected_code=1)
+realm.kinit(realm.user_princ, password('user'))
+realm.run_as_client([kvno, realm.host_princ])
+realm.kinit(realm.user_princ, password('user'), flags=['-T', realm.ccache])
+realm.run_as_client([kvno, realm.host_princ])
+realm.run_as_client([kdestroy])
 
 realm.stop()
 
@@ -31,8 +35,10 @@ realm.run_as_client([kvno, realm.host_princ], expected_code=1)
 realm.kinit(realm.user_princ, password('user'), flags=['-T', realm.ccache],
             expected_code=1)
 
-# kinit (with preauth) should fail.
+# kinit (with preauth) should fail, with or without FAST.
 realm.run_kadminl('modprinc +requires_preauth user')
 realm.kinit(realm.user_princ, password('user'), expected_code=1)
+realm.kinit(realm.user_princ, password('user'), flags=['-T', realm.ccache],
+            expected_code=1)
 
 success('Clock skew tests')