From 70f41a8dafaadfb43aba4918564c22460f812dca Mon Sep 17 00:00:00 2001 From: Greg Hudson Date: Thu, 5 Apr 2018 16:23:34 -0400 Subject: [PATCH] Restrict pre-authentication fallback cases Add a new callback disable_fallback() and call it from each clpreauth module when it generates a client message using credentials to authenticate. (For SPAKE, this is the message responding to a challenge; for all other current mechanisms, it is the first and only client message.) If disable_fallback() is called, do not try another mechanism after a KDC error. Remove k5_reset_preauth_types_tried() and its call sites, so that preauth mechanisms which are tried optimistically will no longer be retried after a failure. ticket: 8654 (cherry picked from commit 7a24a088c16d326127dd2b29084d4ca085c70d10) --- src/include/krb5/clpreauth_plugin.h | 14 ++++ src/lib/krb5/krb/get_in_tkt.c | 21 +++--- src/lib/krb5/krb/init_creds_ctx.h | 1 + src/lib/krb5/krb/int-proto.h | 3 - src/lib/krb5/krb/preauth2.c | 23 +++---- src/lib/krb5/krb/preauth_ec.c | 1 + src/lib/krb5/krb/preauth_encts.c | 2 + src/lib/krb5/krb/preauth_otp.c | 4 ++ src/lib/krb5/krb/preauth_sam2.c | 1 + src/plugins/preauth/pkinit/pkinit_clnt.c | 1 + src/plugins/preauth/spake/spake_client.c | 4 ++ src/plugins/preauth/test/cltest.c | 11 +++ src/tests/t_preauth.py | 88 +++++++++++++++++++++--- src/tests/t_spake.py | 9 +-- 14 files changed, 134 insertions(+), 49 deletions(-) diff --git a/src/include/krb5/clpreauth_plugin.h b/src/include/krb5/clpreauth_plugin.h index 0106734ad..5317669b7 100644 --- a/src/include/krb5/clpreauth_plugin.h +++ b/src/include/krb5/clpreauth_plugin.h @@ -160,7 +160,21 @@ typedef struct krb5_clpreauth_callbacks_st { krb5_error_code (*set_cc_config)(krb5_context context, krb5_clpreauth_rock rock, const char *key, const char *data); + /* End of version 2 clpreauth callbacks (added in 1.11). */ + + /* + * Prevent further fallbacks to other preauth mechanisms if the KDC replies + * with an error. (The module itself can still respond to errors with its + * tryagain method, or continue after KDC_ERR_MORE_PREAUTH_DATA_REQUIRED + * errors with its process method.) A module should invoke this callback + * from the process method when it generates an authenticated request using + * credentials; often this will be the first or only client message + * generated by the mechanism. + */ + void (*disable_fallback)(krb5_context context, krb5_clpreauth_rock rock); + + /* End of version 3 clpreauth callbacks (added in 1.17). */ } *krb5_clpreauth_callbacks; /* diff --git a/src/lib/krb5/krb/get_in_tkt.c b/src/lib/krb5/krb/get_in_tkt.c index 1d96ff163..c026bbc6d 100644 --- a/src/lib/krb5/krb/get_in_tkt.c +++ b/src/lib/krb5/krb/get_in_tkt.c @@ -1331,9 +1331,7 @@ init_creds_step_request(krb5_context context, krb5_free_pa_data(context, ctx->optimistic_padata); ctx->optimistic_padata = NULL; if (code) { - /* Make an unauthenticated request, and possibly try again using - * the same mechanisms as we tried optimistically. */ - k5_reset_preauth_types_tried(ctx); + /* Make an unauthenticated request. */ krb5_clear_error_message(context); code = 0; } @@ -1361,6 +1359,9 @@ init_creds_step_request(krb5_context context, /* Don't continue after a keyboard interrupt. */ if (code == KRB5_LIBOS_PWDINTR) goto cleanup; + /* Don't continue if fallback is disabled. */ + if (code && ctx->fallback_disabled) + goto cleanup; if (code) { /* See if we can try a different preauth mech before giving up. */ k5_save_ctx_error(context, code, &save); @@ -1549,16 +1550,10 @@ init_creds_step_reply(krb5_context context, } else if (reply_code == KDC_ERR_PREAUTH_FAILED && retry) { note_req_timestamp(context, ctx, ctx->err_reply->stime, ctx->err_reply->susec); - if (ctx->method_padata == NULL) { - /* Optimistic preauth failed on the KDC. Allow all mechanisms - * to be tried again using method data. */ - k5_reset_preauth_types_tried(ctx); - } else { - /* Don't try again with the mechanism that failed. */ - code = k5_preauth_note_failed(ctx, ctx->selected_preauth_type); - if (code) - goto cleanup; - } + /* Don't try again with the mechanism that failed. */ + code = k5_preauth_note_failed(ctx, ctx->selected_preauth_type); + if (code) + goto cleanup; ctx->selected_preauth_type = KRB5_PADATA_NONE; /* Accept or update method data if the KDC sent it. */ if (ctx->err_padata != NULL) diff --git a/src/lib/krb5/krb/init_creds_ctx.h b/src/lib/krb5/krb/init_creds_ctx.h index b19410a13..7ba61e17c 100644 --- a/src/lib/krb5/krb/init_creds_ctx.h +++ b/src/lib/krb5/krb/init_creds_ctx.h @@ -60,6 +60,7 @@ struct _krb5_init_creds_context { krb5_enctype etype; krb5_boolean info_pa_permitted; krb5_boolean restarted; + krb5_boolean fallback_disabled; struct krb5_responder_context_st rctx; krb5_preauthtype selected_preauth_type; krb5_preauthtype allowed_preauth_type; diff --git a/src/lib/krb5/krb/int-proto.h b/src/lib/krb5/krb/int-proto.h index cda9010e3..d20133885 100644 --- a/src/lib/krb5/krb/int-proto.h +++ b/src/lib/krb5/krb/int-proto.h @@ -197,9 +197,6 @@ k5_init_preauth_context(krb5_context context); void k5_free_preauth_context(krb5_context context); -void -k5_reset_preauth_types_tried(krb5_init_creds_context ctx); - krb5_error_code k5_preauth_note_failed(krb5_init_creds_context ctx, krb5_preauthtype pa_type); diff --git a/src/lib/krb5/krb/preauth2.c b/src/lib/krb5/krb/preauth2.c index 451e0b7a8..1f17ec2b0 100644 --- a/src/lib/krb5/krb/preauth2.c +++ b/src/lib/krb5/krb/preauth2.c @@ -203,18 +203,6 @@ cleanup: free_handles(context, list); } -/* Reset the memory of which preauth types we have already tried. */ -void -k5_reset_preauth_types_tried(krb5_init_creds_context ctx) -{ - krb5_preauth_req_context reqctx = ctx->preauth_reqctx; - - if (reqctx == NULL) - return; - free(reqctx->failed); - reqctx->failed = NULL; -} - /* Add pa_type to the list of types which has previously failed. */ krb5_error_code k5_preauth_note_failed(krb5_init_creds_context ctx, krb5_preauthtype pa_type) @@ -553,8 +541,14 @@ set_cc_config(krb5_context context, krb5_clpreauth_rock rock, return ret; } +static void +disable_fallback(krb5_context context, krb5_clpreauth_rock rock) +{ + ((krb5_init_creds_context)rock)->fallback_disabled = TRUE; +} + static struct krb5_clpreauth_callbacks_st callbacks = { - 2, + 3, get_etype, fast_armor, get_as_key, @@ -564,7 +558,8 @@ static struct krb5_clpreauth_callbacks_st callbacks = { responder_get_answer, need_as_key, get_cc_config, - set_cc_config + set_cc_config, + disable_fallback }; /* Tweak the request body, for now adding any enctypes which the module claims diff --git a/src/lib/krb5/krb/preauth_ec.c b/src/lib/krb5/krb/preauth_ec.c index c1aa9090f..75aab770e 100644 --- a/src/lib/krb5/krb/preauth_ec.c +++ b/src/lib/krb5/krb/preauth_ec.c @@ -138,6 +138,7 @@ ec_process(krb5_context context, krb5_clpreauth_moddata moddata, encoded_ts->data = NULL; *out_padata = pa; pa = NULL; + cb->disable_fallback(context, rock); } free(pa); krb5_free_data(context, encoded_ts); diff --git a/src/lib/krb5/krb/preauth_encts.c b/src/lib/krb5/krb/preauth_encts.c index cec384227..45bf9da92 100644 --- a/src/lib/krb5/krb/preauth_encts.c +++ b/src/lib/krb5/krb/preauth_encts.c @@ -109,6 +109,8 @@ encts_process(krb5_context context, krb5_clpreauth_moddata moddata, *out_padata = pa; pa = NULL; + cb->disable_fallback(context, rock); + cleanup: krb5_free_data(context, ts); krb5_free_data(context, enc_ts); diff --git a/src/lib/krb5/krb/preauth_otp.c b/src/lib/krb5/krb/preauth_otp.c index 48fcbb5d5..13e584657 100644 --- a/src/lib/krb5/krb/preauth_otp.c +++ b/src/lib/krb5/krb/preauth_otp.c @@ -1123,6 +1123,10 @@ otp_client_process(krb5_context context, krb5_clpreauth_moddata moddata, /* Encode the request into the pa_data output. */ retval = set_pa_data(req, pa_data_out); + if (retval != 0) + goto error; + cb->disable_fallback(context, rock); + error: krb5_free_data_contents(context, &value); krb5_free_data_contents(context, &pin); diff --git a/src/lib/krb5/krb/preauth_sam2.c b/src/lib/krb5/krb/preauth_sam2.c index c8a330655..4c70021a9 100644 --- a/src/lib/krb5/krb/preauth_sam2.c +++ b/src/lib/krb5/krb/preauth_sam2.c @@ -410,6 +410,7 @@ sam2_process(krb5_context context, krb5_clpreauth_moddata moddata, sam_padata[1] = NULL; *out_padata = sam_padata; + cb->disable_fallback(context, rock); return(0); } diff --git a/src/plugins/preauth/pkinit/pkinit_clnt.c b/src/plugins/preauth/pkinit/pkinit_clnt.c index 9483d69e5..77e9e5308 100644 --- a/src/plugins/preauth/pkinit/pkinit_clnt.c +++ b/src/plugins/preauth/pkinit/pkinit_clnt.c @@ -179,6 +179,7 @@ pa_pkinit_gen_req(krb5_context context, *out_padata = return_pa_data; return_pa_data = NULL; + cb->disable_fallback(context, rock); cleanup: krb5_free_data(context, der_req); diff --git a/src/plugins/preauth/spake/spake_client.c b/src/plugins/preauth/spake/spake_client.c index 47a6ba26c..00734a13b 100644 --- a/src/plugins/preauth/spake/spake_client.c +++ b/src/plugins/preauth/spake/spake_client.c @@ -278,6 +278,10 @@ process_challenge(krb5_context context, groupstate *gstate, reqstate *st, goto cleanup; TRACE_SPAKE_SEND_RESPONSE(context); ret = convert_to_padata(response, pa_out); + if (ret) + goto cleanup; + + cb->disable_fallback(context, rock); cleanup: krb5_free_keyblock(context, k0); diff --git a/src/plugins/preauth/test/cltest.c b/src/plugins/preauth/test/cltest.c index f5f7c5aba..51b848481 100644 --- a/src/plugins/preauth/test/cltest.c +++ b/src/plugins/preauth/test/cltest.c @@ -53,6 +53,9 @@ * - If the "fail_optimistic", "fail_2rt", or "fail_tryagain" gic options are * set, it fails with a recognizable error string at the requested point in * processing. + * + * - If the "disable_fallback" gic option is set, fallback is disabled when a + * client message is generated. */ #include "k5-int.h" @@ -66,6 +69,7 @@ struct client_state { krb5_boolean fail_optimistic; krb5_boolean fail_2rt; krb5_boolean fail_tryagain; + krb5_boolean disable_fallback; }; struct client_request_state { @@ -81,6 +85,7 @@ test_init(krb5_context context, krb5_clpreauth_moddata *moddata_out) assert(st != NULL); st->indicators = NULL; st->fail_optimistic = st->fail_2rt = st->fail_tryagain = FALSE; + st->disable_fallback = FALSE; *moddata_out = (krb5_clpreauth_moddata)st; return 0; } @@ -138,6 +143,8 @@ test_process(krb5_context context, krb5_clpreauth_moddata moddata, return KRB5_PREAUTH_FAILED; } *out_pa_data = make_pa_list("optimistic", 10); + if (st->disable_fallback) + cb->disable_fallback(context, rock); return 0; } else if (reqst->second_round_trip) { printf("2rt: %.*s\n", pa_data->length, pa_data->contents); @@ -166,6 +173,8 @@ test_process(krb5_context context, krb5_clpreauth_moddata moddata, indstr = (st->indicators != NULL) ? st->indicators : ""; *out_pa_data = make_pa_list(indstr, strlen(indstr)); + if (st->disable_fallback) + cb->disable_fallback(context, rock); return 0; } @@ -212,6 +221,8 @@ test_gic_opt(krb5_context kcontext, krb5_clpreauth_moddata moddata, st->fail_2rt = TRUE; } else if (strcmp(attr, "fail_tryagain") == 0) { st->fail_tryagain = TRUE; + } else if (strcmp(attr, "disable_fallback") == 0) { + st->disable_fallback = TRUE; } return 0; } diff --git a/src/tests/t_preauth.py b/src/tests/t_preauth.py index efb3ea20d..32e35b08b 100644 --- a/src/tests/t_preauth.py +++ b/src/tests/t_preauth.py @@ -37,8 +37,8 @@ expected_trace = ('Attempting optimistic preauth', realm.run(['./icred', '-o', '-123', realm.user_princ, password('user')], expected_trace=expected_trace) -# Test optimistic preauth failing on client, followed by successful -# preauth using the same module. +# Test optimistic preauth failing on client, falling back to encrypted +# timestamp. msgs = ('Attempting optimistic preauth', 'Processing preauth types: -123', '/induced optimistic fail', @@ -46,15 +46,15 @@ msgs = ('Attempting optimistic preauth', '/Additional pre-authentication required', 'Preauthenticating using KDC method data', 'Processing preauth types:', - 'Preauth module test (-123) (real) returned: 0/Success', - 'Produced preauth for next request: PA-FX-COOKIE (133), -123', + 'Encrypted timestamp (for ', + 'module encrypted_timestamp (2) (real) returned: 0/Success', + 'preauth for next request: PA-FX-COOKIE (133), PA-ENC-TIMESTAMP (2)', 'Decrypted AS reply') realm.run(['./icred', '-o', '-123', '-X', 'fail_optimistic', realm.user_princ, - password('user')], expected_msg='testval', - expected_trace=msgs) + password('user')], expected_trace=msgs) -# Test optimistic preauth failing on KDC, followed by successful preauth -# using the same module. +# Test optimistic preauth failing on KDC, falling back to encrypted +# timestamp. realm.run([kadminl, 'setstr', realm.user_princ, 'failopt', 'yes']) msgs = ('Attempting optimistic preauth', 'Processing preauth types: -123', @@ -63,11 +63,24 @@ msgs = ('Attempting optimistic preauth', '/Preauthentication failed', 'Preauthenticating using KDC method data', 'Processing preauth types:', - 'Preauth module test (-123) (real) returned: 0/Success', - 'Produced preauth for next request: PA-FX-COOKIE (133), -123', + 'Encrypted timestamp (for ', + 'module encrypted_timestamp (2) (real) returned: 0/Success', + 'preauth for next request: PA-FX-COOKIE (133), PA-ENC-TIMESTAMP (2)', 'Decrypted AS reply') realm.run(['./icred', '-o', '-123', realm.user_princ, password('user')], - expected_msg='testval', expected_trace=msgs) + expected_trace=msgs) +# Leave failopt set for the next test. + +# Test optimistic preauth failing on KDC, stopping because the test +# module disabled fallback. +msgs = ('Attempting optimistic preauth', + 'Processing preauth types: -123', + 'Preauth module test (-123) (real) returned: 0/Success', + 'Produced preauth for next request: -123', + '/Preauthentication failed') +realm.run(['./icred', '-X', 'disable_fallback', '-o', '-123', realm.user_princ, + password('user')], expected_code=1, + expected_msg='Preauthentication failed', expected_trace=msgs) realm.run([kadminl, 'delstr', realm.user_princ, 'failopt']) # Test KDC_ERR_MORE_PREAUTH_DATA_REQUIRED and secure cookies. @@ -107,6 +120,23 @@ msgs = ('Sending unauthenticated request', realm.run(['./icred', '-X', 'fail_2rt', realm.user_princ, password('user')], expected_msg='2rt: secondtrip', expected_trace=msgs) +# Test client-side failure after KDC_ERR_MORE_PREAUTH_DATA_REQUIRED, +# stopping because the test module disabled fallback. +msgs = ('Sending unauthenticated request', + '/Additional pre-authentication required', + 'Preauthenticating using KDC method data', + 'Processing preauth types:', + 'Preauth module test (-123) (real) returned: 0/Success', + 'Produced preauth for next request: PA-FX-COOKIE (133), -123', + '/More preauthentication data is required', + 'Continuing preauth mech -123', + 'Processing preauth types: -123, PA-FX-COOKIE (133)', + '/induced 2rt fail') +realm.run(['./icred', '-X', 'fail_2rt', '-X', 'disable_fallback', + realm.user_princ, password('user')], expected_code=1, + expected_msg='Pre-authentication failed: induced 2rt fail', + expected_trace=msgs) + # Test KDC-side failure after KDC_ERR_MORE_PREAUTH_DATA_REQUIRED, # falling back to encrypted timestamp. realm.run([kadminl, 'setstr', realm.user_princ, 'fail2rt', 'yes']) @@ -130,6 +160,25 @@ msgs = ('Sending unauthenticated request', 'Decrypted AS reply') realm.run(['./icred', realm.user_princ, password('user')], expected_msg='2rt: secondtrip', expected_trace=msgs) +# Leave fail2rt set for the next test. + +# Test KDC-side failure after KDC_ERR_MORE_PREAUTH_DATA_REQUIRED, +# stopping because the test module disabled fallback. +msgs = ('Sending unauthenticated request', + '/Additional pre-authentication required', + 'Preauthenticating using KDC method data', + 'Processing preauth types:', + 'Preauth module test (-123) (real) returned: 0/Success', + 'Produced preauth for next request: PA-FX-COOKIE (133), -123', + '/More preauthentication data is required', + 'Continuing preauth mech -123', + 'Processing preauth types: -123, PA-FX-COOKIE (133)', + 'Preauth module test (-123) (real) returned: 0/Success', + 'Produced preauth for next request: PA-FX-COOKIE (133), -123', + '/Preauthentication failed') +realm.run(['./icred', '-X', 'disable_fallback', + realm.user_princ, password('user')], expected_code=1, + expected_msg='Preauthentication failed', expected_trace=msgs) realm.run([kadminl, 'delstr', realm.user_princ, 'fail2rt']) # Test tryagain flow by inducing a KDC_ERR_ENCTYPE_NOSUPP error on the KDC. @@ -170,6 +219,23 @@ msgs = ('Sending unauthenticated request', realm.run(['./icred', '-X', 'fail_tryagain', realm.user_princ, password('user')], expected_trace=msgs) +# Test a client-side tryagain failure, stopping because the test +# module disabled fallback. +msgs = ('Sending unauthenticated request', + '/Additional pre-authentication required', + 'Preauthenticating using KDC method data', + 'Processing preauth types:', + 'Preauth module test (-123) (real) returned: 0/Success', + 'Produced preauth for next request: PA-FX-COOKIE (133), -123', + '/KDC has no support for encryption type', + 'Recovering from KDC error 14 using preauth mech -123', + 'Preauth tryagain input types (-123): -123, PA-FX-COOKIE (133)', + '/induced tryagain fail') +realm.run(['./icred', '-X', 'fail_tryagain', '-X', 'disable_fallback', + realm.user_princ, password('user')], expected_code=1, + expected_msg='KDC has no support for encryption type', + expected_trace=msgs) + # Test that multiple stepwise initial creds operations can be # performed with the same krb5_context, with proper tracking of # clpreauth module request handles. diff --git a/src/tests/t_spake.py b/src/tests/t_spake.py index a81a238b4..5b47e62d3 100644 --- a/src/tests/t_spake.py +++ b/src/tests/t_spake.py @@ -31,9 +31,7 @@ for gnum, gname in groups: 'Decrypted AS reply') realm.kinit('user', 'pw', expected_trace=msgs) - # Test an unsuccessful authentication. (The client will try - # again with encrypted timestamp, which isn't really desired, - # but check for that as long as it is expected.) + # Test an unsuccessful authentication. msgs = ('/Additional pre-authentication required', 'Selected etype info:', 'Sending SPAKE support message', @@ -42,9 +40,6 @@ for gnum, gname in groups: 'Continuing preauth mech PA-SPAKE (151)', 'SPAKE challenge received with group ' + str(gnum), 'Sending SPAKE response', - '/Preauthentication failed', - 'Encrypted timestamp ', - 'for next request: PA-FX-COOKIE (133), PA-ENC-TIMESTAMP (2)', '/Preauthentication failed') realm.kinit('user', 'wrongpw', expected_code=1, expected_trace=msgs) @@ -114,8 +109,6 @@ msgs = ('Attempting optimistic preauth', 'for next request: PA-SPAKE (151)', '/Preauthentication failed', 'Selected etype info:', - 'SPAKE challenge with group 1 rejected', - 'spake (151) (real) returned: -1765328360/Preauthentication failed', 'Encrypted timestamp ', 'for next request: PA-FX-COOKIE (133), PA-ENC-TIMESTAMP (2)', 'AS key determined by preauth:',