[31564] in CVS-changelog-for-Kerberos-V5

home help back first fref pref prev next nref lref last post

krb5 commit: Add auto_fast_armor realm variable

daemon@ATHENA.MIT.EDU (ghudson@mit.edu)
Sat Aug 8 01:51:14 2026

From: ghudson@mit.edu
To: cvs-krb5@mit.edu
Message-Id: <20260808055107.1A5A7101C2A@krbdev.mit.edu>
Date: Sat,  8 Aug 2026 01:51:07 -0400 (EDT)
MIME-Version: 1.0
Reply-To: krbdev@mit.edu
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Errors-To: cvs-krb5-bounces@mit.edu

https://github.com/krb5/krb5/commit/eaff3bcf1a43541208b1fa63a3df50c2080458b7
commit eaff3bcf1a43541208b1fa63a3df50c2080458b7
Author: jason34105533 <a20040108jason@gmail.com>
Date:   Thu Jul 23 16:44:15 2026 +0800

    Add auto_fast_armor realm variable
    
    When auto_fast_armor is true and no FAST ccache name is provided in
    the GIC options, use anonymous PKINIT to acquire an armor ticket into
    a temporary memory ccache.  Refactor krb5int_fast_as_armor() and add
    an armor_ccache parameter so that we can pass in the memory ccache
    without modifying the caller's GIC options.
    
    [ghudson@mit.edu: edited commit messages, documentation, and comments;
    refactored some code changes for simplicity]
    
    ticket: 9226 (new)

 doc/admin/conf_files/krb5_conf.rst |  11 ++++
 src/include/k5-int.h               |   1 +
 src/include/k5-trace.h             |   6 +-
 src/lib/krb5/krb/fast.c            |  79 ++++++++++++++------------
 src/lib/krb5/krb/fast.h            |   3 +-
 src/lib/krb5/krb/get_in_tkt.c      | 111 ++++++++++++++++++++++++++++++++++++-
 src/lib/krb5/krb/init_creds_ctx.h  |   4 ++
 src/tests/gssapi/t_gssapi.py       |  18 ++++++
 src/tests/t_pkinit.py              |  27 +++++++--
 9 files changed, 215 insertions(+), 45 deletions(-)

diff --git a/doc/admin/conf_files/krb5_conf.rst b/doc/admin/conf_files/krb5_conf.rst
index e0c7a6330..d7ea53344 100644
--- a/doc/admin/conf_files/krb5_conf.rst
+++ b/doc/admin/conf_files/krb5_conf.rst
@@ -487,6 +487,17 @@ following tags may be specified in the realm's subsection:
     names to local user names.  The tag is the mapping name, and the
     value is the corresponding local user name.
 
+**auto_fast_armor**
+    If this flag is true, initial credential acquisition will acquire
+    an anonymous PKINIT ticket to use as FAST armor before making the
+    real ticket request, unless an armor ccache is already configured
+    (as with kinit's **-T** option).  This flag allows
+    preauthentication mechanisms that require FAST, such as OTP, to
+    work without a pre-existing ticket.  This flag should only be set
+    for realms known to support anonymous PKINIT.  This flag has no
+    effect if the client principal is itself the anonymous principal.
+    The default value is false.  New in release 1.23.
+
 **default_domain**
     This tag specifies the domain used to expand hostnames when
     translating Kerberos 4 service principals to Kerberos 5 principals
diff --git a/src/include/k5-int.h b/src/include/k5-int.h
index 20611d067..8327c2262 100644
--- a/src/include/k5-int.h
+++ b/src/include/k5-int.h
@@ -185,6 +185,7 @@ typedef unsigned char   u_char;
 #define KRB5_CONF_ALLOW_WEAK_CRYPTO            "allow_weak_crypto"
 #define KRB5_CONF_AUTH_TO_LOCAL                "auth_to_local"
 #define KRB5_CONF_AUTH_TO_LOCAL_NAMES          "auth_to_local_names"
+#define KRB5_CONF_AUTO_FAST_ARMOR              "auto_fast_armor"
 #define KRB5_CONF_CANONICALIZE                 "canonicalize"
 #define KRB5_CONF_CCACHE_TYPE                  "ccache_type"
 #define KRB5_CONF_CLOCKSKEW                    "clockskew"
diff --git a/src/include/k5-trace.h b/src/include/k5-trace.h
index 5d0be63bf..5aefd098a 100644
--- a/src/include/k5-trace.h
+++ b/src/include/k5-trace.h
@@ -173,8 +173,8 @@ void krb5int_trace(krb5_context context, const char *fmt, ...);
 #define TRACE_DNS_URI_SEND(c, domain)                   \
     TRACE(c, "Sending DNS URI query for {str}", domain)
 
-#define TRACE_FAST_ARMOR_CCACHE(c, ccache_name)         \
-    TRACE(c, "FAST armor ccache: {str}", ccache_name)
+#define TRACE_FAST_ARMOR_CCACHE(c, ccache)              \
+    TRACE(c, "FAST armor ccache: {ccache}", ccache)
 #define TRACE_FAST_ARMOR_CCACHE_KEY(c, keyblock)                \
     TRACE(c, "Armor ccache session key: {keyblock}", keyblock)
 #define TRACE_FAST_ARMOR_KEY(c, keyblock)               \
@@ -217,6 +217,8 @@ void krb5int_trace(krb5_context context, const char *fmt, ...);
 
 #define TRACE_INIT_CREDS(c, princ)                              \
     TRACE(c, "Getting initial credentials for {princ}", princ)
+#define TRACE_INIT_CREDS_AUTO_FAST_ARMOR(c)                             \
+    TRACE(c, "Acquiring anonymous PKINIT armor ticket for FAST")
 #define TRACE_INIT_CREDS_AS_KEY_GAK(c, keyblock)                        \
     TRACE(c, "AS key obtained from gak_fct: {keyblock}", keyblock)
 #define TRACE_INIT_CREDS_AS_KEY_PREAUTH(c, keyblock)                    \
diff --git a/src/lib/krb5/krb/fast.c b/src/lib/krb5/krb/fast.c
index 62c9f0841..f9bef7f6b 100644
--- a/src/lib/krb5/krb/fast.c
+++ b/src/lib/krb5/krb/fast.c
@@ -171,55 +171,62 @@ krb5int_fast_prep_req_body(krb5_context context,
 krb5_error_code
 krb5int_fast_as_armor(krb5_context context,
                       struct krb5int_fast_request_state *state,
-                      krb5_get_init_creds_opt *opt, krb5_kdc_req *request)
+                      krb5_get_init_creds_opt *opt,
+                      krb5_ccache armor_ccache, krb5_kdc_req *request)
 {
     krb5_error_code retval = 0;
     krb5_ccache ccache = NULL;
     krb5_principal target_principal = NULL;
-    krb5_data *target_realm;
+    krb5_data *target_realm, config_data = empty_data();
     const char *ccname = k5_gic_opt_get_fast_ccache_name(opt);
     krb5_flags fast_flags;
 
     krb5_clear_error_message(context);
     target_realm = &request->server->realm;
-    if (ccname != NULL) {
-        TRACE_FAST_ARMOR_CCACHE(context, ccname);
-        state->fast_state_flags |= KRB5INT_FAST_ARMOR_AVAIL;
+
+    if (armor_ccache == NULL) {
+        /* Stop if no armor ccache was provided by the direct caller or the GIC
+         * options. */
+        if (ccname == NULL)
+            return 0;
+        /* Resolve the armor ccache name provided in the GIC options. */
         retval = krb5_cc_resolve(context, ccname, &ccache);
-        if (retval == 0) {
-            retval = krb5int_tgtname(context, target_realm, target_realm,
-                                     &target_principal);
-        }
-        if (retval == 0) {
-            krb5_data config_data;
-            config_data.data = NULL;
-            retval = krb5_cc_get_config(context, ccache, target_principal,
-                                        KRB5_CC_CONF_FAST_AVAIL, &config_data);
-            if ((retval == 0) && config_data.data) {
-                TRACE_FAST_CCACHE_CONFIG(context);
-                state->fast_state_flags |= KRB5INT_FAST_DO_FAST;
-            }
-            krb5_free_data_contents(context, &config_data);
-            retval = 0;
-        }
-        fast_flags = k5_gic_opt_get_fast_flags(opt);
-        if (fast_flags & KRB5_FAST_REQUIRED) {
-            TRACE_FAST_REQUIRED(context);
-            state->fast_state_flags |= KRB5INT_FAST_DO_FAST;
-        }
-        if (retval == 0 && (state->fast_state_flags & KRB5INT_FAST_DO_FAST)) {
-            retval = fast_armor_ap_request(context, state, ccache,
-                                           target_principal);
-        }
-        if (retval != 0) {
-            k5_prependmsg(context, retval,
-                          _("Error constructing AP-REQ armor"));
-        }
+        if (retval)
+            goto cleanup;
+        armor_ccache = ccache;
+    }
+
+    TRACE_FAST_ARMOR_CCACHE(context, armor_ccache);
+
+    state->fast_state_flags |= KRB5INT_FAST_ARMOR_AVAIL;
+    retval = krb5int_tgtname(context, target_realm, target_realm,
+                             &target_principal);
+    if (retval)
+        goto cleanup;
+
+    retval = krb5_cc_get_config(context, armor_ccache, target_principal,
+                                KRB5_CC_CONF_FAST_AVAIL, &config_data);
+    if (!retval && config_data.data != NULL) {
+        TRACE_FAST_CCACHE_CONFIG(context);
+        state->fast_state_flags |= KRB5INT_FAST_DO_FAST;
     }
+
+    fast_flags = k5_gic_opt_get_fast_flags(opt);
+    if (fast_flags & KRB5_FAST_REQUIRED) {
+        TRACE_FAST_REQUIRED(context);
+        state->fast_state_flags |= KRB5INT_FAST_DO_FAST;
+    }
+
+    retval = fast_armor_ap_request(context, state, armor_ccache,
+                                   target_principal);
+    if (retval)
+        k5_prependmsg(context, retval, _("Error constructing AP-REQ armor"));
+
+cleanup:
     if (ccache)
         krb5_cc_close(context, ccache);
-    if (target_principal)
-        krb5_free_principal(context, target_principal);
+    krb5_free_principal(context, target_principal);
+    krb5_free_data_contents(context, &config_data);
     return retval;
 }
 
diff --git a/src/lib/krb5/krb/fast.h b/src/lib/krb5/krb/fast.h
index 7156ea203..1dbce2f3f 100644
--- a/src/lib/krb5/krb/fast.h
+++ b/src/lib/krb5/krb/fast.h
@@ -83,7 +83,8 @@ krb5int_fast_free_state(krb5_context context,
 krb5_error_code
 krb5int_fast_as_armor(krb5_context context,
                       struct krb5int_fast_request_state *state,
-                      krb5_get_init_creds_opt *opt, krb5_kdc_req *request);
+                      krb5_get_init_creds_opt *opt,
+                      krb5_ccache auto_armor_ccache, krb5_kdc_req *request);
 
 krb5_error_code
 krb5int_fast_reply_key(krb5_context context,
diff --git a/src/lib/krb5/krb/get_in_tkt.c b/src/lib/krb5/krb/get_in_tkt.c
index 00a5cceea..63ec57e58 100644
--- a/src/lib/krb5/krb/get_in_tkt.c
+++ b/src/lib/krb5/krb/get_in_tkt.c
@@ -537,6 +537,10 @@ krb5_init_creds_free(krb5_context context,
     krb5_free_data_contents(context, &ctx->salt);
     krb5_free_data_contents(context, &ctx->s2kparams);
     krb5_free_keyblock_contents(context, &ctx->as_key);
+    krb5_init_creds_free(context, ctx->auto_armor_ctx);
+    krb5_get_init_creds_opt_free(context, ctx->auto_armor_opt);
+    if (ctx->auto_armor_ccache != NULL)
+        krb5_cc_destroy(context, ctx->auto_armor_ccache);
     k5_json_release(ctx->cc_config_in);
     k5_json_release(ctx->cc_config_out);
     free(ctx);
@@ -771,6 +775,87 @@ encts_disabled(profile_t profile, const krb5_data *realm)
     return (ret == 0) ? bval : FALSE;
 }
 
+/* Return true if auto_fast_armor is enabled for realm. */
+static krb5_boolean
+auto_fast_armor_enabled(profile_t profile, const krb5_data *realm)
+{
+    krb5_error_code ret;
+    char *realmstr;
+    int bval;
+
+    realmstr = k5memdup0(realm->data, realm->length, &ret);
+    if (realmstr == NULL)
+        return FALSE;
+    ret = profile_get_boolean(profile, KRB5_CONF_REALMS, realmstr,
+                              KRB5_CONF_AUTO_FAST_ARMOR, FALSE, &bval);
+    free(realmstr);
+    return (ret == 0) ? bval : FALSE;
+}
+
+/*
+ * Return true if ctx should first acquire FAST armor using anonymous PKINIT.
+ * This decision is primarily dependent on the auto_fast_armor config option,
+ * but we don't acquire armor if the caller passed in an armor ccache or if the
+ * state machine is already performing an anonymous PKINIT request.
+ */
+static krb5_boolean
+want_auto_armor(krb5_context context, krb5_init_creds_context ctx)
+{
+    if (k5_gic_opt_get_fast_ccache_name(ctx->opt) != NULL)
+        return FALSE;
+    if (krb5_principal_compare_any_realm(context, ctx->request->client,
+                                         krb5_anonymous_principal()))
+        return FALSE;
+    return auto_fast_armor_enabled(context->profile,
+                                   &ctx->request->client->realm);
+}
+
+/* Create a memory ccache and nested init_creds context for acquiring FAST amor
+ * via anonymous PKINIT. */
+static krb5_error_code
+begin_auto_armor(krb5_context context, krb5_init_creds_context ctx)
+{
+    krb5_error_code ret;
+    krb5_principal anon_princ = NULL;
+    const krb5_data *realm = &ctx->request->client->realm;
+
+    TRACE_INIT_CREDS_AUTO_FAST_ARMOR(context);
+
+    ret = krb5_cc_new_unique(context, "MEMORY", NULL, &ctx->auto_armor_ccache);
+    if (ret)
+        goto cleanup;
+
+    ret = krb5_build_principal_ext(context, &anon_princ,
+                                   realm->length, realm->data,
+                                   strlen(KRB5_WELLKNOWN_NAMESTR),
+                                   KRB5_WELLKNOWN_NAMESTR,
+                                   strlen(KRB5_ANONYMOUS_PRINCSTR),
+                                   KRB5_ANONYMOUS_PRINCSTR, 0);
+    if (ret)
+        goto cleanup;
+    anon_princ->type = KRB5_NT_WELLKNOWN;
+
+    ret = krb5_get_init_creds_opt_alloc(context, &ctx->auto_armor_opt);
+    if (ret)
+        goto cleanup;
+    krb5_get_init_creds_opt_set_anonymous(ctx->auto_armor_opt, 1);
+    krb5_get_init_creds_opt_set_tkt_life(ctx->auto_armor_opt, 60 * 60);
+    ret = krb5_get_init_creds_opt_set_out_ccache(context, ctx->auto_armor_opt,
+                                                 ctx->auto_armor_ccache);
+    if (ret)
+        goto cleanup;
+
+    ret = krb5_init_creds_init(context, anon_princ, NULL, NULL,
+                               ctx->start_time, ctx->auto_armor_opt,
+                               &ctx->auto_armor_ctx);
+    if (ret)
+        goto cleanup;
+
+cleanup:
+    krb5_free_principal(context, anon_princ);
+    return ret;
+}
+
 /**
  * Throw away any pre-authentication realm state and begin with a
  * unauthenticated or optimistically authenticated request.  If fast_upgrade is
@@ -827,7 +912,7 @@ restart_init_creds_loop(krb5_context context, krb5_init_creds_context ctx,
         goto cleanup;
 
     code = krb5int_fast_as_armor(context, ctx->fast_state, ctx->opt,
-                                 ctx->request);
+                                 ctx->auto_armor_ccache, ctx->request);
     if (code != 0)
         goto cleanup;
     /* give the preauth plugins a chance to prep the request body */
@@ -1043,6 +1128,12 @@ krb5_init_creds_init(krb5_context context,
         ctx->request->client->type = KRB5_NT_WELLKNOWN;
     }
 
+    if (want_auto_armor(context, ctx)) {
+        code = begin_auto_armor(context, ctx);
+        if (code)
+            goto cleanup;
+    }
+
     *pctx = ctx;
     ctx = NULL;
 
@@ -1891,7 +1982,23 @@ krb5_init_creds_step(krb5_context context,
     if (code)
         return code;
 
-    if (in->length != 0) {
+    if (ctx->auto_armor_ctx != NULL) {
+        /* Drive the nested context to acquire an anonymous TGT. */
+        code = krb5_init_creds_step(context, ctx->auto_armor_ctx, in, out,
+                                    realm, flags);
+        if (code || (*flags & KRB5_INIT_CREDS_STEP_FLAG_CONTINUE))
+            return code;
+
+        /* The nested context is complete.  Discard it to signal that the outer
+         * state machine should proceed using auto_armor_ccache. */
+        krb5_init_creds_free(context, ctx->auto_armor_ctx);
+        ctx->auto_armor_ctx = NULL;
+
+        /* Begin the actual AS request, asserting that FAST is available. */
+        code = restart_init_creds_loop(context, ctx, TRUE);
+        if (code)
+            return code;
+    } else if (in->length != 0) {
         code = init_creds_step_reply(context, ctx, in);
         if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG) {
             code2 = krb5int_copy_data_contents(context,
diff --git a/src/lib/krb5/krb/init_creds_ctx.h b/src/lib/krb5/krb/init_creds_ctx.h
index 710b77810..caaff61e8 100644
--- a/src/lib/krb5/krb/init_creds_ctx.h
+++ b/src/lib/krb5/krb/init_creds_ctx.h
@@ -64,6 +64,10 @@ struct _krb5_init_creds_context {
     krb5_boolean info_pa_permitted;
     krb5_boolean restarted;
     krb5_boolean encts_disabled;
+    /* Automatic FAST armor state machine and memory cache */
+    krb5_init_creds_context auto_armor_ctx;
+    krb5_get_init_creds_opt *auto_armor_opt;
+    krb5_ccache auto_armor_ccache;
     struct krb5_responder_context_st rctx;
     krb5_preauthtype current_preauth_type;
     krb5_preauthtype selected_preauth_type;
diff --git a/src/tests/gssapi/t_gssapi.py b/src/tests/gssapi/t_gssapi.py
index 149f46d5c..18a12d56b 100755
--- a/src/tests/gssapi/t_gssapi.py
+++ b/src/tests/gssapi/t_gssapi.py
@@ -255,4 +255,22 @@ check_lifetime('actx gss_accept_sec_context', ln[7], 8000 * 86400 + 300)
 check_lifetime('actx gss_inquire_context', ln[8], 8000 * 86400 + 300)
 check_lifetime('actx gss_context_time', ln[9], 8000 * 86400 + 300)
 
+realm.stop()
+
+# Test auto_fast_armor with IAKERB driving the state machine.
+if pkinit_enabled:
+    mark('IAKERB with auto_fast_armor')
+    afa_conf = {'realms': {'$realm': {'auto_fast_armor': 'true'}}}
+    realm = K5Realm(krb5_conf=afa_conf, get_creds=False, pkinit=True)
+    realm.run([kadminl, 'modprinc', '+preauth', realm.user_princ])
+    realm.addprinc('WELLKNOWN/ANONYMOUS')
+    msgs = ('Acquiring anonymous PKINIT armor ticket for FAST',
+            'Getting initial credentials for WELLKNOWN/ANONYMOUS',
+            'Using FAST due to armor ccache negotiation result',
+            'Preauth module encrypted_challenge (138) (real) returned: 0')
+    realm.run(['./t_iakerb', 'p:' + realm.user_princ, password('user'),
+               'h:host@' + hostname, 'h:host'], expected_trace=msgs)
+else:
+    print('Skipping IAKERB auto_fast_armor test: PKINIT not built')
+
 success('GSSAPI tests')
diff --git a/src/tests/t_pkinit.py b/src/tests/t_pkinit.py
index 91d4630a0..0b6c0382c 100755
--- a/src/tests/t_pkinit.py
+++ b/src/tests/t_pkinit.py
@@ -105,10 +105,6 @@ 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.
 mark('anonymous')
 realm.kinit('@%s' % realm.realm, flags=['-n'], expected_code=1,
@@ -124,6 +120,29 @@ if '97:' in out:
 realm.run([klist, '-C'], expected_msg='start_realm = KRBTEST.COM')
 realm.run([kvno, '-S', 'host', hostname])
 
+# Test auto_fast_armor.
+mark('auto_fast_armor')
+afa_conf = {'realms': {'$realm': {'auto_fast_armor': 'true'}}}
+afa_env = realm.special_env('auto_fast', False, krb5_conf=afa_conf)
+msgs = ('Acquiring anonymous PKINIT armor ticket for FAST',
+        'Getting initial credentials for WELLKNOWN/ANONYMOUS@%s' % realm.realm,
+        'Using FAST due to armor ccache negotiation result',
+        'Preauth module encrypted_challenge (138) (real) returned: 0/Success')
+realm.kinit(realm.user_princ, password=password('user'), env=afa_env,
+            expected_trace=msgs)
+realm.klist(realm.user_princ)
+
+# auto_fast_armor shouldn't trigger for direct use of anonymous PKINIT.
+mark('anonymous (auto_fast_armor=true)')
+out, trace = realm.kinit('@%s' % realm.realm, flags=['-n'], env=afa_env,
+                         return_trace=True)
+if 'Acquiring anonymous PKINIT armor ticket for FAST' in trace:
+    fail('auto_fast_armor improperly triggered for anonymous kinit')
+
+# For the remaining tests in this realm, remove the keys on user for
+# better error reporting (by preventing encrypted timestamp fallback).
+realm.run([kadminl, 'purgekeys', '-all', realm.user_princ])
+
 # Test anonymous kadmin.
 mark('anonymous kadmin')
 f = open(os.path.join(realm.testdir, 'acl'), 'a')
_______________________________________________
cvs-krb5 mailing list
cvs-krb5@mit.edu
https://mailman.mit.edu/mailman/listinfo/cvs-krb5

home help back first fref pref prev next nref lref last post