From: jason34105533 Date: Thu, 23 Jul 2026 08:44:15 +0000 (+0800) Subject: Add auto_fast_armor realm variable X-Git-Url: http://git.ipfire.org/index.cgi?a=commitdiff_plain;p=thirdparty%2Fkrb5.git 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) --- diff --git a/doc/admin/conf_files/krb5_conf.rst b/doc/admin/conf_files/krb5_conf.rst index e0c7a63309..d7ea533443 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 20611d0672..8327c22624 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 5d0be63bf0..5aefd098a6 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 62c9f0841f..f9bef7f6b0 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 7156ea203f..1dbce2f3f0 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 00a5cceea5..63ec57e583 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 710b77810b..caaff61e88 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 149f46d5c9..18a12d56bb 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 91d4630a0a..0b6c0382ca 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')