/*
 * Copyright (C) 2001-2012 Free Software Foundation, Inc.
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * Author: Nikos Mavrogiannopoulos
 *
 * This file is part of GnuTLS.
 *
 * The GnuTLS is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>
 *
 */

#include "gnutls_int.h"
#include "errors.h"
#include "auth.h"
#include "auth.h"
#include "algorithms.h"
#include "auth/cert.h"
#include "auth/psk.h"
#include "auth/srp_kx.h"
#include "auth/anon.h"
#include "datum.h"

/* The functions here are used in order for authentication algorithms
 * to be able to retrieve the needed credentials eg public and private
 * key etc.
 */

/**
 * gnutls_credentials_clear:
 * @session: is a #gnutls_session_t type.
 *
 * Clears all the credentials previously set in this session.
 **/
void gnutls_credentials_clear(gnutls_session_t session)
{
	if (session->key.cred) { /* beginning of the list */
		auth_cred_st *ccred, *ncred;
		ccred = session->key.cred;
		while (ccred != NULL) {
			ncred = ccred->next;
			gnutls_free(ccred);
			ccred = ncred;
		}
		session->key.cred = NULL;
	}
}

/* 
 * This creates a linked list of the form:
 * { algorithm, credentials, pointer to next }
 */
/**
 * gnutls_credentials_set:
 * @session: is a #gnutls_session_t type.
 * @type: is the type of the credentials
 * @cred: the credentials to set
 *
 * Sets the needed credentials for the specified type.  E.g. username,
 * password - or public and private keys etc.  The @cred parameter is
 * a structure that depends on the specified type and on the current
 * session (client or server).
 *
 * In order to minimize memory usage, and share credentials between
 * several threads gnutls keeps a pointer to cred, and not the whole
 * cred structure.  Thus you will have to keep the structure allocated
 * until you call gnutls_deinit().
 *
 * For %GNUTLS_CRD_ANON, @cred should be
 * #gnutls_anon_client_credentials_t in case of a client.  In case of
 * a server it should be #gnutls_anon_server_credentials_t.
 *
 * For %GNUTLS_CRD_SRP, @cred should be #gnutls_srp_client_credentials_t
 * in case of a client, and #gnutls_srp_server_credentials_t, in case
 * of a server.
 *
 * For %GNUTLS_CRD_CERTIFICATE, @cred should be
 * #gnutls_certificate_credentials_t.
 *
 * Returns: On success, %GNUTLS_E_SUCCESS (0) is returned,
 *   otherwise a negative error code is returned.
 **/
int gnutls_credentials_set(gnutls_session_t session,
			   gnutls_credentials_type_t type, void *cred)
{
	auth_cred_st *ccred = NULL, *pcred = NULL;
	int exists = 0;

	if (session->key.cred == NULL) { /* beginning of the list */

		session->key.cred = gnutls_malloc(sizeof(auth_cred_st));
		if (session->key.cred == NULL)
			return GNUTLS_E_MEMORY_ERROR;

		/* copy credentials locally */
		session->key.cred->credentials = cred;

		session->key.cred->next = NULL;
		session->key.cred->algorithm = type;
	} else {
		ccred = session->key.cred;
		while (ccred != NULL) {
			if (ccred->algorithm == type) {
				exists = 1;
				break;
			}
			pcred = ccred;
			ccred = ccred->next;
		}
		/* After this, pcred is not null.
		 */

		if (exists == 0) { /* new entry */
			pcred->next = gnutls_malloc(sizeof(auth_cred_st));
			if (pcred->next == NULL)
				return GNUTLS_E_MEMORY_ERROR;

			ccred = pcred->next;

			/* copy credentials locally */
			ccred->credentials = cred;

			ccred->next = NULL;
			ccred->algorithm = type;
		} else { /* modify existing entry */
			ccred->credentials = cred;
		}
	}

	/* sanity tests */
	if (type == GNUTLS_CRD_CERTIFICATE) {
		gnutls_certificate_credentials_t c = cred;
		unsigned i;
		bool allow_tls13 = 0;
		unsigned key_usage;

		if (c != NULL && c->ncerts != 0) {
			for (i = 0; i < c->ncerts; i++) {
				key_usage = get_key_usage(
					session,
					c->certs[i].cert_list[0].pubkey);
				if (key_usage == 0 ||
				    (key_usage &
				     GNUTLS_KEY_DIGITAL_SIGNATURE)) {
					allow_tls13 = 1;
					break;
				}
			}

			if (session->security_parameters.entity ==
				    GNUTLS_SERVER &&
			    !c->tls13_ok)
				allow_tls13 = 0;

			if (!allow_tls13) {
				/* to prevent the server random indicate TLS1.3 support */
				session->internals.flags |= INT_FLAG_NO_TLS13;
			}
		}
	}

	return 0;
}

/**
 * gnutls_credentials_get:
 * @session: is a #gnutls_session_t type.
 * @type: is the type of the credentials to return
 * @cred: will contain the credentials.
 *
 * Returns the previously provided credentials structures.
 *
 * For %GNUTLS_CRD_ANON, @cred will be
 * #gnutls_anon_client_credentials_t in case of a client.  In case of
 * a server it should be #gnutls_anon_server_credentials_t.
 *
 * For %GNUTLS_CRD_SRP, @cred will be #gnutls_srp_client_credentials_t
 * in case of a client, and #gnutls_srp_server_credentials_t, in case
 * of a server.
 *
 * For %GNUTLS_CRD_CERTIFICATE, @cred will be
 * #gnutls_certificate_credentials_t.
 *
 * Returns: On success, %GNUTLS_E_SUCCESS (0) is returned,
 *   otherwise a negative error code is returned.
 *
 * Since: 3.3.3
 **/
int gnutls_credentials_get(gnutls_session_t session,
			   gnutls_credentials_type_t type, void **cred)
{
	const void *_cred;

	_cred = _gnutls_get_cred(session, type);
	if (_cred == NULL)
		return gnutls_assert_val(GNUTLS_E_INVALID_REQUEST);

	if (cred)
		*cred = (void *)_cred;

	return 0;
}

/**
 * gnutls_auth_get_type:
 * @session: is a #gnutls_session_t type.
 *
 * Returns type of credentials for the current authentication schema.
 * The returned information is to be used to distinguish the function used
 * to access authentication data.
 *
 * Eg. for CERTIFICATE ciphersuites (key exchange algorithms:
 * %GNUTLS_KX_RSA, %GNUTLS_KX_DHE_RSA), the same function are to be
 * used to access the authentication data.
 *
 * Note that on resumed sessions, this function returns the schema
 * used in the original session authentication.
 *
 * Returns: The type of credentials for the current authentication
 *   schema, a #gnutls_credentials_type_t type.
 **/
gnutls_credentials_type_t gnutls_auth_get_type(gnutls_session_t session)
{
	if (session->security_parameters.entity == GNUTLS_SERVER)
		return gnutls_auth_client_get_type(session);
	else
		return gnutls_auth_server_get_type(session);
}

/**
 * gnutls_auth_server_get_type:
 * @session: is a #gnutls_session_t type.
 *
 * Returns the type of credentials that were used for server authentication.
 * The returned information is to be used to distinguish the function used
 * to access authentication data.
 *
 * Note that on resumed sessions, this function returns the schema
 * used in the original session authentication.
 *
 * Returns: The type of credentials for the server authentication
 *   schema, a #gnutls_credentials_type_t type.
 **/
gnutls_credentials_type_t gnutls_auth_server_get_type(gnutls_session_t session)
{
	return session->security_parameters.server_auth_type;
}

/**
 * gnutls_auth_client_get_type:
 * @session: is a #gnutls_session_t type.
 *
 * Returns the type of credentials that were used for client authentication.
 * The returned information is to be used to distinguish the function used
 * to access authentication data.
 *
 * Note that on resumed sessions, this function returns the schema
 * used in the original session authentication.
 *
 * Returns: The type of credentials for the client authentication
 *   schema, a #gnutls_credentials_type_t type.
 **/
gnutls_credentials_type_t gnutls_auth_client_get_type(gnutls_session_t session)
{
	return session->security_parameters.client_auth_type;
}

/* 
 * This returns a pointer to the linked list. Don't
 * free that!!!
 */
const void *_gnutls_get_kx_cred(gnutls_session_t session,
				gnutls_kx_algorithm_t algo)
{
	int server = session->security_parameters.entity == GNUTLS_SERVER ? 1 :
									    0;

	return _gnutls_get_cred(session, _gnutls_map_kx_get_cred(algo, server));
}

const void *_gnutls_get_cred(gnutls_session_t session,
			     gnutls_credentials_type_t type)
{
	auth_cred_st *ccred;
	gnutls_key_st *key = &session->key;

	ccred = key->cred;
	while (ccred != NULL) {
		if (ccred->algorithm == type) {
			break;
		}
		ccred = ccred->next;
	}
	if (ccred == NULL)
		return NULL;

	return ccred->credentials;
}

/*-
 * _gnutls_free_auth_info - Frees the auth info structure
 * @session: is a #gnutls_session_t type.
 *
 * This function frees the auth info structure and sets it to
 * null. It must be called since some structures contain malloced
 * elements.
 -*/
void _gnutls_free_auth_info(gnutls_session_t session)
{
	dh_info_st *dh_info;

	if (session == NULL) {
		gnutls_assert();
		return;
	}

	switch (session->key.auth_info_type) {
	case GNUTLS_CRD_SRP: {
		srp_server_auth_info_t info =
			_gnutls_get_auth_info(session, GNUTLS_CRD_SRP);

		if (info == NULL)
			break;

		gnutls_free(info->username);
		info->username = NULL;
	} break;
#ifdef ENABLE_ANON
	case GNUTLS_CRD_ANON: {
		anon_auth_info_t info =
			_gnutls_get_auth_info(session, GNUTLS_CRD_ANON);

		if (info == NULL)
			break;

		dh_info = &info->dh;
		_gnutls_free_dh_info(dh_info);
	} break;
#endif
	case GNUTLS_CRD_PSK: {
		psk_auth_info_t info =
			_gnutls_get_auth_info(session, GNUTLS_CRD_PSK);

		if (info == NULL)
			break;

		gnutls_free(info->username);
		info->username = NULL;
		info->username_len = 0;

		gnutls_free(info->hint);
		info->hint = NULL;
		info->hint_len = 0;

#ifdef ENABLE_DHE
		dh_info = &info->dh;
		_gnutls_free_dh_info(dh_info);
#endif
	} break;
	case GNUTLS_CRD_CERTIFICATE: {
		unsigned int i;
		cert_auth_info_t info =
			_gnutls_get_auth_info(session, GNUTLS_CRD_CERTIFICATE);

		if (info == NULL)
			break;

		dh_info = &info->dh;
		for (i = 0; i < info->ncerts; i++) {
			_gnutls_free_datum(&info->raw_certificate_list[i]);
		}

		for (i = 0; i < info->nocsp; i++) {
			_gnutls_free_datum(&info->raw_ocsp_list[i]);
		}

		gnutls_free(info->raw_certificate_list);
		gnutls_free(info->raw_ocsp_list);
		info->ncerts = 0;
		info->nocsp = 0;

#ifdef ENABLE_DHE
		_gnutls_free_dh_info(dh_info);
#endif
	}

	break;
	default:
		return;
	}

	gnutls_free(session->key.auth_info);
	session->key.auth_info_size = 0;
	session->key.auth_info_type = 0;
}

/* This function will create the auth info structure in the key
 * structure if needed.
 *
 * If allow change is !=0 then this will allow changing the auth
 * info structure to a different type.
 */
int _gnutls_auth_info_init(gnutls_session_t session,
			   gnutls_credentials_type_t type, int size,
			   int allow_change)
{
	if (session->key.auth_info == NULL) {
		session->key.auth_info = gnutls_calloc(1, size);
		if (session->key.auth_info == NULL) {
			gnutls_assert();
			return GNUTLS_E_MEMORY_ERROR;
		}
		session->key.auth_info_type = type;
		session->key.auth_info_size = size;
	} else {
		if (allow_change == 0) {
			/* If the credentials for the current authentication scheme,
			 * are not the one we want to set, then it's an error.
			 * This may happen if a rehandshake is performed an the
			 * ciphersuite which is negotiated has different authentication
			 * schema.
			 */
			if (type != session->key.auth_info_type) {
				gnutls_assert();
				return GNUTLS_E_INVALID_REQUEST;
			}
		} else {
			/* The new behaviour: Here we reallocate the auth info structure
			 * in order to be able to negotiate different authentication
			 * types. Ie. perform an auth_anon and then authenticate again using a
			 * certificate (in order to prevent revealing the certificate's contents,
			 * to passive eavesdropers.
			 */
			if (type != session->key.auth_info_type) {
				_gnutls_free_auth_info(session);

				session->key.auth_info = gnutls_calloc(1, size);
				if (session->key.auth_info == NULL) {
					gnutls_assert();
					return GNUTLS_E_MEMORY_ERROR;
				}

				session->key.auth_info_type = type;
				session->key.auth_info_size = size;
			}
		}
	}
	return 0;
}
