/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* lib/krb5/krb/get_in_tkt.c */
/*
 * Copyright 1990,1991, 2003, 2008 by the Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * Export of this software from the United States of America may
 *   require a specific license from the United States Government.
 *   It is the responsibility of any person or organization contemplating
 *   export to obtain such a license before exporting.
 *
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 */

#include "k5-int.h"
#include "int-proto.h"
#include "os-proto.h"
#include "fast.h"
#include "init_creds_ctx.h"

/* some typedef's for the function args to make things look a bit cleaner */

static krb5_error_code make_preauth_list (krb5_context,
                                          krb5_preauthtype *,
                                          int, krb5_pa_data ***);
static krb5_error_code sort_krb5_padata_sequence(krb5_context context,
                                                 krb5_data *realm,
                                                 krb5_pa_data **padata);

/*
 * Decrypt the AS reply in ctx, populating ctx->reply->enc_part2.  If
 * strengthen_key is not null, combine it with the reply key as specified in
 * RFC 6113 section 5.4.3.  Place the key used in *key_out.
 */
static krb5_error_code
decrypt_as_reply(krb5_context context, krb5_init_creds_context ctx,
                 const krb5_keyblock *strengthen_key, krb5_keyblock *key_out)
{
    krb5_error_code ret;
    krb5_keyblock key;
    krb5_responder_fn responder;
    void *responder_data;

    memset(key_out, 0, sizeof(*key_out));
    memset(&key, 0, sizeof(key));

    if (ctx->as_key.length) {
        /* The reply key was computed or replaced during preauth processing;
         * try it. */
        TRACE_INIT_CREDS_AS_KEY_PREAUTH(context, &ctx->as_key);
        ret = krb5int_fast_reply_key(context, strengthen_key, &ctx->as_key,
                                     &key);
        if (ret)
            return ret;
        ret = krb5_kdc_rep_decrypt_proc(context, &key, NULL, ctx->reply);
        if (!ret) {
            *key_out = key;
            return 0;
        }
        krb5_free_keyblock_contents(context, &key);
        TRACE_INIT_CREDS_PREAUTH_DECRYPT_FAIL(context, ret);

        /*
         * For two reasons, we fall back to trying or retrying the gak_fct if
         * this fails:
         *
         * 1. The KDC might encrypt the reply using a different enctype than
         *    the AS key we computed during preauth.
         *
         * 2. For 1.1.1 and prior KDC's, when SAM is used with USE_SAD_AS_KEY,
         *    the AS-REP is encrypted in the client long-term key instead of
         *    the SAD.
         *
         * The gak_fct for krb5_get_init_creds_with_password() caches the
         * password, so this fallback does not result in a second password
         * prompt.
         */
    } else {
        /*
         * No AS key was computed during preauth processing, perhaps because
         * preauth was not used.  If the caller supplied a responder callback,
         * possibly invoke it before calling the gak_fct for real.
         */
        k5_gic_opt_get_responder(ctx->opt, &responder, &responder_data);
        if (responder != NULL) {
            /* Indicate a need for the AS key by calling the gak_fct with a
             * NULL as_key. */
            ret = ctx->gak_fct(context, ctx->request->client, ctx->etype, NULL,
                               NULL, NULL, NULL, NULL, ctx->gak_data,
                               ctx->rctx.items);
            if (ret)
                return ret;

            /* If that produced a responder question, invoke the responder. */
            if (!k5_response_items_empty(ctx->rctx.items)) {
                ret = (*responder)(context, responder_data, &ctx->rctx);
                if (ret)
                    return ret;
            }
        }
    }

    /* Compute or re-compute the AS key, prompting for the password if
     * necessary. */
    TRACE_INIT_CREDS_GAK(context, &ctx->salt, &ctx->s2kparams);
    ret = ctx->gak_fct(context, ctx->request->client,
                       ctx->reply->enc_part.enctype, ctx->prompter,
                       ctx->prompter_data, &ctx->salt, &ctx->s2kparams,
                       &ctx->as_key, ctx->gak_data, ctx->rctx.items);
    if (ret)
        return ret;
    TRACE_INIT_CREDS_AS_KEY_GAK(context, &ctx->as_key);

    ret = krb5int_fast_reply_key(context, strengthen_key, &ctx->as_key, &key);
    if (ret)
        return ret;
    ret = krb5_kdc_rep_decrypt_proc(context, &key, NULL, ctx->reply);
    if (ret) {
        krb5_free_keyblock_contents(context, &key);
        return ret;
    }

    *key_out = key;
    return 0;
}

/**
 * Fully anonymous replies include a pa_pkinit_kx padata type including the KDC
 * contribution key.  This routine confirms that the session key is of the
 * right form for fully anonymous requests.  It is here rather than in the
 * preauth code because the session key cannot be verified until the AS reply
 * is decrypted and the preauth code all runs before the AS reply is decrypted.
 */
static krb5_error_code
verify_anonymous( krb5_context context, krb5_kdc_req *request,
                  krb5_kdc_rep *reply, krb5_keyblock *as_key)
{
    krb5_error_code ret = 0;
    krb5_pa_data *pa;
    krb5_data scratch;
    krb5_keyblock *kdc_key = NULL, *expected = NULL;
    krb5_enc_data *enc = NULL;
    krb5_keyblock *session = reply->enc_part2->session;

    if (!krb5_principal_compare_any_realm(context, request->client,
                                          krb5_anonymous_principal()))
        return 0; /* Only applies to fully anonymous */
    pa = krb5int_find_pa_data(context, reply->padata, KRB5_PADATA_PKINIT_KX);
    if (pa == NULL)
        goto verification_error;
    scratch.length = pa->length;
    scratch.data = (char *) pa->contents;
    ret = decode_krb5_enc_data( &scratch, &enc);
    if (ret)
        goto cleanup;
    scratch.data = k5alloc(enc->ciphertext.length, &ret);
    if (ret)
        goto cleanup;
    scratch.length = enc->ciphertext.length;
    ret = krb5_c_decrypt(context, as_key, KRB5_KEYUSAGE_PA_PKINIT_KX,
                         NULL /*cipherstate*/, enc, &scratch);
    if (ret) {
        free(scratch.data);
        goto cleanup;
    }
    ret = decode_krb5_encryption_key( &scratch, &kdc_key);
    zap(scratch.data, scratch.length);
    free(scratch.data);
    if (ret)
        goto cleanup;
    ret = krb5_c_fx_cf2_simple(context, kdc_key, "PKINIT",
                               as_key, "KEYEXCHANGE", &expected);
    if (ret)
        goto cleanup;
    if ((expected->enctype != session->enctype) ||
        (expected->length != session->length) ||
        (memcmp(expected->contents, session->contents, expected->length) != 0))
        goto verification_error;
cleanup:
    if (kdc_key)
        krb5_free_keyblock(context, kdc_key);
    if (expected)
        krb5_free_keyblock(context, expected);
    if (enc)
        krb5_free_enc_data(context, enc);
    return ret;
verification_error:
    ret = KRB5_KDCREP_MODIFIED;
    k5_setmsg(context, ret,
              _("Reply has wrong form of session key for anonymous request"));
    goto cleanup;
}

static krb5_error_code
verify_as_reply(krb5_context            context,
                krb5_timestamp          time_now,
                krb5_kdc_req            *request,
                krb5_kdc_rep            *as_reply)
{
    krb5_error_code             retval;
    int                         canon_req;
    int                         canon_ok;
    krb5_timestamp              time_offset;

    /* check the contents for sanity: */
    if (!as_reply->enc_part2->times.starttime)
        as_reply->enc_part2->times.starttime =
            as_reply->enc_part2->times.authtime;

    /*
     * We only allow the AS-REP server name to be changed if the
     * caller set the canonicalize flag (or requested an enterprise
     * principal) and we requested (and received) a TGT.
     */
    canon_req = ((request->kdc_options & KDC_OPT_CANONICALIZE) != 0) ||
        request->client->type == KRB5_NT_ENTERPRISE_PRINCIPAL ||
        (request->kdc_options & KDC_OPT_REQUEST_ANONYMOUS);
    if (canon_req) {
        canon_ok = IS_TGS_PRINC(request->server) &&
            IS_TGS_PRINC(as_reply->enc_part2->server);
    } else
        canon_ok = 0;

    if ((!canon_ok &&
         !krb5_principal_compare(context, as_reply->enc_part2->server, request->server))
        || (!canon_req && !krb5_principal_compare(context, as_reply->client, request->client))
        || !krb5_principal_compare(context, as_reply->enc_part2->server, as_reply->ticket->server)
        || (request->nonce != as_reply->enc_part2->nonce)
        /* XXX check for extraneous flags */
        /* XXX || (!krb5_addresses_compare(context, addrs, as_reply->enc_part2->caddrs)) */
        || ((request->kdc_options & KDC_OPT_POSTDATED) &&
            (request->from != 0) &&
            (request->from != as_reply->enc_part2->times.starttime))
        || ((request->till != 0) &&
            ts_after(as_reply->enc_part2->times.endtime, request->till))
        || ((request->kdc_options & KDC_OPT_RENEWABLE) &&
            (request->rtime != 0) &&
            ts_after(as_reply->enc_part2->times.renew_till, request->rtime))
        || ((request->kdc_options & KDC_OPT_RENEWABLE_OK) &&
            !(request->kdc_options & KDC_OPT_RENEWABLE) &&
            (as_reply->enc_part2->flags & KDC_OPT_RENEWABLE) &&
            (request->till != 0) &&
            ts_after(as_reply->enc_part2->times.renew_till, request->till))
    ) {
        return KRB5_KDCREP_MODIFIED;
    }

    if (context->library_options & KRB5_LIBOPT_SYNC_KDCTIME) {
        time_offset = ts_delta(as_reply->enc_part2->times.authtime, time_now);
        retval = krb5_set_time_offsets(context, time_offset, 0);
        if (retval)
            return retval;
    } else {
        if ((request->from == 0) &&
            !ts_within(as_reply->enc_part2->times.starttime, time_now,
                       context->clockskew))
            return (KRB5_KDCREP_SKEW);
    }
    return 0;
}

static krb5_error_code
stash_as_reply(krb5_context             context,
               krb5_kdc_rep             *as_reply,
               krb5_creds *             creds,
               krb5_ccache              ccache)
{
    krb5_error_code             retval;
    krb5_data *                 packet;
    krb5_principal              client;
    krb5_principal              server;

    client = NULL;
    server = NULL;

    if (!creds->client)
        if ((retval = krb5_copy_principal(context, as_reply->client, &client)))
            goto cleanup;

    if (!creds->server)
        if ((retval = krb5_copy_principal(context, as_reply->enc_part2->server,
                                          &server)))
            goto cleanup;

    /* fill in the credentials */
    if ((retval = krb5_copy_keyblock_contents(context,
                                              as_reply->enc_part2->session,
                                              &creds->keyblock)))
        goto cleanup;

    creds->times = as_reply->enc_part2->times;
    creds->is_skey = FALSE;             /* this is an AS_REQ, so cannot
                                           be encrypted in skey */
    creds->ticket_flags = as_reply->enc_part2->flags;
    if ((retval = krb5_copy_addresses(context, as_reply->enc_part2->caddrs,
                                      &creds->addresses)))
        goto cleanup;

    creds->second_ticket.length = 0;
    creds->second_ticket.data = 0;

    if ((retval = encode_krb5_ticket(as_reply->ticket, &packet)))
        goto cleanup;

    creds->ticket = *packet;
    free(packet);

    /* store it in the ccache! */
    if (ccache)
        if ((retval = krb5_cc_store_cred(context, ccache, creds)))
            goto cleanup;

    if (!creds->client)
        creds->client = client;
    if (!creds->server)
        creds->server = server;

cleanup:
    if (retval) {
        if (client)
            krb5_free_principal(context, client);
        if (server)
            krb5_free_principal(context, server);
        if (creds->keyblock.contents) {
            memset(creds->keyblock.contents, 0,
                   creds->keyblock.length);
            free(creds->keyblock.contents);
            creds->keyblock.contents = 0;
            creds->keyblock.length = 0;
        }
        if (creds->ticket.data) {
            free(creds->ticket.data);
            creds->ticket.data = 0;
        }
        if (creds->addresses) {
            krb5_free_addresses(context, creds->addresses);
            creds->addresses = 0;
        }
    }
    return (retval);
}

static krb5_error_code
make_preauth_list(krb5_context  context,
                  krb5_preauthtype *    ptypes,
                  int                   nptypes,
                  krb5_pa_data ***      ret_list)
{
    krb5_preauthtype *          ptypep;
    krb5_pa_data **             preauthp;
    int                         i;

    if (nptypes < 0) {
        for (nptypes=0, ptypep = ptypes; *ptypep; ptypep++, nptypes++)
            ;
    }

    /* allocate space for a NULL to terminate the list */

    if ((preauthp =
         (krb5_pa_data **) malloc((nptypes+1)*sizeof(krb5_pa_data *))) == NULL)
        return(ENOMEM);

    for (i=0; i<nptypes; i++) {
        if ((preauthp[i] =
             (krb5_pa_data *) malloc(sizeof(krb5_pa_data))) == NULL) {
            for (; i>=0; i--)
                free(preauthp[i]);
            free(preauthp);
            return (ENOMEM);
        }
        preauthp[i]->magic = KV5M_PA_DATA;
        preauthp[i]->pa_type = ptypes[i];
        preauthp[i]->length = 0;
        preauthp[i]->contents = 0;
    }

    /* fill in the terminating NULL */

    preauthp[nptypes] = NULL;

    *ret_list = preauthp;
    return 0;
}

#define MAX_IN_TKT_LOOPS 16

/* Sort a pa_data sequence so that types named in the "preferred_preauth_types"
 * libdefaults entry are listed before any others. */
static krb5_error_code
sort_krb5_padata_sequence(krb5_context context, krb5_data *realm,
                          krb5_pa_data **padata)
{
    int i, j, base;
    krb5_error_code ret;
    const char *p;
    long l;
    char *q, *preauth_types = NULL;
    krb5_pa_data *tmp;
    int need_free_string = 1;

    if ((padata == NULL) || (padata[0] == NULL)) {
        return 0;
    }

    ret = krb5int_libdefault_string(context, realm, KRB5_CONF_PREFERRED_PREAUTH_TYPES,
                                    &preauth_types);
    if ((ret != 0) || (preauth_types == NULL)) {
        /* Try to use PKINIT first. */
        preauth_types = "17, 16, 15, 14";
        need_free_string = 0;
    }

#ifdef DEBUG
    fprintf (stderr, "preauth data types before sorting:");
    for (i = 0; padata[i]; i++) {
        fprintf (stderr, " %d", padata[i]->pa_type);
    }
    fprintf (stderr, "\n");
#endif

    base = 0;
    for (p = preauth_types; *p != '\0';) {
        /* skip whitespace to find an entry */
        p += strspn(p, ", ");
        if (*p != '\0') {
            /* see if we can extract a number */
            l = strtol(p, &q, 10);
            if ((q != NULL) && (q > p)) {
                /* got a valid number; search for a matching entry */
                for (i = base; padata[i] != NULL; i++) {
                    /* bubble the matching entry to the front of the list */
                    if (padata[i]->pa_type == l) {
                        tmp = padata[i];
                        for (j = i; j > base; j--)
                            padata[j] = padata[j - 1];
                        padata[base] = tmp;
                        base++;
                        break;
                    }
                }
                p = q;
            } else {
                break;
            }
        }
    }
    if (need_free_string)
        free(preauth_types);

#ifdef DEBUG
    fprintf (stderr, "preauth data types after sorting:");
    for (i = 0; padata[i]; i++)
        fprintf (stderr, " %d", padata[i]->pa_type);
    fprintf (stderr, "\n");
#endif

    return 0;
}

static krb5_error_code
build_in_tkt_name(krb5_context context,
                  const char *in_tkt_service,
                  krb5_const_principal client,
                  krb5_principal *server_out)
{
    krb5_error_code ret;
    krb5_principal server = NULL;

    *server_out = NULL;

    if (in_tkt_service) {
        ret = krb5_parse_name_flags(context, in_tkt_service,
                                    KRB5_PRINCIPAL_PARSE_IGNORE_REALM,
                                    &server);
        if (ret)
            return ret;
        krb5_free_data_contents(context, &server->realm);
        ret = krb5int_copy_data_contents(context, &client->realm,
                                         &server->realm);
        if (ret) {
            krb5_free_principal(context, server);
            return ret;
        }
    } else {
        ret = krb5_build_principal_ext(context, &server,
                                       client->realm.length,
                                       client->realm.data,
                                       KRB5_TGS_NAME_SIZE,
                                       KRB5_TGS_NAME,
                                       client->realm.length,
                                       client->realm.data,
                                       0);
        if (ret)
            return ret;
    }

    *server_out = server;
    return 0;
}

void KRB5_CALLCONV
krb5_init_creds_free(krb5_context context,
                     krb5_init_creds_context ctx)
{
    if (ctx == NULL)
        return;

    k5_response_items_free(ctx->rctx.items);
    free(ctx->in_tkt_service);
    zapfree(ctx->gakpw.storage.data, ctx->gakpw.storage.length);
    k5_preauth_request_context_fini(context, ctx);
    krb5_free_error(context, ctx->err_reply);
    krb5_free_pa_data(context, ctx->err_padata);
    krb5_free_cred_contents(context, &ctx->cred);
    krb5_free_kdc_req(context, ctx->request);
    krb5_free_kdc_rep(context, ctx->reply);
    krb5_free_data(context, ctx->outer_request_body);
    krb5_free_data(context, ctx->inner_request_body);
    krb5_free_data(context, ctx->encoded_previous_request);
    krb5int_fast_free_state(context, ctx->fast_state);
    krb5_free_pa_data(context, ctx->optimistic_padata);
    krb5_free_pa_data(context, ctx->method_padata);
    krb5_free_pa_data(context, ctx->more_padata);
    krb5_free_data_contents(context, &ctx->salt);
    krb5_free_data_contents(context, &ctx->s2kparams);
    krb5_free_keyblock_contents(context, &ctx->as_key);
    k5_json_release(ctx->cc_config_in);
    k5_json_release(ctx->cc_config_out);
    free(ctx);
}

krb5_error_code
k5_init_creds_get(krb5_context context, krb5_init_creds_context ctx,
                  krb5_boolean use_primary, struct kdclist *kdcs)
{
    krb5_error_code code;
    krb5_data request;
    krb5_data reply;
    krb5_data realm;
    unsigned int flags = 0;
    int no_udp = 0;

    request.length = 0;
    request.data = NULL;
    reply.length = 0;
    reply.data = NULL;
    realm.length = 0;
    realm.data = NULL;

    for (;;) {
        code = krb5_init_creds_step(context,
                                    ctx,
                                    &reply,
                                    &request,
                                    &realm,
                                    &flags);
        if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG && !no_udp) {
            TRACE_INIT_CREDS_RETRY_TCP(context);
            no_udp = 1;
        } else if (code != 0 || !(flags & KRB5_INIT_CREDS_STEP_FLAG_CONTINUE))
            break;

        krb5_free_data_contents(context, &reply);

        code = k5_sendto_kdc(context, &request, &realm, use_primary, no_udp,
                             &reply, kdcs);
        if (code != 0)
            break;

        krb5_free_data_contents(context, &request);
        krb5_free_data_contents(context, &realm);
    }

    krb5_free_data_contents(context, &request);
    krb5_free_data_contents(context, &reply);
    krb5_free_data_contents(context, &realm);

    return code;
}

/* Heimdal API */
krb5_error_code KRB5_CALLCONV
krb5_init_creds_get(krb5_context context,
                    krb5_init_creds_context ctx)
{
    return k5_init_creds_get(context, ctx, FALSE, NULL);
}

krb5_error_code KRB5_CALLCONV
krb5_init_creds_get_creds(krb5_context context,
                          krb5_init_creds_context ctx,
                          krb5_creds *creds)
{
    if (!ctx->complete)
        return KRB5_NO_TKT_SUPPLIED;

    return k5_copy_creds_contents(context, &ctx->cred, creds);
}

krb5_error_code KRB5_CALLCONV
krb5_init_creds_get_times(krb5_context context,
                          krb5_init_creds_context ctx,
                          krb5_ticket_times *times)
{
    if (!ctx->complete)
        return KRB5_NO_TKT_SUPPLIED;

    *times = ctx->cred.times;

    return 0;
}

krb5_error_code KRB5_CALLCONV
krb5_init_creds_get_error(krb5_context context,
                          krb5_init_creds_context ctx,
                          krb5_error **error)
{
    krb5_error_code code;
    krb5_error *ret = NULL;

    *error = NULL;

    if (ctx->err_reply == NULL)
        return 0;

    ret = k5alloc(sizeof(*ret), &code);
    if (code != 0)
        goto cleanup;

    ret->magic = KV5M_ERROR;
    ret->ctime = ctx->err_reply->ctime;
    ret->cusec = ctx->err_reply->cusec;
    ret->susec = ctx->err_reply->susec;
    ret->stime = ctx->err_reply->stime;
    ret->error = ctx->err_reply->error;

    if (ctx->err_reply->client != NULL) {
        code = krb5_copy_principal(context, ctx->err_reply->client,
                                   &ret->client);
        if (code != 0)
            goto cleanup;
    }

    code = krb5_copy_principal(context, ctx->err_reply->server, &ret->server);
    if (code != 0)
        goto cleanup;

    code = krb5int_copy_data_contents(context, &ctx->err_reply->text,
                                      &ret->text);
    if (code != 0)
        goto cleanup;

    code = krb5int_copy_data_contents(context, &ctx->err_reply->e_data,
                                      &ret->e_data);
    if (code != 0)
        goto cleanup;

    *error = ret;

cleanup:
    if (code != 0)
        krb5_free_error(context, ret);

    return code;
}

/* Return the current time, possibly using the offset from a previously
 * received preauth-required error. */
krb5_error_code
k5_init_creds_current_time(krb5_context context, krb5_init_creds_context ctx,
                           krb5_boolean allow_unauth, krb5_timestamp *time_out,
                           krb5_int32 *usec_out)
{
    if (ctx->pa_offset_state != NO_OFFSET &&
        (allow_unauth || ctx->pa_offset_state == AUTH_OFFSET) &&
        (context->library_options & KRB5_LIBOPT_SYNC_KDCTIME)) {
        /* Use the offset we got from a preauth-required error. */
        return k5_time_with_offset(ctx->pa_offset, ctx->pa_offset_usec,
                                   time_out, usec_out);
    } else {
        /* Use the time offset from the context, or no offset. */
        return krb5_us_timeofday(context, time_out, usec_out);
    }
}

/* Set the timestamps for ctx->request based on the desired lifetimes. */
static krb5_error_code
set_request_times(krb5_context context, krb5_init_creds_context ctx)
{
    krb5_error_code code;
    krb5_timestamp from, now;
    krb5_int32 now_ms;

    code = k5_init_creds_current_time(context, ctx, TRUE, &now, &now_ms);
    if (code != 0)
        return code;

    /* Omit request start time unless the caller explicitly asked for one. */
    from = ts_incr(now, ctx->start_time);
    if (ctx->start_time != 0)
        ctx->request->from = from;

    ctx->request->till = ts_incr(from, ctx->tkt_life);

    if (ctx->renew_life > 0) {
        /* Don't ask for a smaller renewable time than the lifetime. */
        ctx->request->rtime = ts_incr(from, ctx->renew_life);
        if (ts_after(ctx->request->till, ctx->request->rtime))
            ctx->request->rtime = ctx->request->till;
        ctx->request->kdc_options &= ~KDC_OPT_RENEWABLE_OK;
    } else {
        ctx->request->rtime = 0;
    }

    return 0;
}

static void
read_allowed_preauth_type(krb5_context context, krb5_init_creds_context ctx)
{
    krb5_error_code ret;
    krb5_data config;
    char *tmp, *p;
    krb5_ccache in_ccache = k5_gic_opt_get_in_ccache(ctx->opt);

    ctx->allowed_preauth_type = KRB5_PADATA_NONE;
    if (in_ccache == NULL)
        return;
    memset(&config, 0, sizeof(config));
    if (krb5_cc_get_config(context, in_ccache, ctx->request->server,
                           KRB5_CC_CONF_PA_TYPE, &config) != 0)
        return;
    tmp = k5memdup0(config.data, config.length, &ret);
    krb5_free_data_contents(context, &config);
    if (tmp == NULL)
        return;
    ctx->allowed_preauth_type = strtol(tmp, &p, 10);
    if (p == NULL || *p != '\0')
        ctx->allowed_preauth_type = KRB5_PADATA_NONE;
    free(tmp);
}

/* Return true if encrypted timestamp is disabled for realm. */
static krb5_boolean
encts_disabled(profile_t profile, const krb5_data *realm)
{
    krb5_error_code ret;
    char *realmstr;
    int bval;

    realmstr = k5memdup0(realm->data, realm->length, &ret);
    if (realmstr == NULL)
        return FALSE;
    ret = profile_get_boolean(profile, KRB5_CONF_REALMS, realmstr,
                              KRB5_CONF_DISABLE_ENCRYPTED_TIMESTAMP, FALSE,
                              &bval);
    free(realmstr);
    return (ret == 0) ? bval : FALSE;
}

/**
 * Throw away any pre-authentication realm state and begin with a
 * unauthenticated or optimistically authenticated request.  If fast_upgrade is
 * set, use FAST for this request.
 */
static krb5_error_code
restart_init_creds_loop(krb5_context context, krb5_init_creds_context ctx,
                        krb5_boolean fast_upgrade)
{
    krb5_error_code code = 0;

    krb5_free_pa_data(context, ctx->optimistic_padata);
    krb5_free_pa_data(context, ctx->method_padata);
    krb5_free_pa_data(context, ctx->more_padata);
    krb5_free_pa_data(context, ctx->err_padata);
    krb5_free_error(context, ctx->err_reply);
    ctx->optimistic_padata = ctx->method_padata = ctx->more_padata = NULL;
    ctx->err_padata = NULL;
    ctx->err_reply = NULL;
    ctx->selected_preauth_type = KRB5_PADATA_NONE;

    krb5int_fast_free_state(context, ctx->fast_state);
    ctx->fast_state = NULL;
    code = krb5int_fast_make_state(context, &ctx->fast_state);
    if (code != 0)
        goto cleanup;
    if (fast_upgrade)
        ctx->fast_state->fast_state_flags |= KRB5INT_FAST_DO_FAST;

    k5_preauth_request_context_fini(context, ctx);
    k5_preauth_request_context_init(context, ctx);
    krb5_free_data(context, ctx->outer_request_body);
    ctx->outer_request_body = NULL;
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_PREAUTH_LIST) {
        code = make_preauth_list(context, ctx->opt->preauth_list,
                                 ctx->opt->preauth_list_length,
                                 &ctx->optimistic_padata);
        if (code)
            goto cleanup;
    }

    /* Never set encts_disabled back to false, so it can't be circumvented with
     * client realm referrals. */
    if (encts_disabled(context->profile, &ctx->request->client->realm))
        ctx->encts_disabled = TRUE;

    krb5_free_principal(context, ctx->request->server);
    ctx->request->server = NULL;

    code = build_in_tkt_name(context, ctx->in_tkt_service,
                             ctx->request->client,
                             &ctx->request->server);
    if (code != 0)
        goto cleanup;

    code = krb5int_fast_as_armor(context, ctx->fast_state, ctx->opt,
                                 ctx->request);
    if (code != 0)
        goto cleanup;
    /* give the preauth plugins a chance to prep the request body */
    k5_preauth_prepare_request(context, ctx->opt, ctx->request);

    code = krb5int_fast_prep_req_body(context, ctx->fast_state,
                                      ctx->request,
                                      &ctx->outer_request_body);
    if (code != 0)
        goto cleanup;

    /* Read the allowed preauth type for this server principal from the input
     * ccache, if the application supplied one. */
    read_allowed_preauth_type(context, ctx);

cleanup:
    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_init_creds_init(krb5_context context,
                     krb5_principal client,
                     krb5_prompter_fct prompter,
                     void *data,
                     krb5_deltat start_time,
                     krb5_get_init_creds_opt *opt,
                     krb5_init_creds_context *pctx)
{
    krb5_error_code code;
    krb5_init_creds_context ctx;
    int tmp;
    char *str = NULL;

    TRACE_INIT_CREDS(context, client);

    ctx = k5alloc(sizeof(*ctx), &code);
    if (code != 0)
        goto cleanup;

    ctx->request = k5alloc(sizeof(krb5_kdc_req), &code);
    if (code != 0)
        goto cleanup;
    ctx->info_pa_permitted = TRUE;
    code = krb5_copy_principal(context, client, &ctx->request->client);
    if (code != 0)
        goto cleanup;

    ctx->prompter = prompter;
    ctx->prompter_data = data;
    ctx->gak_fct = krb5_get_as_key_password;
    ctx->gak_data = &ctx->gakpw;

    ctx->start_time = start_time;

    if (opt == NULL) {
        ctx->opt = &ctx->opt_storage;
        krb5_get_init_creds_opt_init(ctx->opt);
    } else {
        ctx->opt = opt;
    }

    code = k5_response_items_new(&ctx->rctx.items);
    if (code != 0)
        goto cleanup;

    /* Initialise request parameters as per krb5_get_init_creds() */
    ctx->request->kdc_options = context->kdc_default_options;

    /* forwardable */
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_FORWARDABLE)
        tmp = ctx->opt->forwardable;
    else if (krb5int_libdefault_boolean(context, &ctx->request->client->realm,
                                        KRB5_CONF_FORWARDABLE, &tmp) == 0)
        ;
    else
        tmp = 0;
    if (tmp)
        ctx->request->kdc_options |= KDC_OPT_FORWARDABLE;

    /* proxiable */
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_PROXIABLE)
        tmp = ctx->opt->proxiable;
    else if (krb5int_libdefault_boolean(context, &ctx->request->client->realm,
                                        KRB5_CONF_PROXIABLE, &tmp) == 0)
        ;
    else
        tmp = 0;
    if (tmp)
        ctx->request->kdc_options |= KDC_OPT_PROXIABLE;

    /* canonicalize */
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_CANONICALIZE)
        tmp = 1;
    else if (krb5int_libdefault_boolean(context, &ctx->request->client->realm,
                                        KRB5_CONF_CANONICALIZE, &tmp) == 0)
        ;
    else
        tmp = 0;
    if (tmp)
        ctx->request->kdc_options |= KDC_OPT_CANONICALIZE;

    /* allow_postdate */
    if (ctx->start_time > 0)
        ctx->request->kdc_options |= KDC_OPT_ALLOW_POSTDATE | KDC_OPT_POSTDATED;

    /* ticket lifetime */
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_TKT_LIFE)
        ctx->tkt_life = ctx->opt->tkt_life;
    else if (krb5int_libdefault_string(context, &ctx->request->client->realm,
                                       KRB5_CONF_TICKET_LIFETIME, &str) == 0) {
        code = krb5_string_to_deltat(str, &ctx->tkt_life);
        if (code != 0)
            goto cleanup;
        free(str);
        str = NULL;
    } else
        ctx->tkt_life = 24 * 60 * 60; /* previously hardcoded in kinit */

    /* renewable lifetime */
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_RENEW_LIFE)
        ctx->renew_life = ctx->opt->renew_life;
    else if (krb5int_libdefault_string(context, &ctx->request->client->realm,
                                       KRB5_CONF_RENEW_LIFETIME, &str) == 0) {
        code = krb5_string_to_deltat(str, &ctx->renew_life);
        if (code != 0)
            goto cleanup;
        free(str);
        str = NULL;
    } else
        ctx->renew_life = 0;

    if (ctx->renew_life > 0)
        ctx->request->kdc_options |= KDC_OPT_RENEWABLE;

    /* enctypes */
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_ETYPE_LIST) {
        ctx->request->ktype =
            k5memdup(ctx->opt->etype_list,
                     ctx->opt->etype_list_length * sizeof(krb5_enctype),
                     &code);
        if (code != 0)
            goto cleanup;
        ctx->request->nktypes = ctx->opt->etype_list_length;
    } else if (krb5_get_default_in_tkt_ktypes(context,
                                              &ctx->request->ktype) == 0) {
        ctx->request->nktypes = k5_count_etypes(ctx->request->ktype);
    } else {
        /* there isn't any useful default here. */
        code = KRB5_CONFIG_ETYPE_NOSUPP;
        goto cleanup;
    }

    /*
     * Set a default enctype for optimistic preauth.  If we're not doing
     * optimistic preauth, this should ordinarily get overwritten when we
     * process the etype-info2 of the preauth-required error.
     */
    if (ctx->request->nktypes > 0)
        ctx->etype = ctx->request->ktype[0];

    /* addresses */
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_ADDRESS_LIST) {
        code = krb5_copy_addresses(context, ctx->opt->address_list,
                                   &ctx->request->addresses);
        if (code != 0)
            goto cleanup;
    } else if (krb5int_libdefault_boolean(context, &ctx->request->client->realm,
                                          KRB5_CONF_NOADDRESSES, &tmp) != 0
               || tmp) {
        ctx->request->addresses = NULL;
    } else {
        code = krb5_os_localaddr(context, &ctx->request->addresses);
        if (code != 0)
            goto cleanup;
    }

    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_SALT) {
        code = krb5int_copy_data_contents(context, ctx->opt->salt, &ctx->salt);
        if (code != 0)
            goto cleanup;
        ctx->default_salt = FALSE;
    } else {
        ctx->salt = empty_data();
        ctx->default_salt = TRUE;
    }

    /* Anonymous. */
    if (ctx->opt->flags & KRB5_GET_INIT_CREDS_OPT_ANONYMOUS) {
        ctx->request->kdc_options |= KDC_OPT_REQUEST_ANONYMOUS;
        /* Remap @REALM to WELLKNOWN/ANONYMOUS@REALM. */
        if (client->length == 1 && client->data[0].length ==0) {
            krb5_principal new_client;
            code = krb5_build_principal_ext(context, &new_client,
                                            client->realm.length,
                                            client->realm.data,
                                            strlen(KRB5_WELLKNOWN_NAMESTR),
                                            KRB5_WELLKNOWN_NAMESTR,
                                            strlen(KRB5_ANONYMOUS_PRINCSTR),
                                            KRB5_ANONYMOUS_PRINCSTR,
                                            0);
            if (code)
                goto cleanup;
            krb5_free_principal(context, ctx->request->client);
            ctx->request->client = new_client;
            ctx->request->client->type = KRB5_NT_WELLKNOWN;
        }
    }
    /* We will also handle anonymous if the input principal is the anonymous
     * principal. */
    if (krb5_principal_compare_any_realm(context, ctx->request->client,
                                         krb5_anonymous_principal())) {
        ctx->request->kdc_options |= KDC_OPT_REQUEST_ANONYMOUS;
        ctx->request->client->type = KRB5_NT_WELLKNOWN;
    }

    *pctx = ctx;
    ctx = NULL;

cleanup:
    krb5_init_creds_free(context, ctx);
    free(str);

    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_init_creds_set_service(krb5_context context,
                            krb5_init_creds_context ctx,
                            const char *service)
{
    char *s;

    TRACE_INIT_CREDS_SERVICE(context, service);

    s = strdup(service);
    if (s == NULL)
        return ENOMEM;

    free(ctx->in_tkt_service);
    ctx->in_tkt_service = s;

    return restart_init_creds_loop(context, ctx, FALSE);
}

static krb5_error_code
init_creds_validate_reply(krb5_context context,
                          krb5_init_creds_context ctx,
                          krb5_data *reply)
{
    krb5_error_code code;
    krb5_error *error = NULL;
    krb5_kdc_rep *as_reply = NULL;

    krb5_free_error(context, ctx->err_reply);
    ctx->err_reply = NULL;

    krb5_free_kdc_rep(context, ctx->reply);
    ctx->reply = NULL;

    if (krb5_is_krb_error(reply)) {
        code = decode_krb5_error(reply, &error);
        if (code != 0)
            return code;

        assert(error != NULL);

        TRACE_INIT_CREDS_ERROR_REPLY(context,
                                     error->error + ERROR_TABLE_BASE_krb5);
        if (error->error == KRB_ERR_RESPONSE_TOO_BIG) {
            krb5_free_error(context, error);
            return KRB5KRB_ERR_RESPONSE_TOO_BIG;
        } else {
            ctx->err_reply = error;
            return 0;
        }
    }

    /*
     * Check to make sure it isn't a V4 reply.
     */
    if (reply->length != 0 && !krb5_is_as_rep(reply)) {
/* these are in <kerberosIV/prot.h> as well but it isn't worth including. */
#define V4_KRB_PROT_VERSION     4
#define V4_AUTH_MSG_ERR_REPLY   (5<<1)
        /* check here for V4 reply */
        unsigned int t_switch;

        /* From v4 g_in_tkt.c: This used to be
           switch (pkt_msg_type(rpkt) & ~1) {
           but SCO 3.2v4 cc compiled that incorrectly.  */
        t_switch = reply->data[1];
        t_switch &= ~1;

        if (t_switch == V4_AUTH_MSG_ERR_REPLY
            && reply->data[0] == V4_KRB_PROT_VERSION) {
            code = KRB5KRB_AP_ERR_V4_REPLY;
        } else {
            code = KRB5KRB_AP_ERR_MSG_TYPE;
        }
        return code;
    }

    /* It must be a KRB_AS_REP message, or an bad returned packet */
    code = decode_krb5_as_rep(reply, &as_reply);
    if (code != 0)
        return code;

    if (as_reply->msg_type != KRB5_AS_REP) {
        krb5_free_kdc_rep(context, as_reply);
        return KRB5KRB_AP_ERR_MSG_TYPE;
    }

    ctx->reply = as_reply;

    return 0;
}

static krb5_error_code
save_selected_preauth_type(krb5_context context, krb5_ccache ccache,
                           krb5_init_creds_context ctx)
{
    krb5_data config_data;
    char *tmp;
    krb5_error_code code;

    if (ctx->selected_preauth_type == KRB5_PADATA_NONE)
        return 0;
    if (asprintf(&tmp, "%ld", (long)ctx->selected_preauth_type) < 0)
        return ENOMEM;
    config_data = string2data(tmp);
    code = krb5_cc_set_config(context, ccache, ctx->cred.server,
                              KRB5_CC_CONF_PA_TYPE, &config_data);
    free(tmp);
    return code;
}

static krb5_error_code
clear_cc_config_out_data(krb5_context context, krb5_init_creds_context ctx)
{
    k5_json_release(ctx->cc_config_out);
    ctx->cc_config_out = NULL;
    return k5_json_object_create(&ctx->cc_config_out);
}

static krb5_error_code
read_cc_config_in_data(krb5_context context, krb5_init_creds_context ctx)
{
    k5_json_value val;
    krb5_data config;
    char *encoded;
    krb5_error_code code;
    krb5_ccache in_ccache = k5_gic_opt_get_in_ccache(ctx->opt);

    k5_json_release(ctx->cc_config_in);
    ctx->cc_config_in = NULL;

    if (in_ccache == NULL)
        return 0;

    memset(&config, 0, sizeof(config));
    code = krb5_cc_get_config(context, in_ccache, ctx->request->server,
                              KRB5_CC_CONF_PA_CONFIG_DATA, &config);
    if (code)
        return code;

    encoded = k5memdup0(config.data, config.length, &code);
    krb5_free_data_contents(context, &config);
    if (encoded == NULL)
        return ENOMEM;

    code = k5_json_decode(encoded, &val);
    free(encoded);
    if (code)
        return code;
    if (k5_json_get_tid(val) != K5_JSON_TID_OBJECT) {
        k5_json_release(val);
        return EINVAL;
    }
    ctx->cc_config_in = val;
    return 0;
}

static krb5_error_code
save_cc_config_out_data(krb5_context context, krb5_ccache ccache,
                        krb5_init_creds_context ctx)
{
    krb5_data config;
    char *encoded;
    krb5_error_code code;

    if (ctx->cc_config_out == NULL ||
        k5_json_object_count(ctx->cc_config_out) == 0)
        return 0;
    code = k5_json_encode(ctx->cc_config_out, &encoded);
    if (code)
        return code;
    config = string2data(encoded);
    code = krb5_cc_set_config(context, ccache, ctx->cred.server,
                              KRB5_CC_CONF_PA_CONFIG_DATA, &config);
    free(encoded);
    return code;
}

/* Add a KERB-PA-PAC-REQUEST pa-data item if the gic options require one. */
static krb5_error_code
maybe_add_pac_request(krb5_context context, krb5_init_creds_context ctx)
{
    krb5_error_code code;
    krb5_pa_pac_req pac_req;
    krb5_data *encoded;
    int val;

    val = k5_gic_opt_pac_request(ctx->opt);
    if (val == -1)
        return 0;

    pac_req.include_pac = val;
    code = encode_krb5_pa_pac_req(&pac_req, &encoded);
    if (code)
        return code;
    code = k5_add_pa_data_from_data(&ctx->request->padata,
                                    KRB5_PADATA_PAC_REQUEST, encoded);
    krb5_free_data(context, encoded);
    return code;
}

static krb5_error_code
init_creds_step_request(krb5_context context,
                        krb5_init_creds_context ctx,
                        krb5_data *out)
{
    krb5_error_code code;
    krb5_preauthtype pa_type;
    krb5_data copy;
    struct errinfo save = EMPTY_ERRINFO;
    uint32_t rcode = (ctx->err_reply == NULL) ? 0 : ctx->err_reply->error;

    if (ctx->loopcount >= MAX_IN_TKT_LOOPS) {
        code = KRB5_GET_IN_TKT_LOOP;
        goto cleanup;
    }

    /* RFC 6113 requires a new nonce for the inner request on each try. */
    code = k5_generate_nonce(context, &ctx->request->nonce);
    if (code != 0)
        goto cleanup;

    /* Reset the request timestamps, possibly adjusting to the KDC time. */
    code = set_request_times(context, ctx);
    if (code != 0)
        goto cleanup;

    krb5_free_data(context, ctx->inner_request_body);
    ctx->inner_request_body = NULL;
    code = encode_krb5_kdc_req_body(ctx->request, &ctx->inner_request_body);
    if (code)
        goto cleanup;

    /*
     * Read cached preauth configuration data for this server principal from
     * the in_ccache, if the application supplied one, and delete any that was
     * stored by a previous (clearly failed) module.
     */
    read_cc_config_in_data(context, ctx);
    clear_cc_config_out_data(context, ctx);

    ctx->request->padata = NULL;
    if (ctx->optimistic_padata != NULL) {
        /* Our first attempt, using an optimistic padata list. */
        TRACE_INIT_CREDS_PREAUTH_OPTIMISTIC(context);
        code = k5_preauth(context, ctx, ctx->optimistic_padata, TRUE,
                          &ctx->request->padata, &ctx->selected_preauth_type);
        krb5_free_pa_data(context, ctx->optimistic_padata);
        ctx->optimistic_padata = NULL;
        if (code) {
            /* Make an unauthenticated request. */
            krb5_clear_error_message(context);
            code = 0;
        }
    } else if (ctx->more_padata != NULL) {
        /* Continuing after KDC_ERR_MORE_PREAUTH_DATA_REQUIRED. */
        TRACE_INIT_CREDS_PREAUTH_MORE(context, ctx->selected_preauth_type);
        code = k5_preauth(context, ctx, ctx->more_padata, TRUE,
                          &ctx->request->padata, &pa_type);
    } else if (rcode == KDC_ERR_PREAUTH_FAILED) {
        /* Report the KDC-side failure code if we can't try another mech. */
        code = KRB5KDC_ERR_PREAUTH_FAILED;
    } else if (rcode && rcode != KDC_ERR_PREAUTH_REQUIRED) {
        /* Retrying after an error (possibly mechanism-specific), using error
         * padata to figure out what to change. */
        TRACE_INIT_CREDS_PREAUTH_TRYAGAIN(context, ctx->err_reply->error,
                                          ctx->selected_preauth_type);
        code = k5_preauth_tryagain(context, ctx, ctx->selected_preauth_type,
                                   ctx->err_reply, ctx->err_padata,
                                   &ctx->request->padata);
        if (code) {
            krb5_clear_error_message(context);
            code = ctx->err_reply->error + ERROR_TABLE_BASE_krb5;
        }
    }
    /* 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);
        ctx->selected_preauth_type = KRB5_PADATA_NONE;
    }

    if (ctx->request->padata == NULL && ctx->method_padata != NULL) {
        /* Retrying after KDC_ERR_PREAUTH_REQUIRED, or trying again with a
         * different mechanism after a failure. */
        TRACE_INIT_CREDS_PREAUTH(context);
        code = k5_preauth(context, ctx, ctx->method_padata, TRUE,
                          &ctx->request->padata, &ctx->selected_preauth_type);
        if (code) {
            if (save.code != 0)
                code = k5_restore_ctx_error(context, &save);
            goto cleanup;
        }
    }
    if (ctx->request->padata == NULL)
        TRACE_INIT_CREDS_PREAUTH_NONE(context);

    /* Remember when we sent this request (after any preauth delay). */
    ctx->request_time = time(NULL);

    if (ctx->encoded_previous_request != NULL) {
        krb5_free_data(context, ctx->encoded_previous_request);
        ctx->encoded_previous_request = NULL;
    }
    if (ctx->info_pa_permitted) {
        code = k5_add_empty_pa_data(&ctx->request->padata,
                                    KRB5_PADATA_AS_FRESHNESS);
        if (code)
            goto cleanup;
        code = k5_add_empty_pa_data(&ctx->request->padata,
                                    KRB5_ENCPADATA_REQ_ENC_PA_REP);
    }
    if (code)
        goto cleanup;

    if (ctx->subject_cert != NULL) {
        code = krb5int_copy_data_contents(context, ctx->subject_cert, &copy);
        if (code)
            goto cleanup;
        code = k5_add_pa_data_from_data(&ctx->request->padata,
                                        KRB5_PADATA_S4U_X509_USER, &copy);
        krb5_free_data_contents(context, &copy);
        if (code)
            goto cleanup;
    }

    code = maybe_add_pac_request(context, ctx);
    if (code)
        goto cleanup;

    code = krb5int_fast_prep_req(context, ctx->fast_state,
                                 ctx->request, ctx->outer_request_body,
                                 encode_krb5_as_req,
                                 &ctx->encoded_previous_request);
    if (code != 0)
        goto cleanup;

    code = krb5int_copy_data_contents(context,
                                      ctx->encoded_previous_request,
                                      out);
    if (code != 0)
        goto cleanup;

cleanup:
    krb5_free_pa_data(context, ctx->request->padata);
    ctx->request->padata = NULL;
    k5_clear_error(&save);
    return code;
}

/* Ensure that the reply enctype was among the requested enctypes. */
static krb5_error_code
check_reply_enctype(krb5_init_creds_context ctx)
{
    int i;

    for (i = 0; i < ctx->request->nktypes; i++) {
        if (ctx->request->ktype[i] == ctx->reply->enc_part.enctype)
            return 0;
    }
    return KRB5_CONFIG_ETYPE_NOSUPP;
}

/* Note the difference between the KDC's time, as reported to us in a
 * preauth-required error, and the current time. */
static void
note_req_timestamp(krb5_context context, krb5_init_creds_context ctx,
                   krb5_timestamp kdc_time, krb5_int32 kdc_usec)
{
    krb5_timestamp now;
    krb5_int32 usec;

    if (k5_time_with_offset(0, 0, &now, &usec) != 0)
        return;
    ctx->pa_offset = ts_delta(kdc_time, now);
    ctx->pa_offset_usec = kdc_usec - usec;
    ctx->pa_offset_state = (ctx->fast_state->armor_key != NULL) ?
        AUTH_OFFSET : UNAUTH_OFFSET;
}

/*
 * Determine whether err is a client referral to another realm, given the
 * previously requested client principal name.
 *
 * RFC 6806 Section 7 requires that KDCs return the referral realm in an error
 * type WRONG_REALM, but Microsoft Windows Server 2003 (and possibly others)
 * return the realm in a PRINCIPAL_UNKNOWN message.
 */
static krb5_boolean
is_referral(krb5_context context, krb5_error *err, krb5_principal client)
{
    if (err->error != KDC_ERR_WRONG_REALM &&
        err->error != KDC_ERR_C_PRINCIPAL_UNKNOWN)
        return FALSE;
    if (err->client == NULL)
        return FALSE;
    return !krb5_realm_compare(context, err->client, client);
}

/* Transfer error padata to method data in ctx and sort it according to
 * configuration. */
static krb5_error_code
accept_method_data(krb5_context context, krb5_init_creds_context ctx)
{
    krb5_free_pa_data(context, ctx->method_padata);
    ctx->method_padata = ctx->err_padata;
    ctx->err_padata = NULL;
    return sort_krb5_padata_sequence(context, &ctx->request->client->realm,
                                     ctx->method_padata);
}

/* Return the password expiry time indicated by enc_part2.  Set *is_last_req
 * if the information came from a last_req value. */
static void
get_expiry_times(krb5_enc_kdc_rep_part *enc_part2, krb5_timestamp *pw_exp,
                 krb5_timestamp *acct_exp, krb5_boolean *is_last_req)
{
    krb5_last_req_entry **last_req;
    krb5_int32 lr_type;

    *pw_exp = 0;
    *acct_exp = 0;
    *is_last_req = FALSE;

    /* Look for last-req entries for password or account expiration. */
    if (enc_part2->last_req) {
        for (last_req = enc_part2->last_req; *last_req; last_req++) {
            lr_type = (*last_req)->lr_type;
            if (lr_type == KRB5_LRQ_ALL_PW_EXPTIME ||
                lr_type == KRB5_LRQ_ONE_PW_EXPTIME) {
                *is_last_req = TRUE;
                *pw_exp = (*last_req)->value;
            } else if (lr_type == KRB5_LRQ_ALL_ACCT_EXPTIME ||
                       lr_type == KRB5_LRQ_ONE_ACCT_EXPTIME) {
                *is_last_req = TRUE;
                *acct_exp = (*last_req)->value;
            }
        }
    }

    /* If we didn't find any, use the ambiguous key_exp field. */
    if (*is_last_req == FALSE)
        *pw_exp = enc_part2->key_exp;
}

/*
 * Send an appropriate warning prompter if as_reply indicates that the password
 * is going to expire soon.  If an expire callback was provided, use that
 * instead.
 */
static void
warn_pw_expiry(krb5_context context, krb5_get_init_creds_opt *options,
               krb5_prompter_fct prompter, void *data,
               const char *in_tkt_service, krb5_kdc_rep *as_reply)
{
    krb5_error_code ret;
    krb5_expire_callback_func expire_cb;
    void *expire_data;
    krb5_timestamp pw_exp, acct_exp, now;
    krb5_boolean is_last_req;
    uint32_t interval;
    char ts[256], banner[1024];

    if (as_reply == NULL || as_reply->enc_part2 == NULL)
        return;

    get_expiry_times(as_reply->enc_part2, &pw_exp, &acct_exp, &is_last_req);

    k5_gic_opt_get_expire_cb(options, &expire_cb, &expire_data);
    if (expire_cb != NULL) {
        /* Invoke the expire callback and don't send prompter warnings. */
        (*expire_cb)(context, expire_data, pw_exp, acct_exp, is_last_req);
        return;
    }

    /* Don't warn if no password expiry value was sent. */
    if (pw_exp == 0)
        return;

    /* Don't warn if the password is being changed. */
    if (in_tkt_service && strcmp(in_tkt_service, "kadmin/changepw") == 0)
        return;

    /*
     * If the expiry time came from a last_req field, assume the KDC wants us
     * to warn.  Otherwise, warn only if the expiry time is less than a week
     * from now.
     */
    ret = krb5_timeofday(context, &now);
    if (ret != 0)
        return;
    interval = ts_interval(now, pw_exp);
    if (!is_last_req && (!interval || interval > 7 * 24 * 60 * 60))
        return;

    if (!prompter)
        return;

    ret = krb5_timestamp_to_string(pw_exp, ts, sizeof(ts));
    if (ret != 0)
        return;

    if (interval < 3600) {
        snprintf(banner, sizeof(banner),
                 _("Warning: Your password will expire in less than one hour "
                   "on %s"), ts);
    } else if (interval < 86400 * 2) {
        snprintf(banner, sizeof(banner),
                 _("Warning: Your password will expire in %d hour%s on %s"),
                 interval / 3600, interval < 7200 ? "" : "s", ts);
    } else {
        snprintf(banner, sizeof(banner),
                 _("Warning: Your password will expire in %d days on %s"),
                 interval / 86400, ts);
    }

    /* PROMPTER_INVOCATION */
    (*prompter)(context, data, 0, banner, 0, 0);
}

/* Display a warning via the prompter if a deprecated enctype was used for
 * either the reply key or the session key. */
static void
warn_deprecated(krb5_context context, krb5_init_creds_context ctx,
                krb5_enctype as_key_enctype)
{
    krb5_enctype etype;
    char encbuf[128], banner[256];

    if (ctx->prompter == NULL)
        return;

    if (krb5int_c_deprecated_enctype(as_key_enctype))
        etype = as_key_enctype;
    else if (krb5int_c_deprecated_enctype(ctx->cred.keyblock.enctype))
        etype = ctx->cred.keyblock.enctype;
    else
        return;

    if (krb5_enctype_to_name(etype, FALSE, encbuf, sizeof(encbuf)) != 0)
        return;
    snprintf(banner, sizeof(banner),
             _("Warning: encryption type %s used for authentication is "
               "deprecated and will be disabled"), encbuf);

    /* PROMPTER_INVOCATION */
    (*ctx->prompter)(context, ctx->prompter_data, NULL, banner, 0, NULL);
}

/*
 * If ctx specifies an output ccache, create or refresh it (atomically, if
 * possible) with the obtained credential and any appropriate ccache
 * configuration.
 */
static krb5_error_code
write_out_ccache(krb5_context context, krb5_init_creds_context ctx,
                 krb5_boolean fast_avail)
{
    krb5_error_code ret;
    krb5_ccache out_ccache = k5_gic_opt_get_out_ccache(ctx->opt);
    krb5_ccache mcc = NULL;
    krb5_data yes = string2data("yes");

    if (out_ccache == NULL)
        return 0;

    ret = krb5_cc_new_unique(context, "MEMORY", NULL, &mcc);
    if (ret)
        goto cleanup;

    ret = krb5_cc_initialize(context, mcc, ctx->cred.client);
    if (ret)
        goto cleanup;

    if (fast_avail) {
        ret = krb5_cc_set_config(context, mcc, ctx->cred.server,
                                 KRB5_CC_CONF_FAST_AVAIL, &yes);
        if (ret)
            goto cleanup;
    }

    ret = save_selected_preauth_type(context, mcc, ctx);
    if (ret)
        goto cleanup;

    ret = save_cc_config_out_data(context, mcc, ctx);
    if (ret)
        goto cleanup;

    ret = k5_cc_store_primary_cred(context, mcc, &ctx->cred);
    if (ret)
        goto cleanup;

    ret = krb5_cc_move(context, mcc, out_ccache);
    if (ret)
        goto cleanup;
    mcc = NULL;

cleanup:
    if (mcc != NULL)
        krb5_cc_destroy(context, mcc);
    return ret;
}

static krb5_error_code
init_creds_step_reply(krb5_context context,
                      krb5_init_creds_context ctx,
                      krb5_data *in)
{
    krb5_error_code code;
    krb5_pa_data **kdc_padata = NULL;
    krb5_preauthtype kdc_pa_type;
    krb5_boolean retry = FALSE;
    int canon_flag = 0;
    uint32_t reply_code;
    krb5_keyblock *strengthen_key = NULL;
    krb5_keyblock encrypting_key;
    krb5_boolean fast_avail;

    encrypting_key.length = 0;
    encrypting_key.contents = NULL;

    /* process previous KDC response */
    code = init_creds_validate_reply(context, ctx, in);
    if (code != 0)
        goto cleanup;

    /* per referrals draft, enterprise principals imply canonicalization */
    canon_flag = ((ctx->request->kdc_options & KDC_OPT_CANONICALIZE) != 0) ||
        ctx->request->client->type == KRB5_NT_ENTERPRISE_PRINCIPAL;

    if (ctx->err_reply != NULL) {
        krb5_free_pa_data(context, ctx->more_padata);
        krb5_free_pa_data(context, ctx->err_padata);
        ctx->more_padata = ctx->err_padata = NULL;
        code = krb5int_fast_process_error(context, ctx->fast_state,
                                          &ctx->err_reply, &ctx->err_padata,
                                          &retry);
        if (code != 0)
            goto cleanup;
        reply_code = ctx->err_reply->error;
        if (!ctx->restarted &&
            k5_upgrade_to_fast_p(context, ctx->fast_state, ctx->err_padata)) {
            /* Retry with FAST after discovering that the KDC supports
             * it.  (FAST negotiation usually avoids this restart.) */
            TRACE_FAST_PADATA_UPGRADE(context);
            ctx->restarted = TRUE;
            code = restart_init_creds_loop(context, ctx, TRUE);
        } else if (!ctx->restarted && reply_code == KDC_ERR_PREAUTH_FAILED &&
                   ctx->selected_preauth_type == KRB5_PADATA_NONE) {
            /* The KDC didn't like our informational padata (probably a pre-1.7
             * MIT krb5 KDC).  Retry without it. */
            ctx->info_pa_permitted = FALSE;
            ctx->restarted = TRUE;
            code = restart_init_creds_loop(context, ctx, FALSE);
        } else if (reply_code == KDC_ERR_PREAUTH_EXPIRED) {
            /* We sent an expired KDC cookie.  Start over, allowing another
             * FAST upgrade. */
            ctx->restarted = FALSE;
            code = restart_init_creds_loop(context, ctx, FALSE);
        } else if (ctx->identify_realm &&
                   (reply_code == KDC_ERR_PREAUTH_REQUIRED ||
                    reply_code == KDC_ERR_KEY_EXP)) {
            /* The client exists in this realm; we can stop. */
            ctx->complete = TRUE;
            goto cleanup;
        } else if (reply_code == KDC_ERR_PREAUTH_REQUIRED && retry) {
            note_req_timestamp(context, ctx, ctx->err_reply->stime,
                               ctx->err_reply->susec);
            code = accept_method_data(context, ctx);
        } else if (reply_code == KDC_ERR_PREAUTH_FAILED && retry) {
            note_req_timestamp(context, ctx, ctx->err_reply->stime,
                               ctx->err_reply->susec);
            /* 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)
                code = accept_method_data(context, ctx);
        } else if (reply_code == KDC_ERR_MORE_PREAUTH_DATA_REQUIRED && retry) {
            ctx->more_padata = ctx->err_padata;
            ctx->err_padata = NULL;
        } else if (canon_flag && is_referral(context, ctx->err_reply,
                                             ctx->request->client)) {
            TRACE_INIT_CREDS_REFERRAL(context, &ctx->err_reply->client->realm);
            /* Rewrite request.client with realm from error reply */
            krb5_free_data_contents(context, &ctx->request->client->realm);
            code = krb5int_copy_data_contents(context,
                                              &ctx->err_reply->client->realm,
                                              &ctx->request->client->realm);
            if (code != 0)
                goto cleanup;
            /* Reset per-realm negotiation state. */
            ctx->restarted = FALSE;
            ctx->info_pa_permitted = TRUE;
            code = restart_init_creds_loop(context, ctx, FALSE);
        } else {
            if (retry && ctx->selected_preauth_type != KRB5_PADATA_NONE) {
                code = 0;
            } else {
                /* error + no hints (or no preauth mech) = give up */
                code = (krb5_error_code)reply_code + ERROR_TABLE_BASE_krb5;
            }
        }

        /* Return error code, or continue with next iteration */
        goto cleanup;
    }

    /* We have a response. Process it. */
    assert(ctx->reply != NULL);

    /* Check for replies (likely forged) with unasked-for enctypes. */
    code = check_reply_enctype(ctx);
    if (code != 0)
        goto cleanup;

    /* process any preauth data in the as_reply */
    code = krb5int_fast_process_response(context, ctx->fast_state,
                                         ctx->reply, &strengthen_key);
    if (code != 0)
        goto cleanup;

    if (ctx->identify_realm) {
        /* Just getting a reply means the client exists in this realm. */
        ctx->complete = TRUE;
        goto cleanup;
    }

    code = sort_krb5_padata_sequence(context, &ctx->request->client->realm,
                                     ctx->reply->padata);
    if (code != 0)
        goto cleanup;

    ctx->etype = ctx->reply->enc_part.enctype;

    /* Process the final reply padata.  Don't restrict the preauth types or
     * record a selected preauth type. */
    ctx->allowed_preauth_type = KRB5_PADATA_NONE;
    code = k5_preauth(context, ctx, ctx->reply->padata, FALSE, &kdc_padata,
                      &kdc_pa_type);
    if (code != 0)
        goto cleanup;

    /*
     * If we haven't gotten a salt from another source yet, set up one
     * corresponding to the client principal returned by the KDC.  We
     * could get the same effect by passing local_as_reply->client to
     * gak_fct below, but that would put the canonicalized client name
     * in the prompt, which raises issues of needing to sanitize
     * unprintable characters.  So for now we just let it affect the
     * salt.  local_as_reply->client will be checked later on in
     * verify_as_reply.
     */
    if (ctx->default_salt) {
        code = krb5_principal2salt(context, ctx->reply->client, &ctx->salt);
        TRACE_INIT_CREDS_SALT_PRINC(context, &ctx->salt);
        if (code != 0)
            goto cleanup;
    }

    code = decrypt_as_reply(context, ctx, strengthen_key, &encrypting_key);
    if (code)
        goto cleanup;
    TRACE_INIT_CREDS_DECRYPTED_REPLY(context, ctx->reply->enc_part2->session);

    code = krb5int_fast_verify_nego(context, ctx->fast_state,
                                    ctx->reply, ctx->encoded_previous_request,
                                    &encrypting_key, &fast_avail);
    if (code)
        goto cleanup;
    code = verify_as_reply(context, ctx->request_time,
                           ctx->request, ctx->reply);
    if (code != 0)
        goto cleanup;
    code = verify_anonymous(context, ctx->request, ctx->reply,
                            &ctx->as_key);
    if (code)
        goto cleanup;

    code = stash_as_reply(context, ctx->reply, &ctx->cred, NULL);
    if (code != 0)
        goto cleanup;
    code = write_out_ccache(context, ctx, fast_avail);
    if (code)
        k5_prependmsg(context, code, _("Failed to store credentials"));

    k5_preauth_request_context_fini(context, ctx);

    /* success */
    ctx->complete = TRUE;
    warn_pw_expiry(context, ctx->opt, ctx->prompter, ctx->prompter_data,
                   ctx->in_tkt_service, ctx->reply);
    warn_deprecated(context, ctx, encrypting_key.enctype);

cleanup:
    krb5_free_pa_data(context, kdc_padata);
    krb5_free_keyblock(context, strengthen_key);
    krb5_free_keyblock_contents(context, &encrypting_key);

    return code;
}

/*
 * Do next step of credentials acquisition.
 *
 * On success returns 0 or KRB5KRB_ERR_RESPONSE_TOO_BIG if the request
 * should be sent with TCP.
 */
krb5_error_code KRB5_CALLCONV
krb5_init_creds_step(krb5_context context,
                     krb5_init_creds_context ctx,
                     krb5_data *in,
                     krb5_data *out,
                     krb5_data *realm,
                     unsigned int *flags)
{
    krb5_error_code code, code2;

    *flags = 0;

    out->data = NULL;
    out->length = 0;

    realm->data = NULL;
    realm->length = 0;

    if (ctx->complete)
        return EINVAL;

    code = k5_preauth_check_context(context, ctx);
    if (code)
        return code;

    if (in->length != 0) {
        code = init_creds_step_reply(context, ctx, in);
        if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG) {
            code2 = krb5int_copy_data_contents(context,
                                               ctx->encoded_previous_request,
                                               out);
            if (code2 != 0) {
                code = code2;
                goto cleanup;
            }
            goto copy_realm;
        }
        if (code != 0 || ctx->complete)
            goto cleanup;
    } else {
        code = restart_init_creds_loop(context, ctx, FALSE);
        if (code)
            goto cleanup;
    }

    code = init_creds_step_request(context, ctx, out);
    if (code != 0)
        goto cleanup;

    /* Only a new request increments the loop count, not a TCP retry */
    ctx->loopcount++;

copy_realm:
    assert(ctx->request->server != NULL);

    code2 = krb5int_copy_data_contents(context,
                                       &ctx->request->server->realm,
                                       realm);
    if (code2 != 0) {
        code = code2;
        goto cleanup;
    }

cleanup:
    if (code == KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN) {
        char *client_name;

        /* See if we can produce a more detailed error message */
        code2 = krb5_unparse_name(context, ctx->request->client, &client_name);
        if (code2 == 0) {
            k5_setmsg(context, code,
                      _("Client '%s' not found in Kerberos database"),
                      client_name);
            krb5_free_unparsed_name(context, client_name);
        }
    }

    *flags = ctx->complete ? 0 : KRB5_INIT_CREDS_STEP_FLAG_CONTINUE;
    return code;
}

static krb5_error_code
try_init_creds(krb5_context context, krb5_creds *creds, krb5_principal client,
               krb5_prompter_fct prompter, void *prompter_data,
               krb5_deltat start_time, const char *in_tkt_service,
               krb5_get_init_creds_opt *options, get_as_key_fn gak_fct,
               void *gak_data, krb5_boolean use_primary, struct kdclist *kdcs,
               krb5_kdc_rep **as_reply)
{
    krb5_error_code code;
    krb5_init_creds_context ctx = NULL;

    code = krb5_init_creds_init(context,
                                client,
                                prompter,
                                prompter_data,
                                start_time,
                                options,
                                &ctx);
    if (code != 0)
        goto cleanup;

    ctx->gak_fct = gak_fct;
    ctx->gak_data = gak_data;

    if (in_tkt_service) {
        code = krb5_init_creds_set_service(context, ctx, in_tkt_service);
        if (code != 0)
            goto cleanup;
    }

    code = k5_init_creds_get(context, ctx, use_primary, kdcs);
    if (code != 0)
        goto cleanup;

    code = krb5_init_creds_get_creds(context, ctx, creds);
    if (code != 0)
        goto cleanup;

    if (as_reply != NULL) {
        *as_reply = ctx->reply;
        ctx->reply = NULL;
    }

cleanup:
    krb5_init_creds_free(context, ctx);

    return code;
}

krb5_error_code
k5_get_init_creds(krb5_context context, krb5_creds *creds,
                  krb5_principal client, krb5_prompter_fct prompter,
                  void *prompter_data, krb5_deltat start_time,
                  const char *in_tkt_service, krb5_get_init_creds_opt *options,
                  get_as_key_fn gak_fct, void *gak_data,
                  krb5_kdc_rep **as_reply)
{
    krb5_error_code ret;
    struct kdclist *kdcs = NULL;
    struct errinfo errsave = EMPTY_ERRINFO;

    ret = k5_kdclist_create(&kdcs);
    if (ret)
        goto cleanup;

    /* Try getting the requested ticket from any KDC. */
    ret = try_init_creds(context, creds, client, prompter, prompter_data,
                         start_time, in_tkt_service, options, gak_fct,
                         gak_data, FALSE, kdcs, as_reply);
    if (!ret)
        goto cleanup;

    /* If all of the KDCs are unavailable, or if the error was due to a user
     * interrupt, fail. */
    if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
        ret == KRB5_LIBOS_PWDINTR || ret == KRB5_LIBOS_CANTREADPWD)
        goto cleanup;

    /* If any reply came from a replica, try again with only primary KDCs. */
    if (k5_kdclist_any_replicas(context, kdcs)) {
        k5_save_ctx_error(context, ret, &errsave);
        TRACE_INIT_CREDS_PRIMARY(context);
        ret = try_init_creds(context, creds, client, prompter, prompter_data,
                             start_time, in_tkt_service, options, gak_fct,
                             gak_data, TRUE, NULL, as_reply);
        if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
            ret == KRB5_REALM_UNKNOWN) {
            /* We couldn't contact a primary KDC; return the error from the
             * replica we were able to contact. */
            ret = k5_restore_ctx_error(context, &errsave);
        }
    }

cleanup:
    k5_kdclist_free(kdcs);
    k5_clear_error(&errsave);
    return ret;
}

krb5_error_code
k5_identify_realm(krb5_context context, krb5_principal client,
                  const krb5_data *subject_cert, krb5_principal *client_out)
{
    krb5_error_code ret;
    krb5_get_init_creds_opt *opts = NULL;
    krb5_init_creds_context ctx = NULL;

    *client_out = NULL;

    ret = krb5_get_init_creds_opt_alloc(context, &opts);
    if (ret)
        goto cleanup;
    krb5_get_init_creds_opt_set_tkt_life(opts, 15);
    krb5_get_init_creds_opt_set_renew_life(opts, 0);
    krb5_get_init_creds_opt_set_forwardable(opts, 0);
    krb5_get_init_creds_opt_set_proxiable(opts, 0);
    krb5_get_init_creds_opt_set_canonicalize(opts, 1);

    ret = krb5_init_creds_init(context, client, NULL, NULL, 0, opts, &ctx);
    if (ret)
        goto cleanup;

    ctx->identify_realm = TRUE;
    ctx->subject_cert = subject_cert;

    ret = k5_init_creds_get(context, ctx, FALSE, NULL);
    if (ret)
        goto cleanup;

    TRACE_INIT_CREDS_IDENTIFIED_REALM(context, &ctx->request->client->realm);
    ret = krb5_copy_principal(context, ctx->request->client, client_out);

cleanup:
    krb5_get_init_creds_opt_free(context, opts);
    krb5_init_creds_free(context, ctx);
    return ret;
}

krb5_error_code
k5_populate_gic_opt(krb5_context context, krb5_get_init_creds_opt **out,
                    krb5_flags options, krb5_address *const *addrs,
                    krb5_enctype *ktypes, krb5_preauthtype *pre_auth_types,
                    krb5_creds *creds)
{
    int i;
    krb5_timestamp starttime;
    krb5_deltat lifetime;
    krb5_get_init_creds_opt *opt;
    krb5_error_code retval;

    *out = NULL;
    retval = krb5_get_init_creds_opt_alloc(context, &opt);
    if (retval)
        return(retval);

    if (addrs)
        krb5_get_init_creds_opt_set_address_list(opt, (krb5_address **) addrs);
    if (ktypes) {
        i = k5_count_etypes(ktypes);
        if (i)
            krb5_get_init_creds_opt_set_etype_list(opt, ktypes, i);
    }
    if (pre_auth_types) {
        for (i=0; pre_auth_types[i]; i++);
        if (i)
            krb5_get_init_creds_opt_set_preauth_list(opt, pre_auth_types, i);
    }
    if (options&KDC_OPT_FORWARDABLE)
        krb5_get_init_creds_opt_set_forwardable(opt, 1);
    else krb5_get_init_creds_opt_set_forwardable(opt, 0);
    if (options&KDC_OPT_PROXIABLE)
        krb5_get_init_creds_opt_set_proxiable(opt, 1);
    else krb5_get_init_creds_opt_set_proxiable(opt, 0);
    if (creds && creds->times.endtime) {
        retval = krb5_timeofday(context, &starttime);
        if (retval)
            goto cleanup;
        if (creds->times.starttime) starttime = creds->times.starttime;
        lifetime = ts_delta(creds->times.endtime, starttime);
        krb5_get_init_creds_opt_set_tkt_life(opt, lifetime);
    }
    *out = opt;
    return 0;

cleanup:
    krb5_get_init_creds_opt_free(context, opt);
    return retval;
}
