https://github.com/krb5/krb5/commit/eaff3bcf1a43541208b1fa63a3df50c2080458b7
commit eaff3bcf1a43541208b1fa63a3df50c2080458b7
Author: jason34105533 <[email protected]>
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.
[[email protected]: 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')
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.