/*
 * Copyright (c) 2020 Laszlo Orban <laszlo.orban@oneidentity.com>
 *
 * 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>

#if defined(HAVE_OPENSSL)
# if defined(HAVE_WOLFSSL)
#  include <wolfssl/options.h>
# endif
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <stdlib.h>
# include <string.h>

# define NEED_INET_NTOP		/* to expose sudo_inet_ntop in sudo_compat.h */

# include  <sudo_compat.h>
# include  <sudo_debug.h>
# include  <sudo_util.h>
# include  <hostcheck.h>

#ifndef INET_ADDRSTRLEN
# define INET_ADDRSTRLEN 16
#endif
#ifndef INET6_ADDRSTRLEN
# define INET6_ADDRSTRLEN 46
#endif

#if !defined(HAVE_ASN1_STRING_GET0_DATA) && !defined(HAVE_WOLFSSL)
# define ASN1_STRING_get0_data(x)	ASN1_STRING_data(x)
#endif /* !HAVE_ASN1_STRING_GET0_DATA && !HAVE_WOLFSSL */

/**
 * @brief Compares the given hostname with a DNS entry in a certificate.
 *
 * The certificate DNS name can contain wildcards in the left-most label.
 * A wildcard can match only one label.
 * Accepted names:
 *  - foo.bar.example.com
 *  - *.example.com
 *  - *.bar.example.com
 *
 * @param hostname          peer's name
 * @param certname_asn1     hostname in the certificate
 *
 * @return  MatchFound
 *          MatchNotFound
 *          MalformedCertificate
 */
static HostnameValidationResult
validate_name(const char *hostname, ASN1_STRING *certname_asn1)
{
    const char *certname_s = (const char *)ASN1_STRING_get0_data(certname_asn1);
    size_t certname_len = (size_t)ASN1_STRING_length(certname_asn1);
    size_t hostname_len = strlen(hostname);
    debug_decl(validate_name, SUDO_DEBUG_UTIL);

    /* Make sure there isn't an embedded NUL character in certname */
    if (memchr(certname_s, '\0', certname_len) != NULL) {
	debug_return_int(MalformedCertificate);
    }

    /* Remove last '.' from hostname if it exists */
    if (hostname_len != 0 && hostname[hostname_len - 1] == '.') {
	--hostname_len;
    }

    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	"comparing %.*s to %.*s in cert", (int)hostname_len, hostname,
	(int)certname_len, certname_s);

    /* Skip the first label if wildcard */
    if (certname_len > 2 && certname_s[0] == '*' && certname_s[1] == '.') {
	while (hostname_len != 0) {
	    --hostname_len;
	    if (*hostname++ == '.') {
		break;
	    }
	}
	certname_s += 2;
	certname_len -= 2;
    }

    /* Compare expected hostname with the DNS name */
    if (certname_len != hostname_len) {
	    debug_return_int(MatchNotFound);
    }
    if (strncasecmp(hostname, certname_s, hostname_len) != 0) {
	debug_return_int(MatchNotFound);
    }

    debug_return_int(MatchFound);
}

/**
 * @brief Matches a hostname with the cert's CN.
 *
 * @param hostname  remote peer's name or NULL if no name
 * @param cert      peer's X509 certificate
 *
 * @return  MatchFound
 *          MatchNotFound
 *          MalformedCertificate
 *          Error
 */
static HostnameValidationResult
matches_common_name(const char *hostname, X509 *cert)
{
    X509_NAME_ENTRY *common_name_entry = NULL;
    ASN1_STRING *common_name_asn1 = NULL;
    int common_name_loc;
    debug_decl(matches_common_name, SUDO_DEBUG_UTIL);

    if (hostname == NULL) {
	debug_return_int(MatchNotFound);
    }

    /* Find the CN field's index in the Subject field of the certificate */
    common_name_loc = X509_NAME_get_index_by_NID(X509_get_subject_name(cert),
	NID_commonName, -1);
    if (common_name_loc < 0) {
	debug_return_int(Error);
    }

    /* Extract the CN field */
    common_name_entry = X509_NAME_get_entry(X509_get_subject_name(cert),
	common_name_loc);
    if (common_name_entry == NULL) {
	debug_return_int(Error);
    }

    /* Compare expected hostname with the CN */
    common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry);
    if (common_name_asn1 == NULL) {
	debug_return_int(Error);
    }			
    if (validate_name(hostname, common_name_asn1) == MatchFound) {
	debug_return_int(MatchFound);
    }

    debug_return_int(MatchNotFound);
}

/**
 * @brief Matches a hostname or ipaddr with the cert's corresponding SAN field.
 *
 * SAN can have different fields. For hostname matching, the GEN_DNS field is
 * used. For IP address matching, the GEN_IPADD field is used.
 * Since SAN is an X503 v3 extension, it may not be preseent in the cert.
 *
 * @param hostname  remote peer's name or NULL if no name
 * @param ipaddr    remote peer's IP address
 * @param cert      peer's X509 certificate
 *
 * @return  MatchFound
 *          MatchNotFound
 *          NoSANPresent
 *          MalformedCertificate
 *          Error
 */
static HostnameValidationResult
matches_subject_alternative_name(const char *hostname, const char *ipaddr,
    X509 *cert)
{
    HostnameValidationResult ret = MatchNotFound;
    STACK_OF(GENERAL_NAME) *san_names;
    int i, san_names_nb;
    debug_decl(matches_subject_alternative_name, SUDO_DEBUG_UTIL);

    /* Try to extract the names within the SAN extension from the certificate */
    san_names = X509_get_ext_d2i((X509 *) cert, NID_subject_alt_name, NULL, NULL);
    if (san_names == NULL) {
	debug_return_int(NoSANPresent);
    }
    san_names_nb = sk_GENERAL_NAME_num(san_names);

    /* Check each name within the extension */
    for (i = 0; i < san_names_nb; i++) {
	const GENERAL_NAME *current_name = sk_GENERAL_NAME_value(san_names, i);
	if (current_name->type == GEN_DNS && hostname != NULL) {
	    /* Compare expected hostname with the DNS name */
	    if (validate_name(hostname, current_name->d.dNSName) == MatchFound) {
		ret = MatchFound;
		break;
	    }
	} else if (current_name->type == GEN_IPADD && ipaddr != NULL) {
	    const unsigned char *san_ip =
		ASN1_STRING_get0_data(current_name->d.iPAddress);
#if defined(HAVE_STRUCT_IN6_ADDR)
	    char san_ip_str[INET6_ADDRSTRLEN];
#else
	    char san_ip_str[INET_ADDRSTRLEN];
#endif

	    /* IPV4 address */
	    if (ASN1_STRING_length(current_name->d.iPAddress) == 4) {
		if (inet_ntop(AF_INET, san_ip, san_ip_str, INET_ADDRSTRLEN) == NULL) {
		    ret = MalformedCertificate;
		    break;
		}
#if defined(HAVE_STRUCT_IN6_ADDR)
	    /* IPV6 address */
	    } else if (ASN1_STRING_length(current_name->d.iPAddress) == 16) {
		if (inet_ntop(AF_INET6, san_ip, san_ip_str, INET6_ADDRSTRLEN) == NULL) {
		    ret = MalformedCertificate;
		    break;
		}
#endif
	    } else {
		ret = MalformedCertificate;
		break;
	    }

	    if (strcasecmp(ipaddr, san_ip_str) == 0) {
		ret = MatchFound;
		break;
	    }
	}
    }
    sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free);

    debug_return_int(ret);
}

/**
 * @brief Perform hostname/IP validation on the given X509 certificate.
 *
 * According to RFC 6125 section 6.4.4, if the SAN field is present, it
 * must be checked first.  The certificate's CN field must only be checked
 * if no SAN field is present.
 *
 * @param cert      X509 certificate
 * @param hostname  remote peer's name or NULL if no name
 * @param ipaddr    remote peer's IP address
 *
 * @return  MatchFound
 *          MatchNotFound
 *          MalformedCertificate
 *          Error
 */
HostnameValidationResult
validate_hostname(X509 *cert, const char *hostname, const char *ipaddr)
{
    HostnameValidationResult ret;
    debug_decl(validate_hostname, SUDO_DEBUG_UTIL);

    /* Check SAN first if exists */
    ret = matches_subject_alternative_name(hostname, ipaddr, cert);

    /* RFC 6125 section 6.4.4 says only check CN if no SAN name is present */
    if (ret == NoSANPresent) {
	ret = matches_common_name(hostname, cert);
    }

    debug_return_int(ret);
}
#endif /* HAVE_OPENSSL */
