/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
 * Copyright 2009  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 "gssapiP_krb5.h"
#ifdef HAVE_MEMORY_H
#include <memory.h>
#endif
#include <assert.h>

static int
kg_is_initiator_cred(krb5_gss_cred_id_t cred)
{
    return (cred->usage == GSS_C_INITIATE || cred->usage == GSS_C_BOTH) &&
        (cred->ccache != NULL);
}

static OM_uint32
kg_impersonate_name(OM_uint32 *minor_status,
                    const krb5_gss_cred_id_t impersonator_cred,
                    const krb5_gss_name_t user,
                    OM_uint32 time_req,
                    krb5_gss_cred_id_t *output_cred,
                    OM_uint32 *time_rec,
                    krb5_context context)
{
    OM_uint32 major_status;
    krb5_error_code code;
    krb5_creds in_creds, *out_creds = NULL;

    *output_cred = NULL;
    memset(&in_creds, 0, sizeof(in_creds));

    in_creds.client = user->princ;
    in_creds.server = impersonator_cred->name->princ;

    if (impersonator_cred->req_enctypes != NULL)
        in_creds.keyblock.enctype = impersonator_cred->req_enctypes[0];

    code = k5_mutex_lock(&user->lock);
    if (code != 0) {
        *minor_status = code;
        return GSS_S_FAILURE;
    }

    if (user->ad_context != NULL) {
        code = krb5_authdata_export_authdata(context,
                                             user->ad_context,
                                             AD_USAGE_TGS_REQ,
                                             &in_creds.authdata);
        if (code != 0) {
            k5_mutex_unlock(&user->lock);
            *minor_status = code;
            return GSS_S_FAILURE;
        }
    }

    k5_mutex_unlock(&user->lock);

    code = krb5_get_credentials_for_user(context,
                                         KRB5_GC_CANONICALIZE | KRB5_GC_NO_STORE,
                                         impersonator_cred->ccache,
                                         &in_creds,
                                         NULL, &out_creds);
    if (code != 0) {
        krb5_free_authdata(context, in_creds.authdata);
        *minor_status = code;
        return GSS_S_FAILURE;
    }

    major_status = kg_compose_deleg_cred(minor_status,
                                         impersonator_cred,
                                         out_creds,
                                         time_req,
                                         output_cred,
                                         time_rec,
                                         context);

    krb5_free_authdata(context, in_creds.authdata);
    krb5_free_creds(context, out_creds);

    return major_status;
}

/* The mechglue always passes null desired_mechs and actual_mechs. */
OM_uint32 KRB5_CALLCONV
krb5_gss_acquire_cred_impersonate_name(OM_uint32 *minor_status,
                                       const gss_cred_id_t impersonator_cred_handle,
                                       const gss_name_t desired_name,
                                       OM_uint32 time_req,
                                       const gss_OID_set desired_mechs,
                                       gss_cred_usage_t cred_usage,
                                       gss_cred_id_t *output_cred_handle,
                                       gss_OID_set *actual_mechs,
                                       OM_uint32 *time_rec)
{
    OM_uint32 major_status;
    krb5_error_code code;
    krb5_gss_cred_id_t cred;
    krb5_context context;

    if (impersonator_cred_handle == GSS_C_NO_CREDENTIAL)
        return GSS_S_CALL_INACCESSIBLE_READ;

    if (desired_name == GSS_C_NO_NAME)
        return GSS_S_CALL_INACCESSIBLE_READ;

    if (output_cred_handle == NULL)
        return GSS_S_CALL_INACCESSIBLE_WRITE;

    if (cred_usage != GSS_C_INITIATE) {
        *minor_status = (OM_uint32)G_BAD_USAGE;
        return GSS_S_FAILURE;
    }

    *output_cred_handle = GSS_C_NO_CREDENTIAL;
    if (time_rec != NULL)
        *time_rec = 0;

    code = krb5_gss_init_context(&context);
    if (code != 0) {
        *minor_status = code;
        return GSS_S_FAILURE;
    }

    major_status = kg_cred_resolve(minor_status, context,
                                   impersonator_cred_handle, NULL);
    if (GSS_ERROR(major_status)) {
        krb5_free_context(context);
        return major_status;
    }

    major_status = kg_impersonate_name(minor_status,
                                       (krb5_gss_cred_id_t)impersonator_cred_handle,
                                       (krb5_gss_name_t)desired_name,
                                       time_req,
                                       &cred,
                                       time_rec,
                                       context);

    if (!GSS_ERROR(major_status))
        *output_cred_handle = (gss_cred_id_t)cred;

    k5_mutex_unlock(&((krb5_gss_cred_id_t)impersonator_cred_handle)->lock);
    krb5_free_context(context);

    return major_status;

}

/*
 * Set up cred to be an S4U2Proxy credential by copying in the impersonator's
 * creds, setting a cache config variable with the impersonator principal name,
 * and saving the impersonator principal name in the cred structure.
 */
static krb5_error_code
make_proxy_cred(krb5_context context, krb5_gss_cred_id_t cred,
                krb5_gss_cred_id_t impersonator_cred)
{
    krb5_error_code code;
    krb5_data data;
    char *str;

    code = krb5_cc_copy_creds(context, impersonator_cred->ccache,
                              cred->ccache);
    if (code)
        return code;

    code = krb5_unparse_name(context, impersonator_cred->name->princ, &str);
    if (code)
        return code;

    data = string2data(str);
    code = krb5_cc_set_config(context, cred->ccache, NULL,
                              KRB5_CONF_PROXY_IMPERSONATOR, &data);
    krb5_free_unparsed_name(context, str);
    if (code)
        return code;

    return krb5_copy_principal(context, impersonator_cred->name->princ,
                               &cred->impersonator);
}

OM_uint32
kg_compose_deleg_cred(OM_uint32 *minor_status,
                      krb5_gss_cred_id_t impersonator_cred,
                      krb5_creds *subject_creds,
                      OM_uint32 time_req,
                      krb5_gss_cred_id_t *output_cred,
                      OM_uint32 *time_rec,
                      krb5_context context)
{
    OM_uint32 major_status;
    krb5_error_code code;
    krb5_gss_cred_id_t cred = NULL;

    *output_cred = NULL;
    k5_mutex_assert_locked(&impersonator_cred->lock);

    if (!kg_is_initiator_cred(impersonator_cred) ||
        impersonator_cred->name == NULL ||
        impersonator_cred->impersonator != NULL) {
        code = G_BAD_USAGE;
        goto cleanup;
    }

    assert(impersonator_cred->name->princ != NULL);

    assert(subject_creds != NULL);
    assert(subject_creds->client != NULL);

    cred = xmalloc(sizeof(*cred));
    if (cred == NULL) {
        code = ENOMEM;
        goto cleanup;
    }
    memset(cred, 0, sizeof(*cred));

    code = k5_mutex_init(&cred->lock);
    if (code != 0)
        goto cleanup;

    cred->usage = GSS_C_INITIATE;

    cred->expire = subject_creds->times.endtime;

    code = kg_init_name(context, subject_creds->client, NULL, NULL, NULL, 0,
                        &cred->name);
    if (code != 0)
        goto cleanup;

    code = krb5_cc_new_unique(context, "MEMORY", NULL, &cred->ccache);
    if (code != 0)
        goto cleanup;
    cred->destroy_ccache = 1;

    code = krb5_cc_initialize(context, cred->ccache, subject_creds->client);
    if (code != 0)
        goto cleanup;

    /*
     * Only return a "proxy" credential for use with constrained
     * delegation if the subject credentials are forwardable.
     * Submitting non-forwardable credentials to the KDC for use
     * with constrained delegation will only return an error.
     */
    if (subject_creds->ticket_flags & TKT_FLG_FORWARDABLE) {
        code = make_proxy_cred(context, cred, impersonator_cred);
        if (code != 0)
            goto cleanup;
    }

    code = krb5_cc_store_cred(context, cred->ccache, subject_creds);
    if (code != 0)
        goto cleanup;

    if (time_rec != NULL) {
        krb5_timestamp now;

        code = krb5_timeofday(context, &now);
        if (code != 0)
            goto cleanup;

        *time_rec = cred->expire - now;
    }

    major_status = GSS_S_COMPLETE;
    *minor_status = 0;
    *output_cred = cred;

cleanup:
    if (code != 0) {
        *minor_status = code;
        major_status = GSS_S_FAILURE;
    }

    if (GSS_ERROR(major_status) && cred != NULL) {
        k5_mutex_destroy(&cred->lock);
        krb5_cc_destroy(context, cred->ccache);
        kg_release_name(context, &cred->name);
        xfree(cred);
    }

    return major_status;
}
