]> git.ipfire.org Git - thirdparty/krb5.git/commitdiff
Add PKINIT KDC support for freshness token 742/head
authorGreg Hudson <ghudson@mit.edu>
Mon, 12 Mar 2018 15:31:46 +0000 (11:31 -0400)
committerGreg Hudson <ghudson@mit.edu>
Mon, 19 Mar 2018 20:11:53 +0000 (16:11 -0400)
Send a freshness token in the preauth hint list if PKINIT is
configured and the request padata indicates support.  Verify the
freshness token if the client includes one in a PKINIT request, and
log whether one was received.  If pkinit_require_freshness is set to
true in the realm config, reject non-anonymous requests which don't
contain a freshness token.

Add freshness token tests to t_pkinit.py with some related changes.
Remove client long-term keys after testing password preauth so we get
better error reporting when pkinit_require_freshness is set and a
token is not sent.  Remove ./responder invocations for test cases
which don't ask PKINIT responder questions, or else the responder
would fail now that it isn't being asked for the password.  Leave
anonymous PKINIT enabled after the anonymous tests so that we can use
it again when testing enforcement of pkinit_require_freshness.  Add
expected trace messages for the basic test, including one for
receiving a freshness token.  Add minimal expected trace messages for
the RSA test.

ticket: 8648

13 files changed:
doc/admin/conf_files/kdc_conf.rst
doc/admin/pkinit.rst
doc/appdev/refs/macros/index.rst
doc/formats/freshness_token.rst [new file with mode: 0644]
doc/formats/index.rst
src/include/krb5/kdcpreauth_plugin.h
src/include/krb5/krb5.hin
src/kdc/do_as_req.c
src/kdc/kdc_preauth.c
src/kdc/kdc_util.h
src/plugins/preauth/pkinit/pkinit.h
src/plugins/preauth/pkinit/pkinit_srv.c
src/tests/t_pkinit.py

index 395907968c2733626189c982e8daf2896f15d359..fc6528e244a1cab6711acb6a5c2665c0aa8eb1e4 100644 (file)
@@ -794,6 +794,10 @@ For information about the syntax of some of these options, see
     **pkinit_require_crl_checking** should be set to true if the
     policy is such that up-to-date CRLs must be present for every CA.
 
+**pkinit_require_freshness**
+    Specifies whether to require clients to include a freshness token
+    in PKINIT requests.  The default value is false.  (New in release
+    1.17.)
 
 .. _Encryption_types:
 
index c601c5c9ebbadc222d601ef2a6bdc2a0cfd75ae5..bec4fc800c28b18ca28745b6c075d51708298f27 100644 (file)
@@ -327,3 +327,28 @@ appropriate :ref:`kdc_realms` subsection of the KDC's
 To obtain anonymous credentials on a client, run ``kinit -n``, or
 ``kinit -n @REALMNAME`` to specify a realm.  The resulting tickets
 will have the client name ``WELLKNOWN/ANONYMOUS@WELLKNOWN:ANONYMOUS``.
+
+
+Freshness tokens
+----------------
+
+Freshness tokens can ensure that the client has recently had access to
+its certificate private key.  If freshness tokens are not required by
+the KDC, a client program with temporary possession of the private key
+can compose requests for future timestamps and use them later.
+
+In release 1.17 and later, freshness tokens are supported by the
+client and are sent by the KDC when the client indicates support for
+them.  Because not all clients support freshness tokens yet, they are
+not required by default.  To check if freshness tokens are supported
+by a realm's clients, look in the KDC logs for the lines::
+
+    PKINIT: freshness token received from <client principal>
+    PKINIT: no freshness token received from <client principal>
+
+To require freshness tokens for all clients in a realm (except for
+clients authenticating anonymously), set the
+**pkinit_require_freshness** variable to ``true`` in the appropriate
+:ref:`kdc_realms` subsection of the KDC's :ref:`kdc.conf(5)` file.  To
+test that this option is in effect, run ``kinit -X disable_freshness``
+and verify that authentication is unsuccessful.
index e7674710257697bef6efa2bc7a28a9ce8a35d9b5..dba818b263a2db6f7fc548ac72f28364e607424a 100644 (file)
@@ -181,6 +181,7 @@ Public
    KRB5_KEYUSAGE_KRB_ERROR_CKSUM.rst
    KRB5_KEYUSAGE_KRB_PRIV_ENCPART.rst
    KRB5_KEYUSAGE_KRB_SAFE_CKSUM.rst
+   KRB5_KEYUSAGE_PA_AS_FRESHNESS.rst
    KRB5_KEYUSAGE_PA_FX_COOKIE.rst
    KRB5_KEYUSAGE_PA_OTP_REQUEST.rst
    KRB5_KEYUSAGE_PA_PKINIT_KX.rst
@@ -241,6 +242,7 @@ Public
    KRB5_PADATA_AFS3_SALT.rst
    KRB5_PADATA_AP_REQ.rst
    KRB5_PADATA_AS_CHECKSUM.rst
+   KRB5_PADATA_AS_FRESHNESS.rst
    KRB5_PADATA_ENCRYPTED_CHALLENGE.rst
    KRB5_PADATA_ENC_SANDIA_SECURID.rst
    KRB5_PADATA_ENC_TIMESTAMP.rst
diff --git a/doc/formats/freshness_token.rst b/doc/formats/freshness_token.rst
new file mode 100644 (file)
index 0000000..3127621
--- /dev/null
@@ -0,0 +1,19 @@
+PKINIT freshness tokens
+=======================
+
+:rfc:`8070` specifies a pa-data type PA_AS_FRESHNESS, which clients
+should reflect within signed PKINIT data to prove recent access to the
+client certificate private key.  The contents of a freshness token are
+left to the KDC implementation.  The MIT krb5 KDC uses the following
+format for freshness tokens (starting in release 1.17):
+
+* a four-byte big-endian POSIX timestamp
+* a four-byte big-endian key version number
+* an :rfc:`3961` checksum, with no ASN.1 wrapper
+
+The checksum is computed using the first key in the local krbtgt
+principal entry for the realm (e.g. ``krbtgt/KRBTEST.COM@KRBTEST.COM``
+if the request is to the ``KRBTEST.COM`` realm) of the indicated key
+version.  The checksum type must be the mandatory checksum type for
+the encryption type of the krbtgt key.  The key usage value for the
+checksum is 514.
index 8b30626d4b454a8f8839f232f6928dde4cbde274..4ad5344245076bd67ad087248dcdcdd0d5cb1ea4 100644 (file)
@@ -7,3 +7,4 @@ Protocols and file formats
    ccache_file_format
    keytab_file_format
    cookie
+   freshness_token
index f388200999ef62cffdf1589ebc862e4a0d8044b0..3a47542340bb41f8cb8813f9adf269cb6b60be5d 100644 (file)
@@ -240,6 +240,23 @@ typedef struct krb5_kdcpreauth_callbacks_st {
 
     /* End of version 4 kdcpreauth callbacks. */
 
+    /*
+     * Instruct the KDC to send a freshness token in the method data
+     * accompanying a PREAUTH_REQUIRED or PREAUTH_FAILED error, if the client
+     * indicated support for freshness tokens.  This callback should only be
+     * invoked from the edata method.
+     */
+    void (*send_freshness_token)(krb5_context context,
+                                 krb5_kdcpreauth_rock rock);
+
+    /* Validate a freshness token sent by the client.  Return 0 on success,
+     * KRB5KDC_ERR_PREAUTH_EXPIRED on error. */
+    krb5_error_code (*check_freshness_token)(krb5_context context,
+                                             krb5_kdcpreauth_rock rock,
+                                             const krb5_data *token);
+
+    /* End of version 5 kdcpreauth callbacks. */
+
 } *krb5_kdcpreauth_callbacks;
 
 /* Optional: preauth plugin initialization function. */
index bebd9a53df7816164f7f87b338bef99125e5cf2c..b298bb0189d05aad69ceaf6fe292952cf90e37f6 100644 (file)
@@ -1029,7 +1029,10 @@ krb5_c_keyed_checksum_types(krb5_context context, krb5_enctype enctype,
 #define KRB5_KEYUSAGE_AS_REQ 56
 #define KRB5_KEYUSAGE_CAMMAC 64
 
+/* Key usage values 512-1023 are reserved for uses internal to a Kerberos
+ * implementation. */
 #define KRB5_KEYUSAGE_PA_FX_COOKIE 513  /**< Used for encrypted FAST cookies */
+#define KRB5_KEYUSAGE_PA_AS_FRESHNESS 514  /**< Used for freshness tokens */
 /** @} */ /* end of KRB5_KEYUSAGE group */
 
 /**
index 7c8da63e1861ac7d492020fef5cc6d8bcf601e90..588c1375a8fac2b4674847cc1b188298b2f1931b 100644 (file)
@@ -563,6 +563,7 @@ process_as_req(krb5_kdc_req *request, krb5_data *req_pkt,
     state->rock.rstate = state->rstate;
     state->rock.vctx = vctx;
     state->rock.auth_indicators = &state->auth_indicators;
+    state->rock.send_freshness_token = FALSE;
     if (!state->request->client) {
         state->status = "NULL_CLIENT";
         errcode = KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN;
@@ -659,6 +660,7 @@ process_as_req(krb5_kdc_req *request, krb5_data *req_pkt,
         state->status = "GET_LOCAL_TGT";
         goto errout;
     }
+    state->rock.local_tgt = state->local_tgt;
 
     au_state->stage = VALIDATE_POL;
 
index fdf67d9d8dc90e1b032a91b791fe50a48f07dcb0..62ff9a8a7e62ce278394ad72f6c1fd5ff3c2ffe2 100644 (file)
@@ -87,6 +87,9 @@
 #include <assert.h>
 #include <krb5/kdcpreauth_plugin.h>
 
+/* Let freshness tokens be valid for ten minutes. */
+#define FRESHNESS_LIFETIME 600
+
 typedef struct preauth_system_st {
     const char *name;
     int type;
@@ -497,8 +500,68 @@ client_name(krb5_context context, krb5_kdcpreauth_rock rock)
     return rock->client->princ;
 }
 
+static void
+send_freshness_token(krb5_context context, krb5_kdcpreauth_rock rock)
+{
+    rock->send_freshness_token = TRUE;
+}
+
+static krb5_error_code
+check_freshness_token(krb5_context context, krb5_kdcpreauth_rock rock,
+                      const krb5_data *token)
+{
+    krb5_timestamp token_ts, now;
+    krb5_key_data *kd;
+    krb5_keyblock kb;
+    krb5_kvno token_kvno;
+    krb5_checksum cksum;
+    krb5_data d;
+    uint8_t *token_cksum;
+    size_t token_cksum_len;
+    krb5_boolean valid = FALSE;
+    char ckbuf[4];
+
+    memset(&kb, 0, sizeof(kb));
+
+    if (krb5_timeofday(context, &now) != 0)
+        goto cleanup;
+
+    if (token->length <= 8)
+        goto cleanup;
+    token_ts = load_32_be(token->data);
+    token_kvno = load_32_be(token->data + 4);
+    token_cksum = (uint8_t *)token->data + 8;
+    token_cksum_len = token->length - 8;
+
+    /* Check if the token timestamp is too old. */
+    if (ts_after(now, ts_incr(token_ts, FRESHNESS_LIFETIME)))
+        goto cleanup;
+
+    /* Fetch and decrypt the local krbtgt key of the token's kvno. */
+    if (krb5_dbe_find_enctype(context, rock->local_tgt, -1, -1, token_kvno,
+                              &kd) != 0)
+        goto cleanup;
+    if (krb5_dbe_decrypt_key_data(context, NULL, kd, &kb, NULL) != 0)
+        goto cleanup;
+
+    /* Verify the token checksum against the current KDC time.  The checksum
+     * must use the mandatory checksum type of the krbtgt key's enctype. */
+    store_32_be(token_ts, ckbuf);
+    d = make_data(ckbuf, sizeof(ckbuf));
+    cksum.magic = KV5M_CHECKSUM;
+    cksum.checksum_type = 0;
+    cksum.length = token_cksum_len;
+    cksum.contents = token_cksum;
+    (void)krb5_c_verify_checksum(context, &kb, KRB5_KEYUSAGE_PA_AS_FRESHNESS,
+                                 &d, &cksum, &valid);
+
+cleanup:
+    krb5_free_keyblock_contents(context, &kb);
+    return valid ? 0 : KRB5KDC_ERR_PREAUTH_EXPIRED;
+}
+
 static struct krb5_kdcpreauth_callbacks_st callbacks = {
-    4,
+    5,
     max_time_skew,
     client_keys,
     free_keys,
@@ -514,7 +577,9 @@ static struct krb5_kdcpreauth_callbacks_st callbacks = {
     get_cookie,
     set_cookie,
     match_client,
-    client_name
+    client_name,
+    send_freshness_token,
+    check_freshness_token
 };
 
 static krb5_error_code
@@ -771,6 +836,62 @@ cleanup:
     return ret;
 }
 
+static krb5_error_code
+add_freshness_token(krb5_context context, krb5_kdcpreauth_rock rock,
+                    krb5_pa_data ***pa_list)
+{
+    krb5_error_code ret;
+    krb5_timestamp now;
+    krb5_key_data *kd;
+    krb5_keyblock kb;
+    krb5_checksum cksum;
+    krb5_data d;
+    krb5_pa_data *pa;
+    char ckbuf[4];
+
+    memset(&cksum, 0, sizeof(cksum));
+    memset(&kb, 0, sizeof(kb));
+
+    if (!rock->send_freshness_token)
+        return 0;
+    if (krb5int_find_pa_data(context, rock->request->padata,
+                             KRB5_PADATA_AS_FRESHNESS) == NULL)
+        return 0;
+
+    /* Fetch and decrypt the current local krbtgt key. */
+    ret = krb5_dbe_find_enctype(context, rock->local_tgt, -1, -1, 0, &kd);
+    if (ret)
+        goto cleanup;
+    ret = krb5_dbe_decrypt_key_data(context, NULL, kd, &kb, NULL);
+    if (ret)
+        goto cleanup;
+
+    /* Compute a checksum over the current KDC time. */
+    ret = krb5_timeofday(context, &now);
+    if (ret)
+        goto cleanup;
+    store_32_be(now, ckbuf);
+    d = make_data(ckbuf, sizeof(ckbuf));
+    ret = krb5_c_make_checksum(context, 0, &kb, KRB5_KEYUSAGE_PA_AS_FRESHNESS,
+                               &d, &cksum);
+
+    /* Compose a freshness token from the time, krbtgt kvno, and checksum. */
+    ret = alloc_pa_data(KRB5_PADATA_AS_FRESHNESS, 8 + cksum.length, &pa);
+    if (ret)
+        goto cleanup;
+    store_32_be(now, pa->contents);
+    store_32_be(kd->key_data_kvno, pa->contents + 4);
+    memcpy(pa->contents + 8, cksum.contents, cksum.length);
+
+    /* add_pa_data_element() claims pa on success or failure. */
+    ret = add_pa_data_element(pa_list, pa);
+
+cleanup:
+    krb5_free_keyblock_contents(context, &kb);
+    krb5_free_checksum_contents(context, &cksum);
+    return ret;
+}
+
 struct hint_state {
     kdc_hint_respond_fn respond;
     void *arg;
@@ -793,6 +914,11 @@ hint_list_finish(struct hint_state *state, krb5_error_code code)
     void *oldarg = state->arg;
     kdc_realm_t *kdc_active_realm = state->realm;
 
+    /* Add a freshness token if a preauth module requested it and the client
+     * request indicates support for it. */
+    if (!code)
+        code = add_freshness_token(kdc_context, state->rock, &state->pa_data);
+
     if (!code) {
         if (state->pa_data == NULL) {
             krb5_klog_syslog(LOG_INFO,
index 18649b8ad54be7a8b2cd502278ef46d650d3bdb7..a63af2503f48fabe6352fbceda40217be2f11997 100644 (file)
@@ -427,11 +427,13 @@ struct krb5_kdcpreauth_rock_st {
     krb5_kdc_req *request;
     krb5_data *inner_body;
     krb5_db_entry *client;
+    krb5_db_entry *local_tgt;
     krb5_key_data *client_key;
     krb5_keyblock *client_keyblock;
     struct kdc_request_state *rstate;
     verto_ctx *vctx;
     krb5_data ***auth_indicators;
+    krb5_boolean send_freshness_token;
 };
 
 #define isflagset(flagfield, flag) (flagfield & (flag))
index 8489a3e231844940fa4af13c63f122cd7b5e0f79..fe2ec0d31f85db2babb63c9ed262a555662540ef 100644 (file)
@@ -77,6 +77,7 @@
 #define KRB5_CONF_PKINIT_KDC_OCSP               "pkinit_kdc_ocsp"
 #define KRB5_CONF_PKINIT_POOL                   "pkinit_pool"
 #define KRB5_CONF_PKINIT_REQUIRE_CRL_CHECKING   "pkinit_require_crl_checking"
+#define KRB5_CONF_PKINIT_REQUIRE_FRESHNESS      "pkinit_require_freshness"
 #define KRB5_CONF_PKINIT_REVOKE                 "pkinit_revoke"
 
 /* Make pkiDebug(fmt,...) print, or not.  */
@@ -148,6 +149,7 @@ typedef struct _pkinit_plg_opts {
     int allow_upn;         /* allow UPN-SAN instead of pkinit-SAN */
     int dh_or_rsa;         /* selects DH or RSA based pkinit */
     int require_crl_checking; /* require CRL for a CA (default is false) */
+    int require_freshness;  /* require freshness token (default is false) */
     int disable_freshness;  /* disable freshness token on client for testing */
     int dh_min_bits;       /* minimum DH modulus size allowed */
 } pkinit_plg_opts;
index 4e9685885f80f08775ddf49be598064ce3ecef0a..bbfde34b2cc30e1ec51d058bf9cf48bb5c829ece 100644 (file)
@@ -161,6 +161,10 @@ pkinit_server_get_edata(krb5_context context,
     if (plgctx == NULL)
         retval = EINVAL;
 
+    /* Send a freshness token if the client requested one. */
+    if (!retval)
+        cb->send_freshness_token(context, rock);
+
     (*respond)(arg, retval, NULL);
 }
 
@@ -396,6 +400,31 @@ cleanup:
     return ret;
 }
 
+/* Return an error if freshness tokens are required and one was not received.
+ * Log an appropriate message indicating whether a valid token was received. */
+static krb5_error_code
+check_log_freshness(krb5_context context, pkinit_kdc_context plgctx,
+                    krb5_kdc_req *request, krb5_boolean valid_freshness_token)
+{
+    krb5_error_code ret;
+    char *name = NULL;
+
+    ret = krb5_unparse_name(context, request->client, &name);
+    if (ret)
+        return ret;
+    if (plgctx->opts->require_freshness && !valid_freshness_token) {
+        com_err("", 0, _("PKINIT: no freshness token, rejecting auth from %s"),
+                name);
+        ret = KRB5KDC_ERR_PREAUTH_FAILED;
+    } else if (valid_freshness_token) {
+        com_err("", 0, _("PKINIT: freshness token received from %s"), name);
+    } else {
+        com_err("", 0, _("PKINIT: no freshness token received from %s"), name);
+    }
+    krb5_free_unparsed_name(context, name);
+    return ret;
+}
+
 static void
 pkinit_server_verify_padata(krb5_context context,
                             krb5_data *req_pkt,
@@ -418,10 +447,11 @@ pkinit_server_verify_padata(krb5_context context,
     pkinit_kdc_req_context reqctx = NULL;
     krb5_checksum cksum = {0, 0, 0, NULL};
     krb5_data *der_req = NULL;
-    krb5_data k5data;
+    krb5_data k5data, *ftoken;
     int is_signed = 1;
     krb5_pa_data **e_data = NULL;
     krb5_kdcpreauth_modreq modreq = NULL;
+    krb5_boolean valid_freshness_token = FALSE;
     char **sp;
 
     pkiDebug("pkinit_verify_padata: entered!\n");
@@ -592,6 +622,14 @@ pkinit_server_verify_padata(krb5_context context,
             goto cleanup;
         }
 
+        ftoken = auth_pack->pkAuthenticator.freshnessToken;
+        if (ftoken != NULL) {
+            retval = cb->check_freshness_token(context, rock, ftoken);
+            if (retval)
+                goto cleanup;
+            valid_freshness_token = TRUE;
+        }
+
         /* check if kdcPkId present and match KDC's subjectIdentifier */
         if (reqp->kdcPkId.data != NULL) {
             int valid_kdcPkId = 0;
@@ -634,6 +672,13 @@ pkinit_server_verify_padata(krb5_context context,
         break;
     }
 
+    if (is_signed) {
+        retval = check_log_freshness(context, plgctx, request,
+                                     valid_freshness_token);
+        if (retval)
+            goto cleanup;
+    }
+
     if (is_signed && plgctx->auth_indicators != NULL) {
         /* Assert configured authentication indicators. */
         for (sp = plgctx->auth_indicators; *sp != NULL; sp++) {
@@ -1323,6 +1368,10 @@ pkinit_init_kdc_profile(krb5_context context, pkinit_kdc_context plgctx)
                               KRB5_CONF_PKINIT_REQUIRE_CRL_CHECKING,
                               0, &plgctx->opts->require_crl_checking);
 
+    pkinit_kdcdefault_boolean(context, plgctx->realmname,
+                              KRB5_CONF_PKINIT_REQUIRE_FRESHNESS,
+                              0, &plgctx->opts->require_freshness);
+
     pkinit_kdcdefault_string(context, plgctx->realmname,
                              KRB5_CONF_PKINIT_EKU_CHECKING,
                              &eku_string);
index b790a7cda071264f8eef0ec910a39387cd8f074d..3030322e10f262c83f83b81cdf4748ebc6b5bafa 100755 (executable)
@@ -39,6 +39,8 @@ pkinit_kdc_conf = {'realms': {'$realm': {
             'pkinit_indicator': ['indpkinit1', 'indpkinit2']}}}
 restrictive_kdc_conf = {'realms': {'$realm': {
             'restrict_anonymous_to_tgt': 'true' }}}
+freshness_kdc_conf = {'realms': {'$realm': {
+            'pkinit_require_freshness': 'true'}}}
 
 testprincs = {'krbtgt/KRBTEST.COM': {'keys': 'aes128-cts'},
               'user': {'keys': 'aes128-cts', 'flags': '+preauth'},
@@ -118,6 +120,10 @@ realm.kinit(realm.user_princ, password=password('user'))
 realm.klist(realm.user_princ)
 realm.run([kvno, realm.host_princ])
 
+# Having tested password preauth, remove the keys for better error
+# reporting.
+realm.run([kadminl, 'purgekeys', '-all', realm.user_princ])
+
 # Test anonymous PKINIT.
 realm.kinit('@%s' % realm.realm, flags=['-n'], expected_code=1,
             expected_msg='not found in Kerberos database')
@@ -153,23 +159,32 @@ realm.run([kvno, realm.host_princ], expected_code=1,
 realm.kinit(realm.host_princ, flags=['-k'])
 realm.run([kvno, '-U', 'user', realm.host_princ])
 
-# Go back to a normal KDC and disable anonymous PKINIT.
+# Go back to the normal KDC environment.
 realm.stop_kdc()
 realm.start_kdc()
-realm.run([kadminl, 'delprinc', 'WELLKNOWN/ANONYMOUS'])
 
 # Run the basic test - PKINIT with FILE: identity, with no password on the key.
-realm.run(['./responder', '-x', 'pkinit=',
-           '-X', 'X509_user_identity=%s' % file_identity, realm.user_princ])
 realm.kinit(realm.user_princ,
-            flags=['-X', 'X509_user_identity=%s' % file_identity])
+            flags=['-X', 'X509_user_identity=%s' % file_identity],
+            expected_trace=('Sending unauthenticated request',
+                            '/Additional pre-authentication required',
+                            'Preauthenticating using KDC method data',
+                            'PKINIT client received freshness token from KDC',
+                            'PKINIT loading CA certs and CRLs from FILE',
+                            'PKINIT client making DH request',
+                            'Produced preauth for next request: 133, 16',
+                            'PKINIT client verified DH reply',
+                            'PKINIT client found id-pkinit-san in KDC cert',
+                            'PKINIT client matched KDC principal krbtgt/'))
 realm.klist(realm.user_princ)
 realm.run([kvno, realm.host_princ])
 
 # Try again using RSA instead of DH.
 realm.kinit(realm.user_princ,
             flags=['-X', 'X509_user_identity=%s' % file_identity,
-                   '-X', 'flag_RSA_PROTOCOL=yes'])
+                   '-X', 'flag_RSA_PROTOCOL=yes'],
+            expected_trace=('PKINIT client making RSA request',
+                            'PKINIT client verified RSA reply'))
 realm.klist(realm.user_princ)
 
 # Test a DH parameter renegotiation by temporarily setting a 4096-bit
@@ -192,8 +207,23 @@ expected_trace = ('Sending unauthenticated request',
 realm.kinit(realm.user_princ,
             flags=['-X', 'X509_user_identity=%s' % file_identity],
             expected_trace=expected_trace)
+
+# Test enforcement of required freshness tokens.  (We can leave
+# freshness tokens required after this test.)
+realm.kinit(realm.user_princ,
+            flags=['-X', 'X509_user_identity=%s' % file_identity,
+                   '-X', 'disable_freshness=yes'])
+f_env = realm.special_env('freshness', True, kdc_conf=freshness_kdc_conf)
 realm.stop_kdc()
-realm.start_kdc()
+realm.start_kdc(env=f_env)
+realm.kinit(realm.user_princ,
+            flags=['-X', 'X509_user_identity=%s' % file_identity])
+realm.kinit(realm.user_princ,
+            flags=['-X', 'X509_user_identity=%s' % file_identity,
+                   '-X', 'disable_freshness=yes'],
+            expected_code=1, expected_msg='Preauthentication failed')
+# Anonymous should never require a freshness token.
+realm.kinit('@%s' % realm.realm, flags=['-n', '-X', 'disable_freshness=yes'])
 
 # Run the basic test - PKINIT with FILE: identity, with a password on the key,
 # supplied by the prompter.
@@ -229,8 +259,6 @@ shutil.copy(privkey_pem, os.path.join(path, 'user.key'))
 shutil.copy(privkey_enc_pem, os.path.join(path_enc, 'user.key'))
 shutil.copy(user_pem, os.path.join(path, 'user.crt'))
 shutil.copy(user_pem, os.path.join(path_enc, 'user.crt'))
-realm.run(['./responder', '-x', 'pkinit=', '-X',
-           'X509_user_identity=%s' % dir_identity, realm.user_princ])
 realm.kinit(realm.user_princ,
             flags=['-X', 'X509_user_identity=%s' % dir_identity])
 realm.klist(realm.user_princ)
@@ -262,8 +290,6 @@ realm.klist(realm.user_princ)
 realm.run([kvno, realm.host_princ])
 
 # PKINIT with PKCS12: identity, with no password on the bundle.
-realm.run(['./responder', '-x', 'pkinit=',
-           '-X', 'X509_user_identity=%s' % p12_identity, realm.user_princ])
 realm.kinit(realm.user_princ,
             flags=['-X', 'X509_user_identity=%s' % p12_identity])
 realm.klist(realm.user_princ)
@@ -350,8 +376,6 @@ conf = open(softpkcs11rc, 'w')
 conf.write("%s\t%s\t%s\t%s\n" % ('user', 'user token', user_pem, privkey_pem))
 conf.close()
 # Expect to succeed without having to supply any more information.
-realm.run(['./responder', '-x', 'pkinit=',
-           '-X', 'X509_user_identity=%s' % p11_identity, realm.user_princ])
 realm.kinit(realm.user_princ,
             flags=['-X', 'X509_user_identity=%s' % p11_identity])
 realm.klist(realm.user_princ)