From: Greg Hudson Date: Sun, 4 Apr 2010 17:17:17 +0000 (+0000) Subject: Rewrite gc_frm_kdc_step.c to handle the full functionality of X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6039ccb61522061c3ee8f5be16a1aefe96c3fadb;p=thirdparty%2Fkrb5.git Rewrite gc_frm_kdc_step.c to handle the full functionality of gc_frm_krb.c. Not tested yet. git-svn-id: svn://anonsvn.mit.edu/krb5/branches/iakerb@23854 dc483132-0cff-0310-8789-dd5450dbe970 --- diff --git a/src/lib/krb5/krb/gc_frm_kdc_step.c b/src/lib/krb5/krb/gc_frm_kdc_step.c index 42e1828814..882707fe97 100644 --- a/src/lib/krb5/krb/gc_frm_kdc_step.c +++ b/src/lib/krb5/krb/gc_frm_kdc_step.c @@ -36,415 +36,963 @@ #include #include "int-proto.h" -#define KRB5_TKT_CREDS_STEP_FLAG_COMPLETE 0x1 -#define KRB5_TKT_CREDS_STEP_FLAG_CTX_KTYPES 0x2 +/* + * krb5_tkt_creds_step() is implemented using a tail call style. Every + * begin_*, step_*, or *_request function is responsible for returning an + * error, generating the next request, or delegating to another function using + * a tail call. + * + * The process is divided up into states which govern how the next input token + * should be interpreted. Each state has a "begin_" function to set up + * the context fields related to the state, a "step_" function to + * process a reply and update the related context fields, and possibly a + * "_request" function (invoked by the begin_ and step_ functions) to + * generate the next request. If it's time to advance to another state, any of + * the three functions can make a tail call to begin_ to do so. + * + * For the most part the state is always increasing, but we may go from + * REFERRALS to GET_TGT in order to get a TGT for the fallback realm. The + * getting_tgt_for field in the context keeps track of what state we will go to + * after successfully obtaining a foreign TGT, and the end_get_tgt() function + * advances to the proper next state. + */ + +enum state { + STATE_BEGIN, /* Initial step (no input token) */ + STATE_GET_TGT, /* Getting TGT for service realm */ + STATE_GET_TGT_OFFPATH, /* Getting TGT via off-path referrals */ + STATE_REFERRALS, /* Retrieving service ticket or referral */ + STATE_NON_REFERRAL, /* Non-referral service ticket request */ + STATE_COMPLETE /* Creds ready for retrieval */ +}; struct _krb5_tkt_creds_context { - krb5_ccache ccache; - krb5_creds in_cred; - krb5_principal client; - krb5_principal server; - krb5_principal req_server; - int req_kdcopt; - - unsigned int flags; - krb5_creds cc_tgt; - krb5_creds *tgtptr; - unsigned int referral_count; - krb5_creds *referral_tgts[KRB5_REFERRAL_MAXHOPS]; - krb5_boolean default_use_conf_ktypes; - krb5_timestamp timestamp; - krb5_int32 nonce; - int kdcopt; - krb5_keyblock *subkey; - krb5_data encoded_previous_request; - - krb5_creds *out_cred; + enum state state; /* What we should do with the next reply */ + enum state getting_tgt_for; /* STATE_REFERRALS or STATE_NON_REFERRAL */ + + /* The following fields are set up at initialization time. */ + krb5_creds *in_creds; /* Creds requested by caller */ + krb5_principal client; /* Caller-requested client principal (alias) */ + krb5_principal server; /* Server principal (alias) */ + krb5_principal req_server; /* Caller-requested server principal */ + krb5_ccache ccache; /* Caller-provided ccache (alias) */ + int req_kdcopt; /* Caller-requested KDC options */ + krb5_authdata **authdata; /* Caller-requested authdata */ + + /* The following fields are used in multiple steps. */ + krb5_creds *cur_tgt; /* TGT to be used for next query */ + krb5_data *realms_seen; /* For loop detection */ + + /* The following fields track state between request and reply. */ + krb5_timestamp timestamp; /* Timestamp of request */ + krb5_int32 nonce; /* Nonce of request */ + int kdcopt; /* KDC options of request */ + krb5_keyblock *subkey; /* subkey of request */ + krb5_data previous_request; /* Encoded request (for TCP retransmission) */ + + /* The following fields are used when acquiring foreign TGTs. */ + krb5_data *realm_path; /* Path from client to server realm */ + const krb5_data *last_realm;/* Last realm in realm_path */ + const krb5_data *cur_realm; /* Position of cur_tgt in realm_path */ + const krb5_data *next_realm;/* Current target realm in realm_path */ + unsigned int offpath_count; /* Offpath requests made */ + + /* The following fields are used during the referrals loop. */ + unsigned int referral_count;/* Referral requests made */ + + /* The following fields are used within a _step call to avoid + * passing them as parameters everywhere. */ + krb5_creds *reply_creds; /* Creds from TGS reply */ + krb5_error_code reply_code; /* Error status from TGS reply */ + krb5_data *caller_out; /* Caller's out parameter */ + krb5_data *caller_realm; /* Caller's realm parameter */ + unsigned int *caller_flags; /* Caller's flags parameter */ }; /* Convert ticket flags to necessary KDC options */ #define FLAGS2OPTS(flags) (flags & KDC_TKT_COMMON_MASK) static krb5_error_code -tkt_make_tgs_request(krb5_context context, - krb5_tkt_creds_context ctx, - krb5_creds *tgt, - krb5_creds *in_cred, - krb5_data *req) -{ - krb5_error_code code; - - /* These flags are always included */ - ctx->kdcopt |= FLAGS2OPTS(tgt->ticket_flags); - - if ((ctx->kdcopt & KDC_OPT_ENC_TKT_IN_SKEY) == 0) - in_cred->is_skey = FALSE; - - if (!krb5_c_valid_enctype(tgt->keyblock.enctype)) - return KRB5_PROG_ETYPE_NOSUPP; - - code = krb5int_make_tgs_request(context, tgt, ctx->kdcopt, - tgt->addresses, NULL, - in_cred, NULL, NULL, req, - &ctx->timestamp, &ctx->nonce, &ctx->subkey); - return code; -} +begin_get_tgt(krb5_context context, krb5_tkt_creds_context ctx); +/* + * Fill in the caller out, realm, and flags output variables. out is filled in + * with ctx->previous_request, which the caller should set, and realm is filled + * in with the realm of ctx->cur_tgt. + */ static krb5_error_code -tkt_process_tgs_reply(krb5_context context, - krb5_tkt_creds_context ctx, - krb5_data *rep, - krb5_creds *tgt, - krb5_creds *in_cred, - krb5_creds **out_cred) +set_caller_request(krb5_context context, krb5_tkt_creds_context ctx) { krb5_error_code code; + const krb5_data *req = &ctx->previous_request; + const krb5_data *realm = &ctx->cur_tgt->server->data[1]; + krb5_data out_copy = empty_data(), realm_copy = empty_data(); + + code = krb5int_copy_data_contents(context, req, &out_copy); + if (code != 0) + goto cleanup; + code = krb5int_copy_data_contents(context, realm, &realm_copy); + if (code != 0) + goto cleanup; - code = krb5int_process_tgs_reply(context, - rep, - tgt, - ctx->kdcopt, - tgt->addresses, - NULL, - in_cred, - ctx->timestamp, - ctx->nonce, - ctx->subkey, - NULL, - NULL, - out_cred); + *ctx->caller_out = out_copy; + *ctx->caller_realm = realm_copy; + *ctx->caller_flags = 1; + return 0; +cleanup: + krb5_free_data_contents(context, &out_copy); + krb5_free_data_contents(context, &realm_copy); return code; } /* - * Asynchronous API + * Point *TGT at an allocated credentials structure containing a TGT for realm + * retrieved from ctx->ccache. If we are retrieving a foreign TGT, accept any + * issuing realm (i.e. match only the service principal name). If the TGT is + * not found in the cache, return successfully but set *tgt to NULL. */ -krb5_error_code KRB5_CALLCONV -krb5_tkt_creds_init(krb5_context context, - krb5_ccache ccache, - krb5_creds *creds, - int kdcopt, - krb5_tkt_creds_context *pctx) +static krb5_error_code +get_cached_tgt(krb5_context context, krb5_tkt_creds_context ctx, + const krb5_data *realm, krb5_creds **tgt) { + krb5_creds mcreds, *creds = NULL; krb5_error_code code; - krb5_tkt_creds_context ctx = NULL; - krb5_creds tgtq; - krb5_flags flags = KRB5_TC_MATCH_SRV_NAMEONLY | KRB5_TC_SUPPORTED_KTYPES; + krb5_principal tgtname = NULL; + krb5_flags flags; - memset(&tgtq, 0, sizeof(tgtq)); + *tgt = NULL; - ctx = k5alloc(sizeof(*ctx), &code); + /* Construct the principal krbtgt/@. The realm + * won't matter unless we're getting the local TGT. */ + code = krb5int_tgtname(context, realm, &ctx->client->realm, &tgtname); if (code != 0) goto cleanup; - code = krb5int_copy_creds_contents(context, creds, &ctx->in_cred); - if (code != 0) + /* Match the TGT realm only if we're getting the local TGT. */ + flags = KRB5_TC_SUPPORTED_KTYPES; + if (!data_eq(*realm, ctx->client->realm)) + flags |= KRB5_TC_MATCH_SRV_NAMEONLY; + + /* Allocate a structure for the resulting creds. */ + creds = k5alloc(sizeof(*creds), &code); + if (creds == NULL) goto cleanup; - ctx->ccache = ccache; /* XXX */ + /* Construct a matching cred for the ccache query. */ + memset(&mcreds, 0, sizeof(mcreds)); + mcreds.client = ctx->client; + mcreds.server = tgtname; + + /* Fetch the TGT credential, handling not-found errors. */ + context->use_conf_ktypes = TRUE; + code = krb5_cc_retrieve_cred(context, ctx->ccache, flags, &mcreds, + creds); + context->use_conf_ktypes = FALSE; + if (code != 0 && code != KRB5_CC_NOTFOUND && code != KRB5_CC_NOT_KTYPE) + goto cleanup; + if (code == 0) { + *tgt = creds; + creds = NULL; + } + code = 0; - ctx->req_kdcopt = kdcopt; - ctx->default_use_conf_ktypes = context->use_conf_ktypes; - ctx->client = ctx->in_cred.client; - ctx->server = ctx->in_cred.server; +cleanup: + krb5_free_principal(context, tgtname); + free(creds); + return code; +} - code = krb5_copy_principal(context, ctx->server, &ctx->req_server); - if (code != 0) - goto cleanup; +/* + * Set up the request given by in_cred, using ctx->cur_tgt. KDC options for + * the requests are determined by ctx->cur_tgt->ticket_flags and + * extra_options. + */ +static krb5_error_code +make_request(krb5_context context, krb5_tkt_creds_context ctx, + krb5_creds *in_cred, int extra_options) +{ + krb5_error_code code; + krb5_data request = empty_data(); - code = krb5int_tgt_mcred(context, ctx->client, ctx->client, - ctx->client, &tgtq); - if (code != 0) - goto cleanup; + ctx->kdcopt = extra_options | FLAGS2OPTS(ctx->cur_tgt->ticket_flags); - code = krb5_cc_retrieve_cred(context, ctx->ccache, flags, - &tgtq, &ctx->cc_tgt); + /* XXX This check belongs in gc_via_tgt.c or nowhere. */ + if (!krb5_c_valid_enctype(ctx->cur_tgt->keyblock.enctype)) + return KRB5_PROG_ETYPE_NOSUPP; + + code = krb5int_make_tgs_request(context, ctx->cur_tgt, ctx->kdcopt, + ctx->cur_tgt->addresses, + NULL, in_cred, NULL, NULL, &request, + &ctx->timestamp, &ctx->nonce, + &ctx->subkey); if (code != 0) - goto cleanup; + return code; - ctx->tgtptr = &ctx->cc_tgt; + krb5_free_data_contents(context, &ctx->previous_request); + ctx->previous_request = request; + return set_caller_request(context, ctx); +} - *pctx = ctx; +/* Set up a request for a TGT for realm, using ctx->cur_tgt. */ +static krb5_error_code +make_request_for_tgt(krb5_context context, krb5_tkt_creds_context ctx, + const krb5_data *realm) +{ + krb5_error_code code; + krb5_creds mcreds; + krb5_principal tgtname = NULL; -cleanup: + /* Construct the principal krbtgt/@. */ + code = krb5int_tgtname(context, realm, &ctx->cur_tgt->server->realm, + &tgtname); if (code != 0) - krb5_tkt_creds_free(context, ctx); - krb5_free_cred_contents(context, &tgtq); + return code; + /* Make a request for the specified TGT with no extra flags. */ + memset(&mcreds, 0, sizeof(mcreds)); + mcreds.client = ctx->client; + mcreds.server = tgtname; + code = make_request(context, ctx, &mcreds, 0); + krb5_free_principal(context, tgtname); return code; } -krb5_error_code KRB5_CALLCONV -krb5_tkt_creds_get_creds(krb5_context context, - krb5_tkt_creds_context ctx, - krb5_creds *creds) +/* Set up a request for the desired service principal, using ctx->cur_tgt. + * Optionally allow the answer to be a referral. */ +static krb5_error_code +make_request_for_service(krb5_context context, krb5_tkt_creds_context ctx, + krb5_boolean referral) { krb5_error_code code; + int extra_options; - if (ctx->flags & KRB5_TKT_CREDS_STEP_FLAG_COMPLETE) - code = krb5int_copy_creds_contents(context, ctx->out_cred, creds); - else - code = KRB5_NO_TKT_SUPPLIED; + /* Include the caller-specified KDC options in service requests. */ + extra_options = ctx->kdcopt; + + /* Automatically set the enc-tkt-in-skey flag for user-to-user requests. */ + if (ctx->in_creds->second_ticket.length != 0 && + (extra_options & KDC_OPT_CNAME_IN_ADDL_TKT) == 0) + extra_options |= KDC_OPT_ENC_TKT_IN_SKEY; + /* Set the canonicalize flag for referral requests. */ + if (referral) + extra_options |= KDC_OPT_CANONICALIZE; + + /* + * Use the profile enctypes for referral requests, since we might get back + * a TGT. We'll ask again with context enctypes if we get the actual + * service ticket and it's not consistent with the context enctypes. + */ + if (referral) + context->use_conf_ktypes = TRUE; + code = make_request(context, ctx, ctx->in_creds, extra_options); + if (referral) + context->use_conf_ktypes = FALSE; return code; } -/* - * Store credentials in credentials cache. If ccache is NULL, the - * credentials cache associated with the context is used. This can - * be called on an incomplete context, in which case the referral - * TGT only will be stored. - */ -krb5_error_code KRB5_CALLCONV -krb5_tkt_creds_store_creds(krb5_context context, - krb5_tkt_creds_context ctx, - krb5_ccache ccache) +/* Decode and decrypt a TGS reply, and set the reply_code or reply_creds field + * of ctx with the result. Also handle too-big errors. */ +static krb5_error_code +get_creds_from_tgs_reply(krb5_context context, krb5_tkt_creds_context ctx, + krb5_data *reply) { krb5_error_code code; - if (ccache == NULL) - ccache = ctx->ccache; + krb5_free_creds(context, ctx->reply_creds); + ctx->reply_creds = NULL; + code = krb5int_process_tgs_reply(context, reply, ctx->cur_tgt, ctx->kdcopt, + ctx->cur_tgt->addresses, NULL, + ctx->in_creds, ctx->timestamp, + ctx->nonce, ctx->subkey, NULL, NULL, + &ctx->reply_creds); + if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG) { + /* Instruct the caller to re-send the request with TCP. */ + code = set_caller_request(context, ctx); + if (code != 0) + return code; + return KRB5KRB_ERR_RESPONSE_TOO_BIG; + } - /* Only store the referral from our local KDC */ - if (ctx->referral_tgts[0] != NULL) - krb5_cc_store_cred(context, ccache, ctx->referral_tgts[0]); + /* Depending on our state, we may or may not be able to handle an error. + * For now, store it in the context and return success. */ + ctx->reply_code = code; + return 0; +} - if (ctx->flags & KRB5_TKT_CREDS_STEP_FLAG_COMPLETE) - code = krb5_cc_store_cred(context, ccache, ctx->out_cred); - else - code = KRB5_NO_TKT_SUPPLIED; +/* Add realm to ctx->realms_seen so that we can avoid revisiting it later. */ +static krb5_error_code +remember_realm(krb5_context context, krb5_tkt_creds_context ctx, + const krb5_data *realm) +{ + size_t len = 0; + krb5_data *new_list; - return code; + if (ctx->realms_seen != NULL) { + for (len = 0; ctx->realms_seen[len].data != NULL; len++); + } + new_list = realloc(ctx->realms_seen, (len + 2) * sizeof(krb5_data)); + if (new_list == NULL) + return ENOMEM; + ctx->realms_seen = new_list; + new_list[len] = empty_data(); + new_list[len + 1] = empty_data(); + return krb5int_copy_data_contents(context, realm, &new_list[len]); } -krb5_error_code KRB5_CALLCONV -krb5_tkt_creds_get_times(krb5_context context, - krb5_tkt_creds_context ctx, - krb5_ticket_times *times) +/* Return TRUE if realm appears to ctx->realms_seen. */ +static krb5_boolean +seen_realm_before(krb5_context context, krb5_tkt_creds_context ctx, + const krb5_data *realm) { - if ((ctx->flags & KRB5_TKT_CREDS_STEP_FLAG_COMPLETE) == 0) - return KRB5_NO_TKT_SUPPLIED; + size_t i; - *times = ctx->out_cred->times; + if (ctx->realms_seen != NULL) { + for (i = 0; ctx->realms_seen[i].data != NULL; i++) { + if (data_eq(ctx->realms_seen[i], *realm)) + return TRUE; + } + } + return FALSE; +} + +/***** STATE_NON_REFERRAL *****/ +/* Process the response to a non-referral request. */ +static krb5_error_code +step_non_referral(krb5_context context, krb5_tkt_creds_context ctx) +{ + /* No fallbacks if we didn't get a successful reply. */ + if (ctx->reply_code) + return ctx->reply_code; + + /* Note the authdata we asked for in the output creds. */ + ctx->reply_creds->authdata = ctx->authdata; + ctx->authdata = NULL; + ctx->state = STATE_COMPLETE; return 0; } -void KRB5_CALLCONV -krb5_tkt_creds_free(krb5_context context, - krb5_tkt_creds_context ctx) +/* Make a non-referrals request for the desired service ticket. */ +static krb5_error_code +begin_non_referral(krb5_context context, krb5_tkt_creds_context ctx) { - int i; + ctx->state = STATE_NON_REFERRAL; + return make_request_for_service(context, ctx, FALSE); +} - if (ctx == NULL) - return; +/***** STATE_REFERRALS *****/ - krb5_free_principal(context, ctx->req_server); - krb5_free_cred_contents(context, &ctx->in_cred); - krb5_free_creds(context, ctx->out_cred); - krb5_free_data_contents(context, &ctx->encoded_previous_request); - krb5_free_keyblock(context, ctx->subkey); +/* + * Retry a request in the fallback realm after a referral request failure in + * the local realm. We only do this if the originally requested service + * principal was in the referral realm. + */ +static krb5_error_code +try_fallback_realm(krb5_context context, krb5_tkt_creds_context ctx) +{ + krb5_error_code code; + char **hrealms; + krb5_creds *server_tgt; - /* Free referral TGTs list. */ - for (i = 0; i < KRB5_REFERRAL_MAXHOPS; i++) { - if (ctx->referral_tgts[i] != NULL) { - krb5_free_creds(context, ctx->referral_tgts[i]); - ctx->referral_tgts[i] = NULL; - } + if (ctx->server->length < 2) { + /* We need a type/host format principal to find a fallback realm. */ + return KRB5_ERR_HOST_REALM_UNKNOWN; } - free(ctx); + /* We expect this to give exactly one answer (XXX clean up interface). */ + code = krb5_get_fallback_host_realm(context, &ctx->server->data[1], + &hrealms); + if (code != 0) + return code; + + if (data_eq_string(ctx->server->realm, hrealms[0])) { + /* Fallback realm isn't any different, so just give up. */ + return KRB5_ERR_HOST_REALM_UNKNOWN; + } + + /* Rewrite server->realm to be the fallback realm. */ + krb5_free_data_contents(context, &ctx->server->realm); + ctx->server->realm = string2data(hrealms[0]); + free(hrealms); + + /* Obtain a TGT for the new service realm. */ + ctx->getting_tgt_for = STATE_NON_REFERRAL; + return begin_get_tgt(context, ctx); } +/* Return true if context contains app-provided TGS enctypes and enctype is not + * one of them. */ +static krb5_boolean +wrong_enctype(krb5_context context, krb5_enctype enctype) +{ + size_t i; + + if (context->tgs_etypes == NULL) + return FALSE; + for (i = 0; context->tgs_etypes[i] != 0; i++) { + if (enctype == context->tgs_etypes[i]) + return FALSE; + } + return TRUE; +} + +/* Advance the referral request loop. */ static krb5_error_code -tkt_creds_step_request(krb5_context context, - krb5_tkt_creds_context ctx, - krb5_data *req) +step_referrals(krb5_context context, krb5_tkt_creds_context ctx) { krb5_error_code code; + const krb5_data *referral_realm; + + if (ctx->reply_code != 0) { + /* If we had an unknown realm, and we tried the local realm and failed, + * try the fallback realm before giving up. */ + if (ctx->referral_count == 1 && + krb5_is_referral_realm(&ctx->req_server->realm)) + return try_fallback_realm(context, ctx); + else + return ctx->reply_code; + } + + if (krb5_principal_compare(context, ctx->reply_creds->server, + ctx->server)) { + /* We got the ticket we asked for... but we didn't necessarily ask for + * it with the right enctypes. */ + if (wrong_enctype(context, ctx->reply_creds->keyblock.enctype)) { + /* Try again with the app-provided enctypes. */ + krb5_free_creds(context, ctx->reply_creds); + ctx->reply_creds = NULL; + return begin_non_referral(context, ctx); + } + + /* Note the authdata we asked for in the output creds. */ + ctx->reply_creds->authdata = ctx->authdata; + ctx->authdata = NULL; + ctx->state = STATE_COMPLETE; + return 0; + } + + if (!IS_TGS_PRINC(context, ctx->reply_creds->server)) { + /* We didn't get what we asked or a TGT. Old versions of Active + * Directory can do this. Try again with canonicalize off. */ + krb5_free_creds(context, ctx->reply_creds); + ctx->reply_creds = NULL; + return begin_non_referral(context, ctx); + } + + if (ctx->referral_count == 1) { + /* Cache the referral TGT only if it's from the local realm. + * Make sure to note the associated authdata, if any. */ + code = krb5_copy_authdata(context, ctx->authdata, + &ctx->reply_creds->authdata); + if (code != 0) + return code; + code = krb5_cc_store_cred(context, ctx->ccache, ctx->reply_creds); + if (code != 0) + return code; + + /* The authdata is in this TGT and will be copied into subsequent TGTs + * or the final credentials, so we don't need to ask for it again. */ + krb5_free_authdata(context, ctx->in_creds->authdata); + ctx->in_creds->authdata = NULL; + } - if (ctx->referral_count >= KRB5_REFERRAL_MAXHOPS) + if (ctx->referral_count++ >= KRB5_REFERRAL_MAXHOPS) { + /* We've gotten too many referral TGTs; it's time to give up. */ return KRB5_KDC_UNREACH; + } + + /* Check for referral loops. */ + referral_realm = &ctx->reply_creds->server->data[1]; + if (seen_realm_before(context, ctx, referral_realm)) + return KRB5_KDC_UNREACH; + code = remember_realm(context, ctx, referral_realm); + if (code != 0) + return code; - assert(ctx->tgtptr != NULL); + /* Use the referral TGT for the next request. */ + krb5_free_creds(context, ctx->cur_tgt); + ctx->cur_tgt = ctx->reply_creds; + ctx->reply_creds = NULL; - /* Copy krbtgt realm to server principal */ + /* Rewrite the server realm to be the referral realm. */ krb5_free_data_contents(context, &ctx->server->realm); - code = krb5int_copy_data_contents(context, - &ctx->tgtptr->server->data[1], + code = krb5int_copy_data_contents(context, referral_realm, &ctx->server->realm); if (code != 0) return code; - ctx->kdcopt = ctx->req_kdcopt | KDC_OPT_CANONICALIZE; + /* Generate the next referral request. */ + return make_request_for_service(context, ctx, TRUE); +} - if (ctx->in_cred.second_ticket.length != 0 && - (ctx->kdcopt & KDC_OPT_CNAME_IN_ADDL_TKT) == 0) { - ctx->kdcopt |= KDC_OPT_ENC_TKT_IN_SKEY; - } +/* + * Begin the referrals request loop. Expects ctx->cur_tgt to be a TGT for + * ctx->realm->server. + */ +static krb5_error_code +begin_referrals(krb5_context context, krb5_tkt_creds_context ctx) +{ + ctx->state = STATE_REFERRALS; + ctx->referral_count = 1; - if ((ctx->flags & KRB5_TKT_CREDS_STEP_FLAG_CTX_KTYPES) == 0) - context->use_conf_ktypes = 1; + /* Empty out the realms-seen list for loop checking. */ + krb5int_free_data_list(context, ctx->realms_seen); + ctx->realms_seen = NULL; - code = tkt_make_tgs_request(context, ctx, ctx->tgtptr, - &ctx->in_cred, req); + /* Generate the first referral request. */ + return make_request_for_service(context, ctx, TRUE); +} - context->use_conf_ktypes = ctx->default_use_conf_ktypes; +/***** STATE_GET_TGT_OFFPATH *****/ - return code; +/* + * Foreign TGT acquisition can happen either before the referrals loop, if the + * service principal had an explicitly specified foreign realm, or after it + * fails, if we wind up using the fallback realm. end_get_tgt() advances to + * the appropriate state depending on which we were doing. + */ +static krb5_error_code +end_get_tgt(krb5_context context, krb5_tkt_creds_context ctx) +{ + if (ctx->getting_tgt_for == STATE_REFERRALS) + return begin_referrals(context, ctx); + else + return begin_non_referral(context, ctx); } +/* + * We enter STATE_GET_TGT_OFFPATH from STATE_GET_TGT if we receive, from one of + * the KDCs in the expected path, a TGT for a realm not in the path. This may + * happen if the KDC has a different idea of the expected path than we do. If + * it happens, we repeatedly ask the KDC of the TGT we have for a destination + * realm TGT, until we get it, fail, or give up. + */ + +/* Advance the process of chasing off-path TGTs. */ static krb5_error_code -tkt_creds_step_reply(krb5_context context, - krb5_tkt_creds_context ctx, - krb5_data *rep) +step_get_tgt_offpath(krb5_context context, krb5_tkt_creds_context ctx) { krb5_error_code code; - unsigned int i; - krb5_boolean got_tkt = FALSE; + const krb5_data *tgt_realm; + + /* We have no fallback if the last request failed, so just give up. */ + if (ctx->reply_code != 0) + return ctx->reply_code; + + /* Verify that we got a TGT. */ + if (!IS_TGS_PRINC(context, ctx->reply_creds->server)) + return KRB5_KDCREP_MODIFIED; + + /* Use this tgt for the next request. */ + krb5_free_creds(context, ctx->cur_tgt); + ctx->cur_tgt = ctx->reply_creds; + ctx->reply_creds = NULL; + + /* Check if we've seen this realm before, and remember it. */ + tgt_realm = &ctx->cur_tgt->server->data[1]; + if (seen_realm_before(context, ctx, tgt_realm)) + return KRB5_KDC_UNREACH; + code = remember_realm(context, ctx, tgt_realm); + if (code != 0) + return code; + + if (data_eq(*tgt_realm, ctx->server->realm)) { + /* We received the server realm TGT we asked for. */ + return end_get_tgt(context, ctx); + } else if (ctx->offpath_count++ >= KRB5_REFERRAL_MAXHOPS) { + /* Time to give up. */ + return KRB5_KDCREP_MODIFIED; + } + + return make_request_for_tgt(context, ctx, &ctx->server->realm); +} + +/* Begin chasing off-path referrals, starting from ctx->cur_tgt. */ +static krb5_error_code +begin_get_tgt_offpath(krb5_context context, krb5_tkt_creds_context ctx) +{ + ctx->state = STATE_GET_TGT_OFFPATH; + ctx->offpath_count = 1; + return make_request_for_tgt(context, ctx, &ctx->server->realm); +} - krb5_free_creds(context, ctx->out_cred); - ctx->out_cred = NULL; +/***** STATE_GET_TGT *****/ - code = tkt_process_tgs_reply(context, ctx, rep, ctx->tgtptr, - &ctx->in_cred, &ctx->out_cred); +/* + * To obtain a foreign TGT, we first construct a path of realms R1..Rn between + * the local realm and the target realm, using krb5_walk_realm_tree(). Usually + * this path is based on the domain hierarchy, but it may be altered by + * configuration. + * + * We begin with cur_realm set to the local realm (R1) and next_realm set to + * the target realm (Rn). At each step, we check to see if we have a cached + * TGT for next_realm; if not, we ask cur_realm to give us a TGT for + * next_realm. If that fails, we decrement next_realm until we get a + * successful answer or reach cur_realm--in which case we've gotten as far as + * we can, and have to give up. If we do get back a TGT, it may or may not be + * for the realm we asked for, so we search for it in the path. The realm of + * the TGT we get back becomes cur_realm, and next_realm is reset to the target + * realm. Overall, this is an O(n^2) process in the length of the path, but + * the path length will generally be short and the process will usually end + * much faster than the worst case. + * + * In some cases we may get back a realm for a TGT not in the path. In that + * case we enter STATE_GET_TGT_OFFPATH. + */ + +/* Initialize the realm path fields for getting a TGT for + * ctx->server->realm. */ +static krb5_error_code +init_realm_path(krb5_context context, krb5_tkt_creds_context ctx) +{ + krb5_error_code code; + krb5_principal *tgt_princ_list = NULL; + krb5_data *realm_path; + size_t nrealms, i; + + /* Make sure we're actually trying to acquire a foreign TGT. */ + if (data_eq(ctx->client->realm, ctx->server->realm)) + return KRB5_CC_NOTFOUND; + + /* Construct a list of TGT principals from client to server. We will throw + * this away after grabbing the remote realms from each principal. */ + code = krb5_walk_realm_tree(context, &ctx->client->realm, + &ctx->server->realm, + &tgt_princ_list, KRB5_REALM_BRANCH_CHAR); if (code != 0) + return code; + + /* Count the number of principals and allocate the realm path. */ + for (nrealms = 0; tgt_princ_list[nrealms]; nrealms++); + assert(nrealms > 1); + realm_path = k5alloc((nrealms + 1) * sizeof(*realm_path), &code); + if (realm_path == NULL) goto cleanup; - /* - * Referral request succeeded; let's see what it is - */ - if (krb5_principal_compare(context, ctx->server, ctx->out_cred->server)) { - /* - * Check if the return enctype is one that we requested if - * needed. - */ - if (ctx->default_use_conf_ktypes || context->tgs_etypes == NULL) - got_tkt = TRUE; - else - for (i = 0; context->tgs_etypes[i] != ENCTYPE_NULL; i++) { - if (ctx->out_cred->keyblock.enctype == context->tgs_etypes[i]) { - /* Found an allowable etype, so we're done */ - got_tkt = TRUE; - break; - } - } + /* Steal the remote realm field from each TGT principal. */ + for (i = 0; i < nrealms; i++) { + assert(tgt_princ_list[i]->length == 2); + realm_path[i] = tgt_princ_list[i]->data[1]; + tgt_princ_list[i]->data[1].data = NULL; + } + realm_path[nrealms] = empty_data(); - if (got_tkt == FALSE) - ctx->flags |= KRB5_TKT_CREDS_STEP_FLAG_CTX_KTYPES; /* try again */ - } else if (IS_TGS_PRINC(context, ctx->out_cred->server)) { - krb5_data *r1, *r2; + /* Initialize the realm path fields in ctx. */ + ctx->realm_path = realm_path; + ctx->last_realm = realm_path + nrealms - 1; + ctx->cur_realm = realm_path; + ctx->next_realm = ctx->last_realm; + realm_path = NULL; - if (ctx->referral_count == 0) - r1 = &ctx->tgtptr->server->data[1]; - else - r1 = &ctx->referral_tgts[ctx->referral_count - 1]->server->data[1]; +cleanup: + krb5_free_realm_tree(context, tgt_princ_list); + return 0; +} - r2 = &ctx->out_cred->server->data[1]; - if (data_eq(*r1, *r2)) { - code = KRB5_KDC_UNREACH; - goto cleanup; - } +/* Find realm within the portion of ctx->realm_path following + * ctx->cur_realm. Return NULL if it is not found. */ +static const krb5_data * +find_realm_in_path(krb5_context context, krb5_tkt_creds_context ctx, + const krb5_data *realm) +{ + const krb5_data *r; - /* Check for referral routing loop. */ - for (i = 0; i < ctx->referral_count; i++) { - if (krb5_principal_compare(context, - ctx->out_cred->server, - ctx->referral_tgts[i]->server)) { - code = KRB5_KDC_UNREACH; - goto cleanup; - } + for (r = ctx->cur_realm + 1; r->data != NULL; r++) { + if (data_eq(*r, *realm)) + return r; + } + return NULL; +} + +/* + * Generate the next request in the path traversal. If a cached TGT for the + * target realm appeared in the ccache since we started the TGT acquisition + * process, tihs function may invoke end_get_tgt(). + */ +static krb5_error_code +get_tgt_request(krb5_context context, krb5_tkt_creds_context ctx) +{ + krb5_error_code code; + krb5_creds *cached_tgt; + + while (1) { + /* Check if we have a cached TGT for the target realm. */ + code = get_cached_tgt(context, ctx, ctx->next_realm, &cached_tgt); + if (code != 0) + return code; + if (cached_tgt != NULL) { + /* Advance the current realm and keep going. */ + krb5_free_creds(context, ctx->cur_tgt); + ctx->cur_tgt = cached_tgt; + if (ctx->next_realm == ctx->last_realm) + return end_get_tgt(context, ctx); + ctx->cur_realm = ctx->next_realm; + ctx->next_realm = ctx->last_realm; + continue; } - /* Point current tgt pointer at newly-received TGT. */ - ctx->tgtptr = ctx->out_cred; - /* avoid multiple copies of authdata */ - ctx->out_cred->authdata = ctx->in_cred.authdata; - ctx->in_cred.authdata = NULL; + return make_request_for_tgt(context, ctx, ctx->next_realm); + } +} - ctx->referral_tgts[ctx->referral_count++] = ctx->out_cred; - ctx->out_cred = NULL; +/* Process a TGS reply and advance the path traversal to get a foreign TGT. */ +static krb5_error_code +step_get_tgt(krb5_context context, krb5_tkt_creds_context ctx) +{ + krb5_error_code code; + const krb5_data *tgt_realm, *path_realm; + + if (ctx->reply_code != 0) { + /* The last request failed. Try the next-closest realm to + * ctx->cur_realm. */ + ctx->next_realm--; + if (ctx->next_realm == ctx->cur_realm) { + /* We've tried all the realms we could and couldn't progress beyond + * ctx->cur_realm, so it's time to give up. */ + return ctx->reply_code; + } } else { - code = KRB5KRB_AP_ERR_NO_TGT; + /* Verify that we got a TGT. */ + if (!IS_TGS_PRINC(context, ctx->reply_creds->server)) + return KRB5_KDCREP_MODIFIED; + + /* Use this tgt for the next request regardless of what it is. */ + krb5_free_creds(context, ctx->cur_tgt); + ctx->cur_tgt = ctx->reply_creds; + ctx->reply_creds = NULL; + + /* Remember that we saw this realm. */ + tgt_realm = &ctx->cur_tgt->server->data[1]; + code = remember_realm(context, ctx, tgt_realm); + if (code != 0) + return code; + + /* See where we wound up on the path (or off it). */ + path_realm = find_realm_in_path(context, ctx, tgt_realm); + if (path_realm != NULL) { + /* We got a realm on the expected path, so we can cache it. */ + code = krb5_cc_store_cred(context, ctx->ccache, ctx->cur_tgt); + if (code != 0) + return code; + if (path_realm == ctx->last_realm) { + /* We received a TGT for the target realm. */ + return end_get_tgt(context, ctx); + } else if (path_realm != NULL) { + /* We still have further to go; advance the traversal. */ + ctx->cur_realm = path_realm; + ctx->next_realm = ctx->last_realm; + } + } else if (data_eq(*tgt_realm, ctx->client->realm)) { + /* We were referred back to the local realm, which is bad. */ + return KRB5_KDCREP_MODIFIED; + } else { + /* We went off the path; start the off-path chase. */ + return begin_get_tgt_offpath(context, ctx); + } } - assert(ctx->tgtptr == NULL || code == 0); + /* Generate the next request in the path traversal. */ + return get_tgt_request(context, ctx); +} - if (code == 0 && got_tkt == TRUE) { - krb5_free_principal(context, ctx->out_cred->server); - ctx->out_cred->server = ctx->req_server; - ctx->req_server = NULL; +/* + * Begin the process of getting a foreign TGT, either for the explicitly + * specified server realm or for the fallback realm. Expects that + * ctx->server->realm is the realm of the desired TGT, and that + * ctx->getting_tgt_for is the state we should advance to after we have the + * desired TGT. + */ +static krb5_error_code +begin_get_tgt(krb5_context context, krb5_tkt_creds_context ctx) +{ + krb5_error_code code; + krb5_creds *cached_tgt; - if (ctx->in_cred.authdata != NULL) { - code = krb5_copy_authdata(context, ctx->in_cred.authdata, - &ctx->out_cred->authdata); - } + ctx->state = STATE_GET_TGT; - ctx->flags |= KRB5_TKT_CREDS_STEP_FLAG_COMPLETE; + /* See if we have a cached TGT for the server realm. */ + code = get_cached_tgt(context, ctx, &ctx->server->realm, &cached_tgt); + if (code != 0) + return code; + if (cached_tgt != NULL) { + krb5_free_creds(context, ctx->cur_tgt); + ctx->cur_tgt = cached_tgt; + return end_get_tgt(context, ctx); } -cleanup: - return code; + /* Initialize the realm path. */ + code = init_realm_path(context, ctx); + if (code != 0) + return code; + + /* Start with the local tgt. */ + krb5_free_creds(context, ctx->cur_tgt); + ctx->cur_tgt = NULL; + code = get_cached_tgt(context, ctx, &ctx->client->realm, &ctx->cur_tgt); + if (code != 0) + return code; + if (ctx->cur_tgt == NULL) + return KRB5_CC_NOTFOUND; + + /* Empty out the realms-seen list for loop checking. */ + krb5int_free_data_list(context, ctx->realms_seen); + ctx->realms_seen = NULL; + + /* Generate the first request. */ + return get_tgt_request(context, ctx); } -krb5_error_code KRB5_CALLCONV -krb5_tkt_creds_step(krb5_context context, - krb5_tkt_creds_context ctx, - krb5_data *in, - krb5_data *out, - krb5_data *realm, - unsigned int *flags) +/***** STATE_BEGIN *****/ + +static krb5_error_code +begin(krb5_context context, krb5_tkt_creds_context ctx) { - krb5_error_code code, code2; + krb5_creds *server_tgt; + krb5_error_code code; - *flags = 0; + /* If the server realm is unspecified, start with the client realm. */ + if (krb5_is_referral_realm(&ctx->server->realm)) { + krb5_free_data_contents(context, &ctx->server->realm); + code = krb5int_copy_data_contents(context, &ctx->client->realm, + &ctx->server->realm); + if (code != 0) + return code; + } - out->data = NULL; - out->length = 0; + /* Obtain a TGT for the service realm. */ + ctx->getting_tgt_for = STATE_REFERRALS; + return begin_get_tgt(context, ctx); +} - realm->data = NULL; - realm->length = 0; +/***** API functions *****/ - if (ctx->flags & KRB5_TKT_CREDS_STEP_FLAG_COMPLETE) +krb5_error_code KRB5_CALLCONV +krb5_tkt_creds_init(krb5_context context, krb5_ccache ccache, + krb5_creds *in_creds, int kdcopt, + krb5_tkt_creds_context *pctx) +{ + krb5_error_code code; + krb5_tkt_creds_context ctx = NULL; + + ctx = k5alloc(sizeof(*ctx), &code); + if (ctx == NULL) goto cleanup; - if (in != NULL && in->length != 0) { - code = tkt_creds_step_reply(context, ctx, in); - if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG) { - code2 = krb5int_copy_data_contents(context, - &ctx->encoded_previous_request, - out); - if (code2 != 0) - code = code2; - goto copy_realm; - } - if (code != 0 || (ctx->flags & KRB5_TKT_CREDS_STEP_FLAG_COMPLETE)) - goto cleanup; - } + ctx->state = STATE_BEGIN; - code = tkt_creds_step_request(context, ctx, out); + code = krb5_copy_creds(context, in_creds, &ctx->in_creds); if (code != 0) goto cleanup; - - assert(out->length != 0); - - code = krb5int_copy_data_contents(context, - out, - &ctx->encoded_previous_request); + ctx->client = ctx->in_creds->client; + ctx->server = ctx->in_creds->server; + code = krb5_copy_principal(context, ctx->server, &ctx->req_server); if (code != 0) goto cleanup; - -copy_realm: - code2 = krb5int_copy_data_contents(context, &ctx->server->realm, realm); - if (code2 != 0) { - code = code2; + /* XXX Make an alias for now; use krb5_cc_dup later. */ + ctx->ccache = ccache; + ctx->req_kdcopt = kdcopt; + code = krb5_copy_authdata(context, in_creds->authdata, &ctx->authdata); + if (code != 0) goto cleanup; - } -cleanup: - *flags = (ctx->flags & KRB5_TKT_CREDS_STEP_FLAG_COMPLETE); + *pctx = ctx; + ctx = NULL; +cleanup: + krb5_tkt_creds_free(context, ctx); return code; } +krb5_error_code KRB5_CALLCONV +krb5_tkt_creds_get_creds(krb5_context context, krb5_tkt_creds_context ctx, + krb5_creds *creds) +{ + if (ctx->state != STATE_COMPLETE) + return KRB5_NO_TKT_SUPPLIED; + return krb5int_copy_creds_contents(context, ctx->reply_creds, creds); +} + +/* Store credentials in credentials cache. If ccache is NULL, the + * credentials cache associated with the context is used. */ +krb5_error_code KRB5_CALLCONV +krb5_tkt_creds_store_creds(krb5_context context, krb5_tkt_creds_context ctx, + krb5_ccache ccache) +{ + if (ctx->state != STATE_COMPLETE) + return KRB5_NO_TKT_SUPPLIED; + if (ccache == NULL) + ccache = ctx->ccache; + return krb5_cc_store_cred(context, ccache, ctx->reply_creds); +} + +krb5_error_code KRB5_CALLCONV +krb5_tkt_creds_get_times(krb5_context context, krb5_tkt_creds_context ctx, + krb5_ticket_times *times) +{ + if (ctx->state != STATE_COMPLETE) + return KRB5_NO_TKT_SUPPLIED; + *times = ctx->reply_creds->times; + return 0; +} + +void KRB5_CALLCONV +krb5_tkt_creds_free(krb5_context context, krb5_tkt_creds_context ctx) +{ + if (ctx == NULL) + return; + krb5_free_creds(context, ctx->in_creds); + krb5_free_principal(context, ctx->req_server); + krb5_free_authdata(context, ctx->authdata); + krb5_free_creds(context, ctx->cur_tgt); + krb5int_free_data_list(context, ctx->realms_seen); + krb5_free_keyblock(context, ctx->subkey); + krb5_free_data_contents(context, &ctx->previous_request); + krb5int_free_data_list(context, ctx->realm_path); + krb5_free_creds(context, ctx->reply_creds); + free(ctx); +} + +krb5_error_code KRB5_CALLCONV +krb5_tkt_creds_step(krb5_context context, krb5_tkt_creds_context ctx, + krb5_data *in, krb5_data *out, krb5_data *realm, + unsigned int *flags) +{ + krb5_error_code code; + krb5_boolean no_input = (in == NULL || in->length == 0); + + *out = empty_data(); + *realm = empty_data(); + *flags = 0; + + /* We should receive an empty input on the first step only, and should not + * get called after completion. */ + if (no_input != (ctx->state == STATE_BEGIN) || + ctx->state == STATE_COMPLETE) + return EINVAL; + + ctx->caller_out = out; + ctx->caller_realm = realm; + ctx->caller_flags = flags; + + if (!no_input) { + /* Convert the input token into a credential and store it in ctx. */ + code = get_creds_from_tgs_reply(context, ctx, in); + if (code != 0) + return code; + } + + if (ctx->state == STATE_BEGIN) + return begin(context, ctx); + else if (ctx->state == STATE_GET_TGT) + return step_get_tgt(context, ctx); + else if (ctx->state == STATE_GET_TGT_OFFPATH) + return step_get_tgt_offpath(context, ctx); + else if (ctx->state == STATE_REFERRALS) + return step_referrals(context, ctx); + else if (ctx->state == STATE_NON_REFERRAL) + return step_non_referral(context, ctx); + else + return EINVAL; +}