From: Isaac Boukris Date: Thu, 20 Jun 2019 05:00:06 +0000 (+0000) Subject: Add RBCD client support X-Git-Tag: krb5-1.18-beta1~69 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c426ef2ca2ba45dbf96f5380cf7d153ec0679424;p=thirdparty%2Fkrb5.git Add RBCD client support When making S4U2Proxy requests, include a PA-PAC-OPTIONS pa-data element advertising resource-based constrained delegation support. If the KDC returns a referral TGT for the initial request and advertises RBCD support, chase referrals to the target realm with both a regular and proxy TGT, and make an S4U2Proxy request to the target realm with the proxy TGT as evidence ticket. Because cross-realm S4U2Proxy requests must use referrals, an explicit foreign realm in the server name cannot be honored. In the GSSAPI krb5 mech, if a host-based server name is used, omit the realm (if one was obtained from [domain_realm] or similar) when calling krb5_get_credentials() for constrained delegation. [ghudson@mit.edu: rewrote commit message; made style changes] ticket: 8479 --- diff --git a/src/include/k5-int.h b/src/include/k5-int.h index 1d991e79ec..77d7abce19 100644 --- a/src/include/k5-int.h +++ b/src/include/k5-int.h @@ -559,6 +559,13 @@ typedef struct _krb5_secure_cookie { krb5_pa_data **data; } krb5_secure_cookie; +typedef struct _krb5_pa_pac_options { + krb5_flags options; +} krb5_pa_pac_options; + +/* In PAC options, indicates Resource-Based Constrained Delegation support. */ +#define KRB5_PA_PAC_OPTIONS_RBCD 0x10000000 + #include #include @@ -1536,6 +1543,9 @@ encode_utf8_strings(krb5_data *const *ut8fstrings, krb5_data **); krb5_error_code encode_krb5_secure_cookie(const krb5_secure_cookie *, krb5_data **); +krb5_error_code +encode_krb5_pa_pac_options(const krb5_pa_pac_options *, krb5_data **); + /************************************************************************* * End of prototypes for krb5_encode.c *************************************************************************/ @@ -1718,6 +1728,9 @@ decode_utf8_strings(const krb5_data *, krb5_data ***); krb5_error_code decode_krb5_secure_cookie(const krb5_data *, krb5_secure_cookie **); +krb5_error_code +decode_krb5_pa_pac_options(const krb5_data *, krb5_pa_pac_options **); + struct _krb5_key_data; /* kdb.h */ struct ldap_seqof_key_data { diff --git a/src/include/krb5/krb5.hin b/src/include/krb5/krb5.hin index d65cf8f4e4..eed38fd7ea 100644 --- a/src/include/krb5/krb5.hin +++ b/src/include/krb5/krb5.hin @@ -1879,6 +1879,7 @@ krb5_verify_checksum(krb5_context context, krb5_cksumtype ctype, #define KRB5_ENCPADATA_REQ_ENC_PA_REP 149 /**< RFC 6806 */ #define KRB5_PADATA_AS_FRESHNESS 150 /**< RFC 8070 */ #define KRB5_PADATA_SPAKE 151 +#define KRB5_PADATA_PAC_OPTIONS 167 /**< MS-KILE and MS-SFU */ #define KRB5_SAM_USE_SAD_AS_KEY 0x80000000 #define KRB5_SAM_SEND_ENCRYPTED_SAD 0x40000000 diff --git a/src/lib/gssapi/krb5/init_sec_context.c b/src/lib/gssapi/krb5/init_sec_context.c index 949debcfdc..6ce3ad1c0c 100644 --- a/src/lib/gssapi/krb5/init_sec_context.c +++ b/src/lib/gssapi/krb5/init_sec_context.c @@ -129,6 +129,7 @@ static krb5_error_code get_credentials(context, cred, server, now, krb5_error_code code; krb5_creds in_creds, evidence_creds, mcreds, *result_creds = NULL; krb5_flags flags = 0; + krb5_principal_data server_data; *out_creds = NULL; @@ -139,8 +140,14 @@ static krb5_error_code get_credentials(context, cred, server, now, assert(cred->name != NULL); + /* Remove assumed realm from host-based S4U2Proxy requests as they must + * start in the client realm. */ + server_data = *server->princ; + if (cred->impersonator != NULL && server_data.type == KRB5_NT_SRV_HST) + server_data.realm = empty_data(); + in_creds.server = &server_data; + in_creds.client = cred->name->princ; - in_creds.server = server->princ; in_creds.times.endtime = endtime; in_creds.authdata = NULL; in_creds.keyblock.enctype = 0; diff --git a/src/lib/krb5/asn.1/asn1_k_encode.c b/src/lib/krb5/asn.1/asn1_k_encode.c index a026ab390f..39fa8e3bbb 100644 --- a/src/lib/krb5/asn.1/asn1_k_encode.c +++ b/src/lib/krb5/asn.1/asn1_k_encode.c @@ -1732,6 +1732,28 @@ DEFSEQTYPE(secure_cookie, krb5_secure_cookie, secure_cookie_fields); MAKE_ENCODER(encode_krb5_secure_cookie, secure_cookie); MAKE_DECODER(decode_krb5_secure_cookie, secure_cookie); +/* + * -- based on MS-KILE and MS-SFU + * PAC-OPTIONS-FLAGS ::= BIT STRING { + * claims(0), + * branch-aware(1), + * forward-to-full-dc(2), + * resource-based-constrained-delegation(3) + * } + * + * PA-PAC-OPTIONS ::= SEQUENCE { + * flags [0] PAC-OPTIONS-FLAGS + * } + */ +DEFFIELD(pa_pac_options_0, krb5_pa_pac_options, options, 0, krb5_flags); +static const struct atype_info *pa_pac_options_fields[] = { + &k5_atype_pa_pac_options_0 +}; +DEFSEQTYPE(pa_pac_options, krb5_pa_pac_options, pa_pac_options_fields); +MAKE_ENCODER(encode_krb5_pa_pac_options, pa_pac_options); +MAKE_DECODER(decode_krb5_pa_pac_options, pa_pac_options); + + DEFFIELD(spake_factor_0, krb5_spake_factor, type, 0, int32); DEFFIELD(spake_factor_1, krb5_spake_factor, data, 1, opt_ostring_data_ptr); static const struct atype_info *spake_factor_fields[] = { diff --git a/src/lib/krb5/krb/gc_via_tkt.c b/src/lib/krb5/krb/gc_via_tkt.c index 3d0859b412..5ac8a52f59 100644 --- a/src/lib/krb5/krb/gc_via_tkt.c +++ b/src/lib/krb5/krb/gc_via_tkt.c @@ -257,9 +257,12 @@ krb5int_process_tgs_reply(krb5_context context, /* Final hop, check whether KDC supports S4U2Self */ if (krb5_principal_compare(context, dec_rep->client, in_cred->server)) retval = KRB5KDC_ERR_PADATA_TYPE_NOSUPP; - } else if ((kdcoptions & KDC_OPT_CNAME_IN_ADDL_TKT) == 0) { - /* XXX for constrained delegation this check must be performed by caller - * as we don't have access to the key to decrypt the evidence ticket. + } else if ((kdcoptions & KDC_OPT_CNAME_IN_ADDL_TKT) == 0 || + IS_TGS_PRINC(dec_rep->ticket->server)) { + /* + * For constrained delegation this check must be performed by caller, + * as we can't decrypt the evidence ticket. However, if it is a + * referral the client should match the TGT client like normal. */ if (!krb5_principal_compare(context, dec_rep->client, tkt->client)) retval = KRB5_KDCREP_MODIFIED; diff --git a/src/lib/krb5/krb/s4u_creds.c b/src/lib/krb5/krb/s4u_creds.c index 26c15fedc2..71e2a08e62 100644 --- a/src/lib/krb5/krb/s4u_creds.c +++ b/src/lib/krb5/krb/s4u_creds.c @@ -725,6 +725,50 @@ cleanup: return code; } +static krb5_error_code +check_rbcd_support(krb5_context context, krb5_pa_data **padata) +{ + krb5_error_code code; + krb5_pa_data *pa; + krb5_pa_pac_options *pac_options; + krb5_data der_pac_options; + + pa = krb5int_find_pa_data(context, padata, KRB5_PADATA_PAC_OPTIONS); + if (pa == NULL) + return KRB5KDC_ERR_PADATA_TYPE_NOSUPP; + + der_pac_options = make_data(pa->contents, pa->length); + code = decode_krb5_pa_pac_options(&der_pac_options, &pac_options); + if (code) + return code; + + if (!(pac_options->options & KRB5_PA_PAC_OPTIONS_RBCD)) + code = KRB5KDC_ERR_PADATA_TYPE_NOSUPP; + + free(pac_options); + return code; +} + +static krb5_error_code +add_rbcd_padata(krb5_context context, krb5_pa_data ***in_padata) +{ + krb5_error_code code; + krb5_pa_pac_options pac_options; + krb5_data *der_pac_options = NULL; + + memset(&pac_options, 0, sizeof(pac_options)); + pac_options.options |= KRB5_PA_PAC_OPTIONS_RBCD; + + code = encode_krb5_pa_pac_options(&pac_options, &der_pac_options); + if (code) + return code; + + code = k5_add_pa_data_from_data(in_padata, KRB5_PADATA_PAC_OPTIONS, + der_pac_options); + krb5_free_data(context, der_pac_options); + return code; +} + /* Set *tgt_out to a local TGT for the client realm retrieved from ccache. */ static krb5_error_code get_client_tgt(krb5_context context, krb5_flags options, krb5_ccache ccache, @@ -748,8 +792,11 @@ get_client_tgt(krb5_context context, krb5_flags options, krb5_ccache ccache, return code; } -/* Copy req_server to *out_server. If req_server has the referral realm, set - * the realm of *out_server to realm. */ +/* + * Copy req_server to *out_server. If req_server has the referral realm, set + * the realm of *out_server to realm. Otherwise the S4U2Proxy request will + * fail unless the specified realm is the same as the TGT (or an alias to it). + */ static krb5_error_code normalize_server_princ(krb5_context context, const krb5_data *realm, krb5_principal req_server, krb5_principal *out_server) @@ -776,14 +823,162 @@ normalize_server_princ(krb5_context context, const krb5_data *realm, return 0; } +/* Return an error if server is present in referral_list. */ +static krb5_error_code +check_referral_path(krb5_context context, krb5_principal server, + krb5_creds **referral_list, int referral_count) +{ + int i; + + for (i = 0; i < referral_count; i++) { + if (krb5_principal_compare(context, server, referral_list[i]->server)) + return KRB5_KDC_UNREACH; + } + return 0; +} + +/* + * Make TGS requests for in_creds using *tgt_inout, following referrals until + * the requested service ticket is issued. Replace *tgt_inout with the final + * TGT used, or free it and set it to NULL on error. Place the final creds + * received in *creds_out. + */ +static krb5_error_code +chase_referrals(krb5_context context, krb5_creds *in_creds, krb5_flags kdcopt, + krb5_creds **tgt_inout, krb5_creds **creds_out) +{ + krb5_error_code code; + krb5_creds *referral_tgts[KRB5_REFERRAL_MAXHOPS] = { NULL }; + krb5_creds mcreds, *tgt, *tkt = NULL; + krb5_principal_data server; + int referral_count = 0, i; + + tgt = *tgt_inout; + *tgt_inout = NULL; + *creds_out = NULL; + + mcreds = *in_creds; + server = *in_creds->server; + mcreds.server = &server; + + for (referral_count = 0; referral_count < KRB5_REFERRAL_MAXHOPS; + referral_count++) { + code = krb5_get_cred_via_tkt(context, tgt, kdcopt, tgt->addresses, + &mcreds, &tkt); + if (code) + goto cleanup; + + if (krb5_principal_compare_any_realm(context, mcreds.server, + tkt->server)) { + *creds_out = tkt; + *tgt_inout = tgt; + tkt = tgt = NULL; + goto cleanup; + } + + if (!IS_TGS_PRINC(tkt->server)) { + code = KRB5KRB_AP_WRONG_PRINC; + goto cleanup; + } + + if (data_eq(tgt->server->data[1], tkt->server->data[1])) { + code = KRB5_ERR_HOST_REALM_UNKNOWN; + goto cleanup; + } + + code = check_referral_path(context, tkt->server, referral_tgts, + referral_count); + if (code) + goto cleanup; + + referral_tgts[referral_count] = tgt; + tgt = tkt; + tkt = NULL; + server.realm = tgt->server->data[1]; + } + + /* Max hop count exceeded. */ + code = KRB5_KDCREP_MODIFIED; + +cleanup: + for (i = 0; i < KRB5_REFERRAL_MAXHOPS; i++) + krb5_free_creds(context, referral_tgts[i]); + krb5_free_creds(context, tkt); + krb5_free_creds(context, tgt); + return code; +} + +/* + * Make non-S4U2Proxy TGS requests for in_creds using *tgt_inout, following + * referrals until the requested service ticket is returned. Discard the + * service ticket, but replace *tgt_inout with the final referral TGT. + */ +static krb5_error_code +get_tgt_to_target_realm(krb5_context context, krb5_creds *in_creds, + krb5_flags req_kdcopt, krb5_creds **tgt_inout) +{ + krb5_error_code code; + krb5_flags kdcopt; + krb5_creds mcreds, *out; + + mcreds = *in_creds; + mcreds.second_ticket = empty_data(); + kdcopt = FLAGS2OPTS((*tgt_inout)->ticket_flags) | req_kdcopt; + + code = chase_referrals(context, &mcreds, kdcopt, tgt_inout, &out); + krb5_free_creds(context, out); + + return code; +} + +/* + * Make TGS requests for a cross-TGT to realm using *tgt_inout, following + * alternate TGS replies until the requested TGT is issued. Replace *tgt_inout + * with the result. Do nothing if *tgt_inout is already a cross-TGT for realm. + */ +static krb5_error_code +get_target_realm_proxy_tgt(krb5_context context, const krb5_data *realm, + krb5_flags req_kdcopt, krb5_creds **tgt_inout) +{ + krb5_error_code code; + krb5_creds mcreds, *out; + krb5_principal tgs; + krb5_flags flags; + + if (data_eq(*realm, (*tgt_inout)->server->data[1])) + return 0; + + code = krb5int_tgtname(context, realm, &(*tgt_inout)->server->data[1], + &tgs); + if (code) + return code; + + memset(&mcreds, 0, sizeof(mcreds)); + mcreds.client = (*tgt_inout)->client; + mcreds.server = tgs; + flags = req_kdcopt | FLAGS2OPTS((*tgt_inout)->ticket_flags); + + code = chase_referrals(context, &mcreds, flags, tgt_inout, &out); + krb5_free_principal(context, tgs); + if (code) + return code; + + krb5_free_creds(context, *tgt_inout); + *tgt_inout = out; + + return 0; +} + krb5_error_code k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, krb5_ccache ccache, krb5_creds *in_creds, krb5_creds **out_creds) { krb5_error_code code; - krb5_flags kdcopt, flags; + krb5_flags flags, req_kdcopt = 0; krb5_principal server = NULL; + krb5_pa_data **in_padata = NULL; + krb5_pa_data **enc_padata = NULL; krb5_creds mcreds, *tgt = NULL, *tkt = NULL; *out_creds = NULL; @@ -803,27 +998,110 @@ k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, if (code) goto cleanup; - kdcopt = KDC_OPT_CNAME_IN_ADDL_TKT; + code = add_rbcd_padata(context, &in_padata); + if (code) + goto cleanup; + if (options & KRB5_GC_CANONICALIZE) - kdcopt |= KDC_OPT_CANONICALIZE; + req_kdcopt |= KDC_OPT_CANONICALIZE; if (options & KRB5_GC_FORWARDABLE) - kdcopt |= KDC_OPT_FORWARDABLE; + req_kdcopt |= KDC_OPT_FORWARDABLE; if (options & KRB5_GC_NO_TRANSIT_CHECK) - kdcopt |= KDC_OPT_DISABLE_TRANSITED_CHECK; + req_kdcopt |= KDC_OPT_DISABLE_TRANSITED_CHECK; mcreds = *in_creds; mcreds.server = server; - flags = kdcopt | FLAGS2OPTS(tgt->ticket_flags); + flags = req_kdcopt | FLAGS2OPTS(tgt->ticket_flags) | + KDC_OPT_CNAME_IN_ADDL_TKT | KDC_OPT_CANONICALIZE; code = krb5_get_cred_via_tkt_ext(context, tgt, flags, tgt->addresses, - NULL, &mcreds, NULL, NULL, NULL, NULL, - &tkt, NULL); + in_padata, &mcreds, NULL, NULL, NULL, + &enc_padata, &tkt, NULL); + + /* + * If the server principal name included a foreign realm which wasn't an + * alias for the local realm, the KDC won't be able to decrypt the TGT. + * Windows KDCs will return a BAD_INTEGRITY error in this case, while MIT + * KDCs will return S_PRINCIPAL_UNKNOWN. We cannot distinguish the latter + * error from the service principal actually being unknown in the realm, + * but set a comprehensible error message for the BAD_INTEGRITY error. + */ + if (code == KRB5KRB_AP_ERR_BAD_INTEGRITY && + !krb5_realm_compare(context, in_creds->client, server)) { + k5_setmsg(context, code, _("Realm specified but S4U2Proxy must use " + "referral realm")); + } + if (code) goto cleanup; - if (!krb5_principal_compare(context, server, tkt->server)) { - code = KRB5KRB_AP_WRONG_PRINC; - goto cleanup; + if (!krb5_principal_compare_any_realm(context, server, tkt->server)) { + /* Make sure we got a referral. */ + if (!IS_TGS_PRINC(tkt->server)) { + code = KRB5KRB_AP_WRONG_PRINC; + goto cleanup; + } + + /* + * Make sure the KDC supports S4U and resource-based constrained + * delegation; otherwise we might have gotten a regular TGT referral + * rather than a proxy TGT referral. + */ + code = check_rbcd_support(context, enc_padata); + if (code) + goto cleanup; + + krb5_free_pa_data(context, enc_padata); + enc_padata = NULL; + + /* + * Replace tgt with a regular (not proxy) TGT to the target realm, by + * making a normal TGS request and following referrals. Per [MS-SFU] + * 3.1.5.2.2, we need this TGT to make the final TGS request. + */ + code = get_tgt_to_target_realm(context, &mcreds, req_kdcopt, &tgt); + if (code) + goto cleanup; + + /* + * Replace tkt with a proxy TGT (meaning, one obtained using the + * referral TGT we got from the first S4U2Proxy request) to the target + * realm, if it isn't already one. + */ + code = get_target_realm_proxy_tgt(context, &tgt->server->data[1], + req_kdcopt, &tkt); + if (code) + goto cleanup; + + krb5_free_data_contents(context, &server->realm); + code = krb5int_copy_data_contents(context, &tgt->server->data[1], + &server->realm); + if (code) + goto cleanup; + + /* Make an S4U2Proxy request to the target realm using the regular TGT, + * with the proxy TGT as the evidence ticket. */ + mcreds.second_ticket = tkt->ticket; + tkt->ticket = empty_data(); + krb5_free_creds(context, tkt); + tkt = NULL; + flags = req_kdcopt | FLAGS2OPTS(tgt->ticket_flags) | + KDC_OPT_CNAME_IN_ADDL_TKT | KDC_OPT_CANONICALIZE; + code = krb5_get_cred_via_tkt_ext(context, tgt, flags, tgt->addresses, + in_padata, &mcreds, NULL, NULL, NULL, + &enc_padata, &tkt, NULL); + free(mcreds.second_ticket.data); + if (code) + goto cleanup; + + code = check_rbcd_support(context, enc_padata); + if (code) + goto cleanup; + + if (!krb5_principal_compare(context, server, tkt->server)) { + code = KRB5KRB_AP_WRONG_PRINC; + goto cleanup; + } } if (!krb5_principal_compare(context, in_creds->server, tkt->server)) { @@ -844,6 +1122,8 @@ cleanup: krb5_free_creds(context, tgt); krb5_free_creds(context, tkt); krb5_free_principal(context, server); + krb5_free_pa_data(context, in_padata); + krb5_free_pa_data(context, enc_padata); return code; } diff --git a/src/lib/krb5/libkrb5.exports b/src/lib/krb5/libkrb5.exports index 1d124a09fb..f036b1a611 100644 --- a/src/lib/krb5/libkrb5.exports +++ b/src/lib/krb5/libkrb5.exports @@ -34,6 +34,7 @@ decode_krb5_pa_fx_fast_request decode_krb5_pa_otp_challenge decode_krb5_pa_otp_req decode_krb5_pa_otp_enc_req +decode_krb5_pa_pac_options decode_krb5_pa_pac_req decode_krb5_pa_s4u_x509_user decode_krb5_pa_spake @@ -86,6 +87,7 @@ encode_krb5_pa_fx_fast_reply encode_krb5_pa_otp_challenge encode_krb5_pa_otp_req encode_krb5_pa_otp_enc_req +encode_krb5_pa_pac_options encode_krb5_pa_s4u_x509_user encode_krb5_pa_spake encode_krb5_padata_sequence