From 6bd9bc03f2ad2aa5415d738c28180def7e17874f Mon Sep 17 00:00:00 2001 From: Greg Hudson Date: Thu, 15 Nov 2018 13:40:43 -0500 Subject: [PATCH] Clear forwardable flag instead of denying request If the client requests a forwardable or proxiable ticket and the option cannot be honored by policy, issue a non-forwardable or non-proxiable ticket rather than denying the request. Add a test script for testing KDC request options and populate it with tests for the forwardable and proxiable flags. ticket: 7871 (cherry picked from commit 08e948cce2c79a3604066fcf7a64fc527456f83d) --- src/kdc/do_as_req.c | 19 ++------ src/kdc/do_tgs_req.c | 58 +++++----------------- src/kdc/kdc_util.c | 82 ++++++++++++++++++------------- src/kdc/kdc_util.h | 9 ++-- src/kdc/tgs_policy.c | 8 +-- src/tests/Makefile.in | 1 + src/tests/gcred.c | 28 ++++++++--- src/tests/t_kdcoptions.py | 100 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 190 insertions(+), 115 deletions(-) create mode 100644 src/tests/t_kdcoptions.py diff --git a/src/kdc/do_as_req.c b/src/kdc/do_as_req.c index 588c1375a..8a96c12a9 100644 --- a/src/kdc/do_as_req.c +++ b/src/kdc/do_as_req.c @@ -192,13 +192,6 @@ finish_process_as_req(struct as_req_state *state, krb5_error_code errcode) au_state->stage = ENCR_REP; - if ((errcode = validate_forwardable(state->request, *state->client, - *state->server, state->kdc_time, - &state->status))) { - errcode += ERROR_TABLE_BASE_krb5; - goto egress; - } - errcode = check_indicators(kdc_context, state->server, state->auth_indicators); if (errcode) { @@ -708,12 +701,11 @@ process_as_req(krb5_kdc_req *request, krb5_data *req_pkt, } /* Copy options that request the corresponding ticket flags. */ - state->enc_tkt_reply.flags = OPTS2FLAGS(state->request->kdc_options); + state->enc_tkt_reply.flags = get_ticket_flags(state->request->kdc_options, + state->client, state->server, + NULL); state->enc_tkt_reply.times.authtime = state->authtime; - setflag(state->enc_tkt_reply.flags, TKT_FLG_INITIAL); - setflag(state->enc_tkt_reply.flags, TKT_FLG_ENC_PA_REP); - /* * It should be noted that local policy may affect the * processing of any of these flags. For example, some @@ -732,10 +724,9 @@ process_as_req(krb5_kdc_req *request, krb5_data *req_pkt, state->enc_tkt_reply.transited.tr_type = KRB5_DOMAIN_X500_COMPRESS; state->enc_tkt_reply.transited.tr_contents = empty_string; - if (isflagset(state->request->kdc_options, KDC_OPT_POSTDATED)) { - setflag(state->enc_tkt_reply.flags, TKT_FLG_INVALID); + if (isflagset(state->request->kdc_options, KDC_OPT_POSTDATED)) state->enc_tkt_reply.times.starttime = state->request->from; - } else + else state->enc_tkt_reply.times.starttime = state->kdc_time; kdc_get_ticket_endtime(kdc_active_realm, diff --git a/src/kdc/do_tgs_req.c b/src/kdc/do_tgs_req.c index 587342a6c..1da099318 100644 --- a/src/kdc/do_tgs_req.c +++ b/src/kdc/do_tgs_req.c @@ -378,15 +378,16 @@ process_tgs_req(krb5_kdc_req *request, krb5_data *pkt, else ticket_reply.server = request->server; /* XXX careful for realm... */ - enc_tkt_reply.flags = OPTS2FLAGS(request->kdc_options); - enc_tkt_reply.flags |= COPY_TKT_FLAGS(header_enc_tkt->flags); + enc_tkt_reply.flags = get_ticket_flags(request->kdc_options, client, + server, header_enc_tkt); enc_tkt_reply.times.starttime = 0; - if (isflagset(server->attributes, KRB5_KDB_OK_AS_DELEGATE)) - setflag(enc_tkt_reply.flags, TKT_FLG_OK_AS_DELEGATE); - - /* Indicate support for encrypted padata (RFC 6806). */ - setflag(enc_tkt_reply.flags, TKT_FLG_ENC_PA_REP); + /* OK_TO_AUTH_AS_DELEGATE must be set on the service requesting S4U2Self + * for forwardable tickets to be issued. */ + if (isflagset(c_flags, KRB5_KDB_FLAG_PROTOCOL_TRANSITION) && + !is_referral && + !isflagset(server->attributes, KRB5_KDB_OK_TO_AUTH_AS_DELEGATE)) + clear(enc_tkt_reply.flags, TKT_FLG_FORWARDABLE); /* don't use new addresses unless forwarded, see below */ @@ -401,37 +402,6 @@ process_tgs_req(krb5_kdc_req *request, krb5_data *pkt, * realms may refuse to issue renewable tickets */ - if (isflagset(request->kdc_options, KDC_OPT_FORWARDABLE)) { - - if (isflagset(c_flags, KRB5_KDB_FLAG_PROTOCOL_TRANSITION)) { - /* - * If S4U2Self principal is not forwardable, then mark ticket as - * unforwardable. This behaviour matches Windows, but it is - * different to the MIT AS-REQ path, which returns an error - * (KDC_ERR_POLICY) if forwardable tickets cannot be issued. - * - * Consider this block the S4U2Self equivalent to - * validate_forwardable(). - */ - if (client != NULL && - isflagset(client->attributes, KRB5_KDB_DISALLOW_FORWARDABLE)) - clear(enc_tkt_reply.flags, TKT_FLG_FORWARDABLE); - /* - * Forwardable flag is propagated along referral path. - */ - else if (!isflagset(header_enc_tkt->flags, TKT_FLG_FORWARDABLE)) - clear(enc_tkt_reply.flags, TKT_FLG_FORWARDABLE); - /* - * OK_TO_AUTH_AS_DELEGATE must be set on the service requesting - * S4U2Self in order for forwardable tickets to be returned. - */ - else if (!is_referral && - !isflagset(server->attributes, - KRB5_KDB_OK_TO_AUTH_AS_DELEGATE)) - clear(enc_tkt_reply.flags, TKT_FLG_FORWARDABLE); - } - } - if (isflagset(request->kdc_options, KDC_OPT_FORWARDED) || isflagset(request->kdc_options, KDC_OPT_PROXY)) { @@ -440,16 +410,10 @@ process_tgs_req(krb5_kdc_req *request, krb5_data *pkt, enc_tkt_reply.caddrs = request->addresses; reply_encpart.caddrs = request->addresses; } - /* We don't currently handle issuing anonymous tickets based on - * non-anonymous ones, so just ignore the option. */ - if (isflagset(request->kdc_options, KDC_OPT_REQUEST_ANONYMOUS) && - !isflagset(header_enc_tkt->flags, TKT_FLG_ANONYMOUS)) - clear(enc_tkt_reply.flags, TKT_FLG_ANONYMOUS); - - if (isflagset(request->kdc_options, KDC_OPT_POSTDATED)) { - setflag(enc_tkt_reply.flags, TKT_FLG_INVALID); + + if (isflagset(request->kdc_options, KDC_OPT_POSTDATED)) enc_tkt_reply.times.starttime = request->from; - } else + else enc_tkt_reply.times.starttime = kdc_time; if (isflagset(request->kdc_options, KDC_OPT_VALIDATE)) { diff --git a/src/kdc/kdc_util.c b/src/kdc/kdc_util.c index 96c88edc1..f2741090e 100644 --- a/src/kdc/kdc_util.c +++ b/src/kdc/kdc_util.c @@ -697,29 +697,6 @@ validate_as_request(kdc_realm_t *kdc_active_realm, return(KDC_ERR_CANNOT_POSTDATE); } - /* - * A Windows KDC will return KDC_ERR_PREAUTH_REQUIRED instead of - * KDC_ERR_POLICY in the following case: - * - * - KDC_OPT_FORWARDABLE is set in KDCOptions but local - * policy has KRB5_KDB_DISALLOW_FORWARDABLE set for the - * client, and; - * - KRB5_KDB_REQUIRES_PRE_AUTH is set for the client but - * preauthentication data is absent in the request. - * - * Hence, this check most be done after the check for preauth - * data, and is now performed by validate_forwardable() (the - * contents of which were previously below). - */ - - /* Client and server must allow proxiable tickets */ - if (isflagset(request->kdc_options, KDC_OPT_PROXIABLE) && - (isflagset(client.attributes, KRB5_KDB_DISALLOW_PROXIABLE) || - isflagset(server.attributes, KRB5_KDB_DISALLOW_PROXIABLE))) { - *status = "PROXIABLE NOT ALLOWED"; - return(KDC_ERR_POLICY); - } - /* Check to see if client is locked out */ if (isflagset(client.attributes, KRB5_KDB_DISALLOW_ALL_TIX)) { *status = "CLIENT LOCKED OUT"; @@ -752,19 +729,54 @@ validate_as_request(kdc_realm_t *kdc_active_realm, return 0; } -int -validate_forwardable(krb5_kdc_req *request, krb5_db_entry client, - krb5_db_entry server, krb5_timestamp kdc_time, - const char **status) +/* + * Compute ticket flags based on the request, the client and server DB entry + * (which may prohibit forwardable or proxiable tickets), and the header + * ticket. client may be NULL for a TGS request (although it may be set, such + * as for an S4U2Self request). header_enc may be NULL for an AS request. + */ +krb5_flags +get_ticket_flags(krb5_flags reqflags, krb5_db_entry *client, + krb5_db_entry *server, krb5_enc_tkt_part *header_enc) { - *status = NULL; - if (isflagset(request->kdc_options, KDC_OPT_FORWARDABLE) && - (isflagset(client.attributes, KRB5_KDB_DISALLOW_FORWARDABLE) || - isflagset(server.attributes, KRB5_KDB_DISALLOW_FORWARDABLE))) { - *status = "FORWARDABLE NOT ALLOWED"; - return(KDC_ERR_POLICY); - } else - return 0; + krb5_flags flags; + + /* Indicate support for encrypted padata (RFC 6806), and set flags based on + * request options and the header ticket. */ + flags = OPTS2FLAGS(reqflags) | TKT_FLG_ENC_PA_REP; + if (reqflags & KDC_OPT_POSTDATED) + flags |= TKT_FLG_INVALID; + if (header_enc != NULL) + flags |= COPY_TKT_FLAGS(header_enc->flags); + if (header_enc == NULL) + flags |= TKT_FLG_INITIAL; + + /* For TGS requests, indicate if the service is marked ok-as-delegate. */ + if (header_enc != NULL && (server->attributes & KRB5_KDB_OK_AS_DELEGATE)) + flags |= TKT_FLG_OK_AS_DELEGATE; + + /* Unset PROXIABLE if it is disallowed. */ + if (client != NULL && (client->attributes & KRB5_KDB_DISALLOW_PROXIABLE)) + flags &= ~TKT_FLG_PROXIABLE; + if (server->attributes & KRB5_KDB_DISALLOW_PROXIABLE) + flags &= ~TKT_FLG_PROXIABLE; + if (header_enc != NULL && !(header_enc->flags & TKT_FLG_PROXIABLE)) + flags &= ~TKT_FLG_PROXIABLE; + + /* Unset FORWARDABLE if it is disallowed. */ + if (client != NULL && (client->attributes & KRB5_KDB_DISALLOW_FORWARDABLE)) + flags &= ~TKT_FLG_FORWARDABLE; + if (server->attributes & KRB5_KDB_DISALLOW_FORWARDABLE) + flags &= ~TKT_FLG_FORWARDABLE; + if (header_enc != NULL && !(header_enc->flags & TKT_FLG_FORWARDABLE)) + flags &= ~TKT_FLG_FORWARDABLE; + + /* We don't currently handle issuing anonymous tickets based on + * non-anonymous ones. */ + if (header_enc != NULL && !(header_enc->flags & TKT_FLG_ANONYMOUS)) + flags &= ~TKT_FLG_ANONYMOUS; + + return flags; } /* Return KRB5KDC_ERR_POLICY if indicators does not contain the required auth diff --git a/src/kdc/kdc_util.h b/src/kdc/kdc_util.h index 25077cbf5..1314bdd58 100644 --- a/src/kdc/kdc_util.h +++ b/src/kdc/kdc_util.h @@ -85,16 +85,15 @@ validate_as_request (kdc_realm_t *, krb5_kdc_req *, krb5_db_entry, krb5_db_entry, krb5_timestamp, const char **, krb5_pa_data ***); -int -validate_forwardable(krb5_kdc_req *, krb5_db_entry, - krb5_db_entry, krb5_timestamp, - const char **); - int validate_tgs_request (kdc_realm_t *, krb5_kdc_req *, krb5_db_entry, krb5_ticket *, krb5_timestamp, const char **, krb5_pa_data ***); +krb5_flags +get_ticket_flags(krb5_flags reqflags, krb5_db_entry *client, + krb5_db_entry *server, krb5_enc_tkt_part *header_enc); + krb5_error_code check_indicators(krb5_context context, krb5_db_entry *server, krb5_data *const *indicators); diff --git a/src/kdc/tgs_policy.c b/src/kdc/tgs_policy.c index 907fcd330..554345ba5 100644 --- a/src/kdc/tgs_policy.c +++ b/src/kdc/tgs_policy.c @@ -63,9 +63,9 @@ static check_tgs_svc_pol_fn * const svc_pol_fns[] = { }; static const struct tgsflagrule tgsflagrules[] = { - { (KDC_OPT_FORWARDED | KDC_OPT_FORWARDABLE), TKT_FLG_FORWARDABLE, + { KDC_OPT_FORWARDED, TKT_FLG_FORWARDABLE, "TGT NOT FORWARDABLE", KDC_ERR_BADOPTION }, - { (KDC_OPT_PROXY | KDC_OPT_PROXIABLE), TKT_FLG_PROXIABLE, + { KDC_OPT_PROXY, TKT_FLG_PROXIABLE, "TGT NOT PROXIABLE", KDC_ERR_BADOPTION }, { (KDC_OPT_ALLOW_POSTDATE | KDC_OPT_POSTDATED), TKT_FLG_MAY_POSTDATE, "TGT NOT POSTDATABLE", KDC_ERR_BADOPTION }, @@ -98,12 +98,8 @@ check_tgs_opts(krb5_kdc_req *req, krb5_ticket *tkt, const char **status) } static const struct tgsflagrule svcdenyrules[] = { - { KDC_OPT_FORWARDABLE, KRB5_KDB_DISALLOW_FORWARDABLE, - "NON-FORWARDABLE TICKET", KDC_ERR_POLICY }, { KDC_OPT_RENEWABLE, KRB5_KDB_DISALLOW_RENEWABLE, "NON-RENEWABLE TICKET", KDC_ERR_POLICY }, - { KDC_OPT_PROXIABLE, KRB5_KDB_DISALLOW_PROXIABLE, - "NON-PROXIABLE TICKET", KDC_ERR_POLICY }, { KDC_OPT_ALLOW_POSTDATE, KRB5_KDB_DISALLOW_POSTDATED, "NON-POSTDATABLE TICKET", KDC_ERR_CANNOT_POSTDATE }, { KDC_OPT_ENC_TKT_IN_SKEY, KRB5_KDB_DISALLOW_DUP_SKEY, diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in index c96c5d6b7..d2a37c616 100644 --- a/src/tests/Makefile.in +++ b/src/tests/Makefile.in @@ -171,6 +171,7 @@ check-pytests: unlockiter $(RUNPYTEST) $(srcdir)/t_y2038.py $(PYTESTFLAGS) $(RUNPYTEST) $(srcdir)/t_kdcpolicy.py $(PYTESTFLAGS) $(RUNPYTEST) $(srcdir)/t_u2u.py $(PYTESTFLAGS) + $(RUNPYTEST) $(srcdir)/t_kdcoptions.py $(PYTESTFLAGS) clean: $(RM) adata etinfo forward gcred hist hooks hrealm icinterleave icred diff --git a/src/tests/gcred.c b/src/tests/gcred.c index cb0ae6af5..b14e4fc9a 100644 --- a/src/tests/gcred.c +++ b/src/tests/gcred.c @@ -66,20 +66,32 @@ main(int argc, char **argv) krb5_principal client, server; krb5_ccache ccache; krb5_creds in_creds, *creds; + krb5_flags options = 0; char *name; + int c; check(krb5_init_context(&ctx)); - /* Parse arguments. */ - assert(argc == 3); - check(krb5_parse_name(ctx, argv[2], &server)); - if (strcmp(argv[1], "unknown") == 0) + while ((c = getopt(argc, argv, "f")) != -1) { + switch (c) { + case 'f': + options |= KRB5_GC_FORWARDABLE; + break; + default: + abort(); + } + } + argc -= optind; + argv += optind; + assert(argc == 2); + check(krb5_parse_name(ctx, argv[1], &server)); + if (strcmp(argv[0], "unknown") == 0) server->type = KRB5_NT_UNKNOWN; - else if (strcmp(argv[1], "principal") == 0) + else if (strcmp(argv[0], "principal") == 0) server->type = KRB5_NT_PRINCIPAL; - else if (strcmp(argv[1], "srv-inst") == 0) + else if (strcmp(argv[0], "srv-inst") == 0) server->type = KRB5_NT_SRV_INST; - else if (strcmp(argv[1], "srv-hst") == 0) + else if (strcmp(argv[0], "srv-hst") == 0) server->type = KRB5_NT_SRV_HST; else abort(); @@ -89,7 +101,7 @@ main(int argc, char **argv) memset(&in_creds, 0, sizeof(in_creds)); in_creds.client = client; in_creds.server = server; - check(krb5_get_credentials(ctx, 0, ccache, &in_creds, &creds)); + check(krb5_get_credentials(ctx, options, ccache, &in_creds, &creds)); check(krb5_unparse_name(ctx, creds->server, &name)); printf("%s\n", name); diff --git a/src/tests/t_kdcoptions.py b/src/tests/t_kdcoptions.py new file mode 100644 index 000000000..7ec57508c --- /dev/null +++ b/src/tests/t_kdcoptions.py @@ -0,0 +1,100 @@ +from k5test import * +import re + +# KDC option test coverage notes: +# +# FORWARDABLE here +# FORWARDED no test +# PROXIABLE here +# PROXY no test +# ALLOW_POSTDATE no test +# POSTDATED no test +# RENEWABLE t_renew.py +# CNAME_IN_ADDL_TKT gssapi/t_s4u.py +# CANONICALIZE t_kdb.py and various other tests +# REQUEST_ANONYMOUS t_pkinit.py +# DISABLE_TRANSITED_CHECK no test +# RENEWABLE_OK t_renew.py +# ENC_TKT_IN_SKEY t_u2u.py +# RENEW t_renew.py +# VALIDATE no test + +# Run klist -f and return the flags on the ticket for svcprinc. +def get_flags(realm, svcprinc): + grab_flags = False + for line in realm.run([klist, '-f']).splitlines(): + if grab_flags: + return re.findall(r'Flags: ([a-zA-Z]*)', line)[0] + grab_flags = line.endswith(svcprinc) + + +# Get the flags on the ticket for svcprinc, and check for an expected +# element and an expected-absent element, either of which can be None. +def check_flags(realm, svcprinc, expected_flag, expected_noflag): + flags = get_flags(realm, svcprinc) + if expected_flag is not None and not expected_flag in flags: + fail('expected flag ' + expected_flag) + if expected_noflag is not None and expected_noflag in flags: + fail('did not expect flag ' + expected_noflag) + + +# Run kinit with the given flags, and check the flags on the resulting +# TGT. +def kinit_check_flags(realm, flags, expected_flag, expected_noflag): + realm.kinit(realm.user_princ, password('user'), flags) + check_flags(realm, realm.krbtgt_princ, expected_flag, expected_noflag) + + +# Run kinit with kflags. Then get credentials for the host principal +# with gflags, and check the flags on the resulting ticket. +def gcred_check_flags(realm, kflags, gflags, expected_flag, expected_noflag): + realm.kinit(realm.user_princ, password('user'), kflags) + realm.run(['./gcred'] + gflags + ['unknown', realm.host_princ]) + check_flags(realm, realm.host_princ, expected_flag, expected_noflag) + + +realm = K5Realm() + +mark('proxiable (AS)') +kinit_check_flags(realm, [], None, 'P') +kinit_check_flags(realm, ['-p'], 'P', None) +realm.run([kadminl, 'modprinc', '-allow_proxiable', realm.user_princ]) +kinit_check_flags(realm, ['-p'], None, 'P') +realm.run([kadminl, 'modprinc', '+allow_proxiable', realm.user_princ]) +realm.run([kadminl, 'modprinc', '-allow_proxiable', realm.krbtgt_princ]) +kinit_check_flags(realm, ['-p'], None, 'P') +realm.run([kadminl, 'modprinc', '+allow_proxiable', realm.krbtgt_princ]) + +mark('proxiable (TGS)') +gcred_check_flags(realm, [], [], None, 'P') +gcred_check_flags(realm, ['-p'], [], 'P', None) + +# Not tested: PROXIABLE option set with a non-proxiable TGT (because +# there is no krb5_get_credentials() flag to request this; would +# expect a non-proxiable ticket). + +# Not tested: proxiable TGT but PROXIABLE flag not set (because we +# internally set the PROXIABLE option when using a proxiable TGT; +# would expect a non-proxiable ticket). + +mark('forwardable (AS)') +kinit_check_flags(realm, [], None, 'F') +kinit_check_flags(realm, ['-f'], 'F', None) +realm.run([kadminl, 'modprinc', '-allow_forwardable', realm.user_princ]) +kinit_check_flags(realm, ['-f'], None, 'F') +realm.run([kadminl, 'modprinc', '+allow_forwardable', realm.user_princ]) +realm.run([kadminl, 'modprinc', '-allow_forwardable', realm.krbtgt_princ]) +kinit_check_flags(realm, ['-f'], None, 'F') +realm.run([kadminl, 'modprinc', '+allow_forwardable', realm.krbtgt_princ]) + +mark('forwardable (TGS)') +realm.kinit(realm.user_princ, password('user')) +gcred_check_flags(realm, [], [], None, 'F') +gcred_check_flags(realm, [], ['-f'], None, 'F') +gcred_check_flags(realm, ['-f'], [], 'F', None) + +# Not tested: forwardable TGT but FORWARDABLE flag not set (because we +# internally set the FORWARDABLE option when using a forwardable TGT; +# would expect a non-proxiable ticket). + +success('KDC option tests')