/*
 *  Unix SMB/CIFS implementation.
 *
 *  Simple GSSAPI wrappers
 *
 *  Copyright (c) 2012      Andreas Schneider <asn@samba.org>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#include "includes.h"
#include "gss_samba.h"

#ifdef HAVE_GSSAPI

#if !defined(HAVE_GSS_OID_EQUAL)
int smb_gss_oid_equal(const gss_OID first_oid, const gss_OID second_oid)
{
	if (first_oid == GSS_C_NO_OID || second_oid == GSS_C_NO_OID) {
		return 0;
	}

	if (first_oid == second_oid) {
		return 1;
	}

	if ((first_oid)->length != (second_oid)->length) {
		return 0;
	}

	if (memcmp((first_oid)->elements, (second_oid)->elements,
		   (first_oid)->length) == 0) {
		return 1;
	}

	return 0;
}
#endif /* !HAVE_GSS_OID_EQUAL */

/* wrapper around gss_krb5_import_cred() that prefers to use gss_acquire_cred_from()
 * if this GSSAPI extension is available. gss_acquire_cred_from() is properly
 * interposed by GSSPROXY while gss_krb5_import_cred() is not.
 *
 * This wrapper requires a proper krb5_context to resolve ccache name.
 * All gss_krb5_import_cred() callers in Samba already have krb5_context available. */
uint32_t smb_gss_krb5_import_cred(uint32_t *minor_status,
				  krb5_context ctx,
				  krb5_ccache id,
				  krb5_principal keytab_principal,
				  krb5_keytab keytab,
				  gss_cred_id_t *cred)
{
	return smb_gss_mech_import_cred(minor_status,
					ctx,
					id,
					keytab_principal,
					keytab,
					gss_mech_krb5,
					cred);
}

uint32_t smb_gss_mech_import_cred(OM_uint32 *minor_status,
				  krb5_context ctx,
				  krb5_ccache id,
				  krb5_principal keytab_principal,
				  krb5_keytab keytab,
				  const struct gss_OID_desc_struct *mech,
				  gss_cred_id_t *cred)
{
	uint32_t major_status = 0;

#ifdef HAVE_GSS_ACQUIRE_CRED_FROM
	uint32_t minor = 0;
	gss_key_value_element_desc ccache_element = {
		.key = "ccache",
		.value = NULL,
	};

	gss_key_value_element_desc keytab_element = {
		.key = "keytab",
		.value = NULL,
	};

	gss_key_value_element_desc elements[2];

	gss_key_value_set_desc cred_store = {
		.elements = &ccache_element,
		.count = 1,
	};

	/* we are interested exclusively in krb5 credentials,
	 * indicate to GSSAPI that we are not interested in any other
	 * mechanism here */
	gss_OID_set_desc mech_set = {
		.count = 1,
		.elements = discard_const_p(struct gss_OID_desc_struct,
					    mech),
	};

	gss_cred_usage_t cred_usage = GSS_C_INITIATE;
	gss_name_t name = NULL;
	gss_buffer_desc pr_name = {
		.value = NULL,
		.length = 0,
	};

	if (id != NULL) {
		major_status = krb5_cc_get_full_name(ctx,
						     id,
						     discard_const(&ccache_element.value));
		if (major_status != 0) {
			return major_status;
		}
	}

	if (keytab != NULL) {
		keytab_element.value = malloc(4096);
		if (!keytab_element.value) {
			return ENOMEM;
		}
		major_status = krb5_kt_get_name(ctx,
						keytab,
						discard_const(keytab_element.value), 4096);
		if (major_status != 0) {
			free(discard_const(keytab_element.value));
			return major_status;
		}
		cred_usage = GSS_C_ACCEPT;
		cred_store.elements = &keytab_element;

		if (keytab_principal != NULL) {
			major_status = krb5_unparse_name(ctx, keytab_principal, (char**)&pr_name.value);
			if (major_status != 0) {
				free(discard_const(keytab_element.value));
				return major_status;
			}
			pr_name.length = strlen(pr_name.value);

			major_status = gss_import_name(minor_status,
						       &pr_name,
						       discard_const(GSS_KRB5_NT_PRINCIPAL_NAME),
						       &name);
			if (major_status != 0) {
				krb5_free_unparsed_name(ctx, pr_name.value);
				free(discard_const(keytab_element.value));
				return major_status;
			}
		}
	}

	if (id != NULL && keytab != NULL) {
		elements[0] = ccache_element;
		elements[1] = keytab_element;

		cred_store.elements = elements;
		cred_store.count = 2;
		cred_usage = GSS_C_BOTH;
	}

	major_status = gss_acquire_cred_from(minor_status,
					     name,
					     0,
					     &mech_set,
					     cred_usage,
					     &cred_store,
					     cred,
					     NULL,
					     NULL);

	if (pr_name.value != NULL) {
		(void)gss_release_name(&minor, &name);
		krb5_free_unparsed_name(ctx, pr_name.value);
	}
	if (keytab_element.value != NULL) {
		free(discard_const(keytab_element.value));
	}
	krb5_free_string(ctx, discard_const(ccache_element.value));
#else
	major_status = gss_krb5_import_cred(minor_status,
					    id,
					    keytab_principal,
					    keytab, cred);

	if (major_status == (GSS_S_CALL_BAD_STRUCTURE|GSS_S_BAD_NAME)) {
		if ((keytab_principal == NULL) && (keytab != NULL)) {
			/* No principal was specified and MIT krb5 1.9 version failed.
			 * We have to fall back to set global acceptor identity */
			gss_OID_set_desc mech_set;
			char *kt_name = NULL;

			kt_name = malloc(4096);
			if (!kt_name) {
				return ENOMEM;
			}

			major_status = krb5_kt_get_name(ctx,
							keytab,
							kt_name, 4096);
			if (major_status != 0) {
				free(kt_name);
				return major_status;
			}

			major_status = gsskrb5_register_acceptor_identity(kt_name);
			if (major_status) {
				free(kt_name);
				return major_status;
			}

			/* We are dealing with krb5 GSSAPI mech in this fallback */
			mech_set.count = 1;
			mech_set.elements =
				discard_const_p(struct gss_OID_desc_struct,
						gss_mech_krb5);
			major_status = gss_acquire_cred(minor_status,
							GSS_C_NO_NAME,
							GSS_C_INDEFINITE,
							&mech_set,
							GSS_C_ACCEPT,
							cred,
							NULL, NULL);
			free(kt_name);
		}
	}
#endif
	return major_status;
}


#endif /* HAVE_GSSAPI */
