/**
 *
 * \file tls.c
 * @brief getdns TLS functions
 */

/*
 * Copyright (c) 2018-2019, NLnet Labs
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * * Neither the names of the copyright holders nor the
 *   names of its contributors may be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Verisign, Inc. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <gnutls/x509.h>

#include "config.h"

#include "debug.h"
#include "context.h"

#include "tls.h"

/*
 * Cipher suites recommended in RFC7525.
 *
 * The following strings generate a list with the same ciphers that are
 * generated by the equivalent string in the OpenSSL version of this file.
 */
static char const * const _getdns_tls_context_default_cipher_list =
	"+ECDHE-RSA:+ECDHE-ECDSA:+AEAD";

static char const * const _getdns_tls_context_default_cipher_suites =
	"+AES-256-GCM:+AES-128-GCM:+CHACHA20-POLY1305";

static char const * const _getdns_tls_connection_opportunistic_cipher_list =
	"NORMAL";

static char const * const _getdns_tls_priorities[] = {
	NULL,			/* No protocol */
	NULL,		/* SSL3 - no available keyword. */
	"+VERS-TLS1.0",	/* TLS1.0 */
	"+VERS-TLS1.1",	/* TLS1.1 */
	"+VERS-TLS1.2",	/* TLS1.2 */
	"+VERS-TLS1.3",	/* TLS1.3 */
};

static char* getdns_strdup(struct mem_funcs* mfs, const char* s)
{
	char* res;

	if (!s)
		return NULL;

	res = GETDNS_XMALLOC(*mfs, char, strlen(s) + 1);
	if (!res)
		return NULL;
	strcpy(res, s);
	return res;
}

static char* getdns_priappend(struct mem_funcs* mfs, char* s1, const char* s2)
{
	char* res;

	if (!s1)
		return getdns_strdup(mfs, s2);
	if (!s2)
		return s1;

	res = GETDNS_XMALLOC(*mfs, char, strlen(s1) + strlen(s2) + 2);
	if (!res)
		return NULL;
	strcpy(res, s1);
	strcat(res, ":");
	strcat(res, s2);
	GETDNS_FREE(*mfs, s1);
	return res;
}

static int set_connection_ciphers(_getdns_tls_connection* conn)
{
	char* pri = NULL;
	int res;

	pri = getdns_priappend(conn->mfs, pri, "NONE:+COMP-ALL:+SIGN-RSA-SHA384");

	if (conn->cipher_suites)
		pri = getdns_priappend(conn->mfs, pri, conn->cipher_suites);
	else if (conn->ctx->cipher_suites)
		pri = getdns_priappend(conn->mfs, pri, conn->ctx->cipher_suites);

	if (conn->cipher_list)
		pri = getdns_priappend(conn->mfs, pri, conn->cipher_list);
	else if (conn->ctx->cipher_list)
		pri = getdns_priappend(conn->mfs, pri, conn->ctx->cipher_list);

	if (conn->curve_list)
		pri = getdns_priappend(conn->mfs, pri, conn->curve_list);
	else if (conn->ctx->curve_list)
		pri = getdns_priappend(conn->mfs, pri, conn->ctx->curve_list);
	else
		pri = getdns_priappend(conn->mfs, pri, "+CURVE-ALL");

	gnutls_protocol_t min = conn->min_tls;
	gnutls_protocol_t max = conn->max_tls;
	if (!min) min = conn->ctx->min_tls;
	if (!max) max = conn->ctx->max_tls;

	if (!min && !max) {
		pri = getdns_priappend(conn->mfs, pri, "+VERS-TLS-ALL");
	} else {
		if (!max) max = GNUTLS_TLS_VERSION_MAX;

		for (gnutls_protocol_t i = min; i <= max; ++i)
			pri = getdns_priappend(conn->mfs, pri, _getdns_tls_priorities[i]);
	}

	if (pri) {
		res = gnutls_priority_set_direct(conn->tls, pri, NULL);
		if (res != GNUTLS_E_SUCCESS) {
			_getdns_log(conn->log
				    , GETDNS_LOG_UPSTREAM_STATS, GETDNS_LOG_ERR
				    , "%s: %s %s (%s)\n"
				    , STUB_DEBUG_SETUP_TLS
				    , "Error configuring TLS connection with "
				    , pri
				    , gnutls_strerror(res));
		}
	}
	else
		res = gnutls_set_default_priority(conn->tls);
	GETDNS_FREE(*conn->mfs, pri);

	return res;
}

static getdns_return_t error_may_want_read_write(_getdns_tls_connection* conn, int err)
{
	switch (err) {
	case GNUTLS_E_INTERRUPTED:
	case GNUTLS_E_AGAIN:
	case GNUTLS_E_WARNING_ALERT_RECEIVED:
	case GNUTLS_E_GOT_APPLICATION_DATA:
		if (gnutls_record_get_direction(conn->tls) == 0)
			return GETDNS_RETURN_TLS_WANT_READ;
		else
			return GETDNS_RETURN_TLS_WANT_WRITE;

	default:
		return GETDNS_RETURN_GENERIC_ERROR;
	}
}

static getdns_return_t get_gnu_mac_algorithm(int algorithm, gnutls_mac_algorithm_t* gnualg)
{
	switch (algorithm) {
	case GETDNS_HMAC_MD5   : *gnualg = GNUTLS_MAC_MD5   ; break;
	case GETDNS_HMAC_SHA1  : *gnualg = GNUTLS_MAC_SHA1  ; break;
	case GETDNS_HMAC_SHA224: *gnualg = GNUTLS_MAC_SHA224; break;
	case GETDNS_HMAC_SHA256: *gnualg = GNUTLS_MAC_SHA256; break;
	case GETDNS_HMAC_SHA384: *gnualg = GNUTLS_MAC_SHA384; break;
	case GETDNS_HMAC_SHA512: *gnualg = GNUTLS_MAC_SHA512; break;
	default                : return GETDNS_RETURN_GENERIC_ERROR;
	}

	return GETDNS_RETURN_GOOD;
}

static gnutls_protocol_t _getdns_tls_version2gnutls_version(getdns_tls_version_t v)
{
	switch (v) {
	case GETDNS_SSL3  : return GNUTLS_SSL3;
	case GETDNS_TLS1  : return GNUTLS_TLS1;
	case GETDNS_TLS1_1: return GNUTLS_TLS1_1;
	case GETDNS_TLS1_2: return GNUTLS_TLS1_2;
#if GNUTLS_VERSION_NUMBER >= 0x030605
	case GETDNS_TLS1_3: return GNUTLS_TLS1_3;
#endif
	default           : return GNUTLS_TLS_VERSION_MAX;
	}
}

static _getdns_tls_x509* _getdns_tls_x509_new(struct mem_funcs* mfs, gnutls_datum_t cert)
{
	_getdns_tls_x509* res;

	res = GETDNS_MALLOC(*mfs, _getdns_tls_x509);
	if (res)
		res->tls = cert;

	return res;
}

void _getdns_tls_init()
{
	gnutls_global_init();
}

_getdns_tls_context* _getdns_tls_context_new(struct mem_funcs* mfs, const getdns_log_config* log)
{
	_getdns_tls_context* res;

	if (!(res = GETDNS_MALLOC(*mfs, struct _getdns_tls_context)))
		return NULL;

	res->mfs = mfs;
	res->cipher_list = res->cipher_suites = res->curve_list = NULL;
	res->min_tls = res->max_tls = 0;
	res->ca_trust_file = NULL;
	res->ca_trust_path = NULL;
	res->log = log;

	return res;
}

getdns_return_t _getdns_tls_context_free(struct mem_funcs* mfs, _getdns_tls_context* ctx)
{
	if (!ctx)
		return GETDNS_RETURN_INVALID_PARAMETER;

	GETDNS_FREE(*mfs, ctx->ca_trust_path);
	GETDNS_FREE(*mfs, ctx->ca_trust_file);
	GETDNS_FREE(*mfs, ctx->curve_list);
	GETDNS_FREE(*mfs, ctx->cipher_suites);
	GETDNS_FREE(*mfs, ctx->cipher_list);
	GETDNS_FREE(*mfs, ctx);
	return GETDNS_RETURN_GOOD;
}

void _getdns_tls_context_pinset_init(_getdns_tls_context* ctx)
{
	(void) ctx;
}

getdns_return_t _getdns_tls_context_set_min_max_tls_version(_getdns_tls_context* ctx, getdns_tls_version_t min, getdns_tls_version_t max)
{
	if (!ctx)
		return GETDNS_RETURN_INVALID_PARAMETER;
	ctx->min_tls = _getdns_tls_version2gnutls_version(min);
	ctx->max_tls = _getdns_tls_version2gnutls_version(max);
	return GETDNS_RETURN_GOOD;
}

const char* _getdns_tls_context_get_default_cipher_list()
{
	return _getdns_tls_context_default_cipher_list;
}

getdns_return_t _getdns_tls_context_set_cipher_list(_getdns_tls_context* ctx, const char* list)
{
	if (!ctx)
		return GETDNS_RETURN_INVALID_PARAMETER;

	if (!list)
		list = _getdns_tls_context_default_cipher_list;

	GETDNS_FREE(*ctx->mfs, ctx->cipher_list);
	ctx->cipher_list = getdns_strdup(ctx->mfs, list);
	return GETDNS_RETURN_GOOD;
}

const char* _getdns_tls_context_get_default_cipher_suites()
{
	return _getdns_tls_context_default_cipher_suites;
}

getdns_return_t _getdns_tls_context_set_cipher_suites(_getdns_tls_context* ctx, const char* list)
{
	if (!ctx)
		return GETDNS_RETURN_INVALID_PARAMETER;

	if (!list)
		list = _getdns_tls_context_default_cipher_suites;

	GETDNS_FREE(*ctx->mfs, ctx->cipher_suites);
	ctx->cipher_suites = getdns_strdup(ctx->mfs, list);
	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_context_set_curves_list(_getdns_tls_context* ctx, const char* list)
{
	if (!ctx)
		return GETDNS_RETURN_INVALID_PARAMETER;

	GETDNS_FREE(*ctx->mfs, ctx->curve_list);
	ctx->curve_list = getdns_strdup(ctx->mfs, list);
	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_context_set_ca(_getdns_tls_context* ctx, const char* file, const char* path)
{
	if (!ctx)
		return GETDNS_RETURN_INVALID_PARAMETER;

	GETDNS_FREE(*ctx->mfs, ctx->ca_trust_file);
	ctx->ca_trust_file = getdns_strdup(ctx->mfs, file);
	GETDNS_FREE(*ctx->mfs, ctx->ca_trust_path);
	ctx->ca_trust_path = getdns_strdup(ctx->mfs, path);
	return GETDNS_RETURN_GOOD;
}

_getdns_tls_connection* _getdns_tls_connection_new(struct mem_funcs* mfs, _getdns_tls_context* ctx, int fd, const getdns_log_config* log)
{
	_getdns_tls_connection* res;

	if (!ctx)
		return NULL;

	if (!(res = GETDNS_MALLOC(*mfs, struct _getdns_tls_connection)))
		return NULL;

	res->shutdown = 0;
	res->ctx = ctx;
	res->mfs = mfs;
	res->cred = NULL;
	res->tls = NULL;
	res->cipher_list = res->cipher_suites = res->curve_list = NULL;
	res->min_tls = res->max_tls = 0;
	res->dane_state = NULL;
	res->dane_query = NULL;
	res->tlsa = NULL;
	res->log = log;

	if (gnutls_certificate_allocate_credentials(&res->cred) != GNUTLS_E_SUCCESS)
		goto failed;

	if (!ctx->ca_trust_file && !ctx->ca_trust_path)
		gnutls_certificate_set_x509_system_trust(res->cred);
	else {
		if (ctx->ca_trust_file)
			gnutls_certificate_set_x509_trust_file(res->cred, ctx->ca_trust_file, GNUTLS_X509_FMT_PEM);
		if (ctx->ca_trust_path)
			gnutls_certificate_set_x509_trust_dir(res->cred, ctx->ca_trust_path, GNUTLS_X509_FMT_PEM);
	}

	if (gnutls_init(&res->tls, GNUTLS_CLIENT | GNUTLS_NONBLOCK) != GNUTLS_E_SUCCESS)
		goto failed;
	if (set_connection_ciphers(res) != GNUTLS_E_SUCCESS) {

		goto failed;
	}
	if (gnutls_credentials_set(res->tls, GNUTLS_CRD_CERTIFICATE, res->cred) != GNUTLS_E_SUCCESS)
		goto failed;
	if (dane_state_init(&res->dane_state, DANE_F_IGNORE_DNSSEC) != DANE_E_SUCCESS)
		goto failed;

	gnutls_transport_set_int(res->tls, fd);
	return res;

failed:
	_getdns_tls_connection_free(mfs, res);
	return NULL;
}

getdns_return_t _getdns_tls_connection_free(struct mem_funcs* mfs, _getdns_tls_connection* conn)
{
	if (!conn || !conn->tls)
		return GETDNS_RETURN_INVALID_PARAMETER;

	if (conn->dane_query)
		dane_query_deinit(conn->dane_query);
	if (conn->dane_state)
		dane_state_deinit(conn->dane_state);
	if (conn->tls)
		gnutls_deinit(conn->tls);
	if (conn->cred)
		gnutls_certificate_free_credentials(conn->cred);
	GETDNS_FREE(*mfs, conn->tlsa);
	GETDNS_FREE(*mfs, conn->curve_list);
	GETDNS_FREE(*mfs, conn->cipher_suites);
	GETDNS_FREE(*mfs, conn->cipher_list);
	GETDNS_FREE(*mfs, conn);
	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_connection_shutdown(_getdns_tls_connection* conn)
{
	if (!conn || !conn->tls)
		return GETDNS_RETURN_INVALID_PARAMETER;

	if (conn->shutdown == 0) {
		gnutls_bye(conn->tls, GNUTLS_SHUT_WR);
		conn->shutdown++;
	} else {
		gnutls_bye(conn->tls, GNUTLS_SHUT_RDWR);
		conn->shutdown++;
	}

	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_connection_set_min_max_tls_version(_getdns_tls_connection* conn, getdns_tls_version_t min, getdns_tls_version_t max)
{
	if (!conn)
		return GETDNS_RETURN_INVALID_PARAMETER;
	conn->min_tls = _getdns_tls_version2gnutls_version(min);
	conn->max_tls = _getdns_tls_version2gnutls_version(max);
	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_connection_set_cipher_list(_getdns_tls_connection* conn, const char* list)
{
	if (!conn || !conn->tls)
		return GETDNS_RETURN_INVALID_PARAMETER;

	if (!list)
		list = _getdns_tls_connection_opportunistic_cipher_list;

	GETDNS_FREE(*conn->mfs, conn->cipher_list);
	conn->cipher_list = getdns_strdup(conn->mfs, list);
	if (set_connection_ciphers(conn) == GNUTLS_E_SUCCESS)
		return GETDNS_RETURN_GOOD;
	else
		return GETDNS_RETURN_GENERIC_ERROR;
}

getdns_return_t _getdns_tls_connection_set_cipher_suites(_getdns_tls_connection* conn, const char* list)
{
	if (!conn || !conn->tls)
		return GETDNS_RETURN_INVALID_PARAMETER;

	GETDNS_FREE(*conn->mfs, conn->cipher_list);
	conn->cipher_suites = getdns_strdup(conn->mfs, list);
	if (set_connection_ciphers(conn) == GNUTLS_E_SUCCESS)
		return GETDNS_RETURN_GOOD;
	else
		return GETDNS_RETURN_GENERIC_ERROR;
}

getdns_return_t _getdns_tls_connection_set_curves_list(_getdns_tls_connection* conn, const char* list)
{
	if (!conn || !conn->tls)
		return GETDNS_RETURN_INVALID_PARAMETER;

	GETDNS_FREE(*conn->mfs, conn->curve_list);
	conn->curve_list = getdns_strdup(conn->mfs, list);
	if (set_connection_ciphers(conn) == GNUTLS_E_SUCCESS)
		return GETDNS_RETURN_GOOD;
	else
		return GETDNS_RETURN_GENERIC_ERROR;
}

getdns_return_t _getdns_tls_connection_set_session(_getdns_tls_connection* conn, _getdns_tls_session* s)
{
	int r;

	if (!conn || !conn->tls || !s)
		return GETDNS_RETURN_INVALID_PARAMETER;

	r = gnutls_session_set_data(conn->tls, s->tls.data, s->tls.size);
	if (r != GNUTLS_E_SUCCESS)
		return GETDNS_RETURN_GENERIC_ERROR;
	return GETDNS_RETURN_GOOD;
}

_getdns_tls_session* _getdns_tls_connection_get_session(struct mem_funcs* mfs, _getdns_tls_connection* conn)
{
	_getdns_tls_session* res;
	int r;

	if (!conn || !conn->tls)
		return NULL;

	if (!(res = GETDNS_MALLOC(*mfs, struct _getdns_tls_session)))
		return NULL;

	r = gnutls_session_get_data2(conn->tls, &res->tls);
	if (r != GNUTLS_E_SUCCESS) {
		GETDNS_FREE(*mfs, res);
		return NULL;
	}

	return res;
}

const char* _getdns_tls_connection_get_version(_getdns_tls_connection* conn)
{
	if (!conn || !conn->tls)
		return NULL;

	return gnutls_protocol_get_name(gnutls_protocol_get_version(conn->tls));
}

getdns_return_t _getdns_tls_connection_do_handshake(_getdns_tls_connection* conn)
{
	int r;

	if (!conn || !conn->tls)
		return GETDNS_RETURN_INVALID_PARAMETER;

	r = gnutls_handshake(conn->tls);
	if (r == GNUTLS_E_SUCCESS) {
		return GETDNS_RETURN_GOOD;
	}
	else
		return error_may_want_read_write(conn, r);
}

_getdns_tls_x509* _getdns_tls_connection_get_peer_certificate(struct mem_funcs* mfs, _getdns_tls_connection* conn)
{
	const gnutls_datum_t *cert_list;
	unsigned int cert_list_size;

	if (!conn || !conn->tls)
		return NULL;

	cert_list = gnutls_certificate_get_peers(conn->tls, &cert_list_size);
	if (cert_list == NULL)
		return NULL;

	return _getdns_tls_x509_new(mfs, *cert_list);
}

getdns_return_t _getdns_tls_connection_is_session_reused(_getdns_tls_connection* conn)
{
	if (!conn || !conn->tls)
		return GETDNS_RETURN_INVALID_PARAMETER;

	if (gnutls_session_is_resumed(conn->tls) != 0)
		return GETDNS_RETURN_GOOD;
	else
		return GETDNS_RETURN_TLS_CONNECTION_FRESH;
}

getdns_return_t _getdns_tls_connection_setup_hostname_auth(_getdns_tls_connection* conn, const char* auth_name)
{
	int r;

	if (!conn || !conn->tls || !auth_name)
		return GETDNS_RETURN_INVALID_PARAMETER;

	r = gnutls_server_name_set(conn->tls, GNUTLS_NAME_DNS, auth_name, strlen(auth_name));
	if (r != GNUTLS_E_SUCCESS)
		return GETDNS_RETURN_GENERIC_ERROR;

	gnutls_session_set_verify_cert(conn->tls, auth_name, 0);
	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_connection_set_host_pinset(_getdns_tls_connection* conn, const char* auth_name, const sha256_pin_t* pinset)
{
	int r;

	if (!conn || !conn->tls || !auth_name)
		return GETDNS_RETURN_INVALID_PARAMETER;

	size_t npins = 0;
	for (const sha256_pin_t* pin = pinset; pin; pin = pin->next)
		npins++;

	GETDNS_FREE(*conn->mfs, conn->tlsa);
	conn->tlsa = GETDNS_XMALLOC(*conn->mfs, char, npins * (SHA256_DIGEST_LENGTH + 3) * 2);
	if (!conn->tlsa)
		return GETDNS_RETURN_GENERIC_ERROR;

	char** dane_data = GETDNS_XMALLOC(*conn->mfs, char*, npins * 2 + 1);
	if (!dane_data)
		return GETDNS_RETURN_GENERIC_ERROR;
	int* dane_data_len = GETDNS_XMALLOC(*conn->mfs, int, npins * 2 + 1);
	if (!dane_data_len) {
		GETDNS_FREE(*conn->mfs, dane_data);
		return GETDNS_RETURN_GENERIC_ERROR;
	}

	char** dane_p = dane_data;
	int* dane_len_p = dane_data_len;
	char* p = conn->tlsa;
	for (const sha256_pin_t* pin = pinset; pin; pin = pin->next) {
		*dane_p++ = p;
		*dane_len_p++ = SHA256_DIGEST_LENGTH + 3;
		p[0] = DANE_CERT_USAGE_LOCAL_CA;
		p[1] = DANE_CERT_PK;
		p[2] = DANE_MATCH_SHA2_256;
		memcpy(&p[3], pin->pin, SHA256_DIGEST_LENGTH);
		p += SHA256_DIGEST_LENGTH + 3;

		*dane_p++ = p;
		*dane_len_p++ = SHA256_DIGEST_LENGTH + 3;
		p[0] = DANE_CERT_USAGE_LOCAL_EE;
		p[1] = DANE_CERT_PK;
		p[2] = DANE_MATCH_SHA2_256;
		memcpy(&p[3], pin->pin, SHA256_DIGEST_LENGTH);
		p += SHA256_DIGEST_LENGTH + 3;
	}
	*dane_p = NULL;

	if (conn->dane_query)
		dane_query_deinit(conn->dane_query);
	r = dane_raw_tlsa(conn->dane_state, &conn->dane_query, dane_data, dane_data_len, 0, 0);
	GETDNS_FREE(*conn->mfs, dane_data_len);
	GETDNS_FREE(*conn->mfs, dane_data);

	return (r == DANE_E_SUCCESS) ? GETDNS_RETURN_GOOD : GETDNS_RETURN_GENERIC_ERROR;
}

getdns_return_t _getdns_tls_connection_certificate_verify(_getdns_tls_connection* conn, long* errnum, const char** errmsg)
{
	if (!conn || !conn->tls)
		return GETDNS_RETURN_INVALID_PARAMETER;

	/* If no pinset, no DANE info to check. */
	if (!conn->dane_query)
		return GETDNS_RETURN_GOOD;

	/* Most of the internals of dane_verify_session_crt() */

	const gnutls_datum_t* cert_list;
	unsigned int cert_list_size = 0;
	unsigned int type;
	int ret;
	const gnutls_datum_t* cl;
	gnutls_datum_t* new_cert_list = NULL;
	int clsize;
	unsigned int verify;

	cert_list = gnutls_certificate_get_peers(conn->tls, &cert_list_size);
	if (cert_list_size == 0) {
		*errnum = 1;
		*errmsg = "No peer certificate";
		return GETDNS_RETURN_GENERIC_ERROR;
        }
	cl = cert_list;

        type = gnutls_certificate_type_get(conn->tls);

        /* this list may be incomplete, try to get the self-signed CA if any */
        if (cert_list_size > 0) {
                gnutls_x509_crt_t crt, ca;
                gnutls_certificate_credentials_t sc;

                ret = gnutls_x509_crt_init(&crt);
                if (ret < 0)
                        goto failsafe;

                ret = gnutls_x509_crt_import(crt, &cert_list[cert_list_size-1], GNUTLS_X509_FMT_DER);
                if (ret < 0) {
                        gnutls_x509_crt_deinit(crt);
                        goto failsafe;
                }

                /* if it is already self signed continue normally */
                ret = gnutls_x509_crt_check_issuer(crt, crt);
                if (ret != 0) {
                        gnutls_x509_crt_deinit(crt);
                        goto failsafe;
                }

                /* chain does not finish in a self signed cert, try to obtain the issuer */
                ret = gnutls_credentials_get(conn->tls, GNUTLS_CRD_CERTIFICATE, (void**)&sc);
                if (ret < 0) {
                        gnutls_x509_crt_deinit(crt);
                        goto failsafe;
                }

                ret = gnutls_certificate_get_issuer(sc, crt, &ca, 0);
                if (ret < 0) {
                        gnutls_x509_crt_deinit(crt);
                        goto failsafe;
                }

                /* make the new list */
                new_cert_list = GETDNS_XMALLOC(*conn->mfs, gnutls_datum_t, cert_list_size + 1);
                if (new_cert_list == NULL) {
                        gnutls_x509_crt_deinit(crt);
                        goto failsafe;
                }

                memcpy(new_cert_list, cert_list, cert_list_size*sizeof(gnutls_datum_t));
		cl = new_cert_list;

                ret = gnutls_x509_crt_export2(ca, GNUTLS_X509_FMT_DER, &new_cert_list[cert_list_size]);
                if (ret < 0) {
                        GETDNS_FREE(*conn->mfs, new_cert_list);
                        gnutls_x509_crt_deinit(crt);
                        goto failsafe;
                }
	}

failsafe:

	clsize = cert_list_size;
	if (cl == new_cert_list)
		clsize += 1;

	ret = dane_verify_crt_raw(conn->dane_state, cl, clsize, type, conn->dane_query, 0, 0, &verify);

	if (new_cert_list) {
		gnutls_free(new_cert_list[cert_list_size].data);
		GETDNS_FREE(*conn->mfs, new_cert_list);
	}

	if (ret != DANE_E_SUCCESS)
		return GETDNS_RETURN_GENERIC_ERROR;

	if (verify != 0) {
		if (verify & DANE_VERIFY_CERT_DIFFERS) {
			*errnum = 3;
			*errmsg = "Pinset validation: Certificate differs";
		} else if (verify &  DANE_VERIFY_CA_CONSTRAINTS_VIOLATED) {
			*errnum = 2;
			*errmsg = "Pinset validation: CA constraints violated";
		} else {
			*errnum = 4;
			*errmsg = "Pinset validation: Unknown DANE info";
		}
		return GETDNS_RETURN_GENERIC_ERROR;
	}

	return GETDNS_RETURN_GOOD;
}


getdns_return_t _getdns_tls_connection_read(_getdns_tls_connection* conn, uint8_t* buf, size_t to_read, size_t* read)
{
	ssize_t sread;

	if (!conn || !conn->tls || !read)
		return GETDNS_RETURN_INVALID_PARAMETER;

	sread = gnutls_record_recv(conn->tls, buf, to_read);
	if (sread < 0)
		return error_may_want_read_write(conn, sread);

	*read = sread;
	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_connection_write(_getdns_tls_connection* conn, uint8_t* buf, size_t to_write, size_t* written)
{
	int swritten;

	if (!conn || !conn->tls || !written)
		return GETDNS_RETURN_INVALID_PARAMETER;

	swritten = gnutls_record_send(conn->tls, buf, to_write);
	if (swritten < 0)
		return error_may_want_read_write(conn, swritten);

	*written = swritten;
	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_session_free(struct mem_funcs* mfs, _getdns_tls_session* s)
{
	if (!s)
		return GETDNS_RETURN_INVALID_PARAMETER;
	GETDNS_FREE(*mfs, s);
	return GETDNS_RETURN_GOOD;
}

getdns_return_t _getdns_tls_get_api_information(getdns_dict* dict)
{
	if (! getdns_dict_set_int(
	    dict, "gnutls_version_number", GNUTLS_VERSION_NUMBER)

	    && ! getdns_dict_util_set_string(
	    dict, "gnutls_version_string", GNUTLS_VERSION)
		)
		return GETDNS_RETURN_GOOD;
	return GETDNS_RETURN_GENERIC_ERROR;
}

void _getdns_tls_x509_free(struct mem_funcs* mfs, _getdns_tls_x509* cert)
{
	if (cert)
		GETDNS_FREE(*mfs, cert);
}

int _getdns_tls_x509_to_der(struct mem_funcs* mfs, _getdns_tls_x509* cert, getdns_bindata* bindata)
{
	gnutls_x509_crt_t crt;
	size_t s;

	if (!cert || gnutls_x509_crt_init(&crt) != GNUTLS_E_SUCCESS)
		return 0;

	gnutls_x509_crt_import(crt, &cert->tls, GNUTLS_X509_FMT_DER);
	gnutls_x509_crt_export(crt, GNUTLS_X509_FMT_DER, NULL, &s);

	if (!bindata) {
		gnutls_x509_crt_deinit(crt);
		return s;
	}

	bindata->data = GETDNS_XMALLOC(*mfs, uint8_t, s);
	if (!bindata->data) {
		gnutls_x509_crt_deinit(crt);
		return 0;
	}

	gnutls_x509_crt_export(crt, GNUTLS_X509_FMT_DER, bindata->data, &s);
	bindata->size = s;
	gnutls_x509_crt_deinit(crt);
	return s;
}

unsigned char* _getdns_tls_hmac_hash(struct mem_funcs* mfs, int algorithm, const void* key, size_t key_size, const void* data, size_t data_size, size_t* output_size)
{
	gnutls_mac_algorithm_t alg;
	unsigned int md_len;
	unsigned char* res;

	if (get_gnu_mac_algorithm(algorithm, &alg) != GETDNS_RETURN_GOOD)
		return NULL;

	md_len = gnutls_hmac_get_len(alg);
	res = (unsigned char*) GETDNS_XMALLOC(*mfs, unsigned char, md_len);
	if (!res)
		return NULL;

	(void) gnutls_hmac_fast(alg, key, key_size, data, data_size, res);

	if (output_size)
		*output_size = md_len;
	return res;
}

_getdns_tls_hmac* _getdns_tls_hmac_new(struct mem_funcs* mfs, int algorithm, const void* key, size_t key_size)
{
	gnutls_mac_algorithm_t alg;
	_getdns_tls_hmac* res;

	if (get_gnu_mac_algorithm(algorithm, &alg) != GETDNS_RETURN_GOOD)
		return NULL;

	if (!(res = GETDNS_MALLOC(*mfs, struct _getdns_tls_hmac)))
		return NULL;

	if (gnutls_hmac_init(&res->tls, alg, key, key_size) < 0) {
		GETDNS_FREE(*mfs, res);
		return NULL;
	}
	res->md_len = gnutls_hmac_get_len(alg);
	return res;
}

getdns_return_t _getdns_tls_hmac_add(_getdns_tls_hmac* h, const void* data, size_t data_size)
{
	if (!h || !h->tls || !data)
		return GETDNS_RETURN_INVALID_PARAMETER;

	if (gnutls_hmac(h->tls, data, data_size) < 0)
		return GETDNS_RETURN_GENERIC_ERROR;
	else
		return GETDNS_RETURN_GOOD;
}

unsigned char* _getdns_tls_hmac_end(struct mem_funcs* mfs, _getdns_tls_hmac* h, size_t* output_size)
{
	unsigned char* res;

	if (!h || !h->tls)
		return NULL;

	res = (unsigned char*) GETDNS_XMALLOC(*mfs, unsigned char, h->md_len);
	if (!res)
		return NULL;

	gnutls_hmac_deinit(h->tls, res);
	if (output_size)
		*output_size = h->md_len;

	GETDNS_FREE(*mfs, h);
	return res;
}

void _getdns_tls_sha1(const void* data, size_t data_size, unsigned char* buf)
{
	gnutls_hash_fast(GNUTLS_DIG_SHA1, data, data_size, buf);
}

void _getdns_tls_cookie_sha256(uint32_t secret, void* addr, size_t addrlen, unsigned char* buf, size_t* buflen)
{
	gnutls_hash_hd_t digest;

	gnutls_hash_init(&digest, GNUTLS_DIG_SHA256);
	gnutls_hash(digest, &secret, sizeof(secret));
	gnutls_hash(digest, addr, addrlen);
	gnutls_hash_deinit(digest, buf);
	*buflen = gnutls_hash_get_len(GNUTLS_DIG_SHA256);
}

/* tls.c */
