/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <errno.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include <compat/stdbool.h>
#endif /* HAVE_STDBOOL_H */
#if defined(HAVE_STDINT_H)
# include <stdint.h>
#elif defined(HAVE_INTTYPES_H)
# include <inttypes.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <sudo_compat.h>
#include <sudo_debug.h>
#include <sudo_event.h>
#include <sudo_fatal.h>
#include <sudo_gettext.h>
#include <sudo_util.h>

#include "logsrv_util.h"
#include <tls_common.h>
#include <hostcheck.h>

#if defined(HAVE_OPENSSL)

/*
 * Check that the server's certificate is valid and that it
 * contains the server name or IP address.
 * Returns 0 if the cert is invalid, else 1.
 */
static int
verify_peer_identity(int preverify_ok, X509_STORE_CTX *ctx)
{
    HostnameValidationResult result;
    struct peer_info *peer_info;
    SSL *ssl;
    X509 *current_cert;
    X509 *peer_cert;
    debug_decl(verify_peer_identity, SUDO_DEBUG_UTIL);

    current_cert = X509_STORE_CTX_get_current_cert(ctx);

    /* If pre-verification of the cert failed, just propagate that result back */
    if (preverify_ok != 1) {
        int err = X509_STORE_CTX_get_error(ctx);
        char current_cert_name[256] = "";
        if (current_cert != NULL)
            X509_NAME_oneline(X509_get_subject_name(current_cert), current_cert_name, sizeof(current_cert_name));

        sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
            "TLS verification failed for cert '%s': '%d:%s'", current_cert_name,
            err, X509_verify_cert_error_string(err));
        debug_return_int(0);
    }

    /*
     * Since this callback is called for each cert in the chain,
     * check that current cert is the peer's certificate.
     */
    peer_cert = X509_STORE_CTX_get0_cert(ctx);
    if (current_cert != peer_cert) {
        debug_return_int(1);
    }

    /* Fetch the attached peer_info from the ssl connection object. */
    ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
    if (ssl == NULL) {
        debug_return_int(0);
    }
    peer_info = SSL_get_ex_data(ssl, 1);
    if (peer_info == NULL) {
        debug_return_int(0);
    }

    /* Validate the cert based on the host name and IP address. */
    result = validate_hostname(peer_cert, peer_info->name, peer_info->ipaddr);

    debug_return_int(result == MatchFound);
}

void
tls_connect_cb(int sock, int what, void *v)
{
    struct tls_client_closure *tls_client = v;
    struct sudo_event_base *evbase = tls_client->evbase;
    const struct timespec *timeout = NULL;
    const char *errstr;
    int con_stat;
    debug_decl(tls_connect_cb, SUDO_DEBUG_UTIL);

    if (what == SUDO_EV_TIMEOUT) {
        sudo_warnx("%s", U_("TLS handshake timeout occurred"));
        goto bad;
    }

    if (sudo_timespecisset(&tls_client->connect_timeout))
	timeout = &tls_client->connect_timeout;

    con_stat = SSL_connect(tls_client->ssl);

    if (con_stat == 1) {
	sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	    "SSL_connect successful");
        tls_client->tls_connect_state = true;
    } else {
        switch (SSL_get_error(tls_client->ssl, con_stat)) {
            /* TLS handshake is not finished, reschedule event */
            case SSL_ERROR_WANT_READ:
		sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
		    "SSL_connect returns SSL_ERROR_WANT_READ");
		if (what != SUDO_EV_READ) {
		    if (sudo_ev_set(tls_client->tls_connect_ev,
			    SSL_get_fd(tls_client->ssl), SUDO_EV_READ,
			    tls_connect_cb, tls_client) == -1) {
			sudo_warnx("%s", U_("unable to set event"));
			goto bad;
		    }
		}
                if (sudo_ev_add(evbase, tls_client->tls_connect_ev, timeout, false) == -1) {
                    sudo_warnx("%s", U_("unable to add event to queue"));
		    goto bad;
                }
		break;
            case SSL_ERROR_WANT_WRITE:
		sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
		    "SSL_connect returns SSL_ERROR_WANT_WRITE");
		if (what != SUDO_EV_WRITE) {
		    if (sudo_ev_set(tls_client->tls_connect_ev,
			    SSL_get_fd(tls_client->ssl), SUDO_EV_WRITE,
			    tls_connect_cb, tls_client) == -1) {
			sudo_warnx("%s", U_("unable to set event"));
			goto bad;
		    }
		}
                if (sudo_ev_add(evbase, tls_client->tls_connect_ev, timeout, false) == -1) {
                    sudo_warnx("%s", U_("unable to add event to queue"));
		    goto bad;
                }
		break;
	    case SSL_ERROR_SYSCALL:
                sudo_warnx(U_("TLS connection failed: %s"), strerror(errno));
		goto bad;
            default:
		errstr = ERR_reason_error_string(ERR_get_error());
                sudo_warnx(U_("TLS connection failed: %s"),
		    errstr ? errstr : strerror(errno));
                goto bad;
        }
    }

    if (tls_client->tls_connect_state) {
	sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	    "TLS version: %s, negotiated cipher suite: %s",
	    SSL_get_version(tls_client->ssl), SSL_get_cipher(tls_client->ssl));

	/* Done with TLS connect, send ClientHello */
	sudo_ev_free(tls_client->tls_connect_ev);
	tls_client->tls_connect_ev = NULL;
	if (!tls_client->start_fn(tls_client))
	    goto bad;
    }

    debug_return;

bad:
    tls_client->connect_error_fn(tls_client);
    debug_return;
}

bool
tls_ctx_client_setup(SSL_CTX *ssl_ctx, int sock,
    struct tls_client_closure *closure)
{
    const char *errstr;
    bool ret = false;
    debug_decl(tls_ctx_client_setup, SUDO_DEBUG_UTIL);

    SSL_free(closure->ssl);
    if ((closure->ssl = SSL_new(ssl_ctx)) == NULL) {
	errstr = ERR_reason_error_string(ERR_get_error());
        sudo_warnx(U_("unable to allocate ssl object: %s"),
	    errstr ? errstr : strerror(errno));
        goto done;
    }

    if (SSL_set_ex_data(closure->ssl, 1, closure->peer_name) <= 0) {
	errstr = ERR_reason_error_string(ERR_get_error());
	sudo_warnx(U_("Unable to attach user data to the ssl object: %s"),
	    errstr ? errstr : strerror(errno));
	goto done;
    }

    if (SSL_set_fd(closure->ssl, sock) <= 0) {
	errstr = ERR_reason_error_string(ERR_get_error());
        sudo_warnx(U_("Unable to attach socket to the ssl object: %s"),
	    errstr ? errstr : strerror(errno));
        goto done;
    }

    if (sudo_ev_add(closure->evbase, closure->tls_connect_ev, NULL, false) == -1) {
	sudo_warnx("%s", U_("unable to add event to queue"));
	goto done;
    }

    ret = true;

done:
    debug_return_bool(ret);
}

bool
tls_client_setup(int sock, const char *ca_bundle_file, const char *cert_file,
    const char *key_file, const char *dhparam_file, const char *ciphers_v12,
    const char *ciphers_v13, bool verify_server, bool check_peer,
    struct tls_client_closure *closure)
{
    SSL_CTX *ssl_ctx;
    debug_decl(tls_client_setup, SUDO_DEBUG_UTIL);

    ssl_ctx = init_tls_context(ca_bundle_file, cert_file, key_file,
	dhparam_file, ciphers_v12, ciphers_v13, verify_server);
    if (ssl_ctx == NULL) {
        sudo_warnx("%s", U_("unable to initialize TLS context"));
	debug_return_bool(false);
    }

    if (check_peer) {
	/* Verify server cert during the handshake. */
	SSL_CTX_set_verify(ssl_ctx,
	    SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
	    verify_peer_identity);
    }

    debug_return_bool(tls_ctx_client_setup(ssl_ctx, sock, closure));
}
#endif /* HAVE_OPENSSL */
