[31564] in CVS-changelog-for-Kerberos-V5
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