/* $Id: ncat_ssl.c 13714 2009-06-12 22:21:39Z david $ */

#include "nbase.h"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#ifdef HAVE_OPENSSL
#include "nsock.h"
#include "ncat.h"

#include <assert.h>
#include <stdio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>

static SSL_CTX *sslctx;

static int ssl_gen_cert(X509 **cert, EVP_PKEY **key);

/* Parameters for automatic key and certificate generation. */
enum {
    DEFAULT_KEY_BITS = 1024,
    DEFAULT_CERT_DURATION = 60 * 60 * 24 * 365,
};
#define CERTIFICATE_COMMENT "Automatically generated by Ncat. See http://nmap.org/ncat/."

SSL_CTX *setup_ssl_listen(void)
{
    SSL_METHOD *method;

    if (sslctx)
        goto done;

    SSL_library_init();
    OpenSSL_add_all_algorithms();
    ERR_load_crypto_strings();
    SSL_load_error_strings();

    /* RAND_status initializes the random number generator through a variety of
       platform-dependent methods, then returns 1 if there is enough entropy or
       0 otherwise. This seems to be a good platform-independent way of seeding
       the generator, as well as of refusing to continue without enough
       entropy. */
    if (!RAND_status())
        bye("Failed to seed OpenSSL PRNG (RAND_status returned false).");

    if (!(method = SSLv23_server_method()))
        bye("SSLv23_server_method(): %s.", ERR_error_string(ERR_get_error(), NULL));
    if (!(sslctx = SSL_CTX_new(method)))
        bye("SSL_CTX_new(): %s.", ERR_error_string(ERR_get_error(), NULL));

    SSL_CTX_set_options(sslctx, SSL_OP_ALL | SSL_OP_NO_SSLv2);

    /* Secure ciphers list taken from Nsock. */
    if (!SSL_CTX_set_cipher_list(sslctx, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"))
        bye("Unable to set OpenSSL cipher list: %s", ERR_error_string(ERR_get_error(), NULL));

    if (o.sslcert == NULL && o.sslkey == NULL) {
        X509 *cert;
        EVP_PKEY *key;
        char digest_buf[SHA1_STRING_LENGTH + 1];

        if (o.verbose)
            loguser("Generating a temporary %d-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.\n", DEFAULT_KEY_BITS);
        if (ssl_gen_cert(&cert, &key) == 0)
            bye("ssl_gen_cert(): %s.", ERR_error_string(ERR_get_error(), NULL));
        if (o.verbose) {
            assert(ssl_cert_fp_str_sha1(cert, digest_buf, sizeof(digest_buf)) != NULL);
            loguser("SHA-1 fingerprint: %s\n", digest_buf);
        }
        if (SSL_CTX_use_certificate(sslctx, cert) != 1)
            bye("SSL_CTX_use_certificate(): %s.", ERR_error_string(ERR_get_error(), NULL));
        if (SSL_CTX_use_PrivateKey(sslctx, key) != 1)
            bye("SSL_CTX_use_PrivateKey(): %s.", ERR_error_string(ERR_get_error(), NULL));
        X509_free(cert);
        EVP_PKEY_free(key);
    } else {
        if (o.sslcert == NULL || o.sslkey == NULL)
            bye("The --ssl-key and --ssl-cert options must be used together.");
        if (SSL_CTX_use_certificate_file(sslctx, o.sslcert, SSL_FILETYPE_PEM) != 1)
            bye("SSL_CTX_use_certificate_file(): %s.", ERR_error_string(ERR_get_error(), NULL));
        if (SSL_CTX_use_PrivateKey_file(sslctx, o.sslkey, SSL_FILETYPE_PEM) != 1)
            bye("SSL_CTX_use_Privatekey_file(): %s.", ERR_error_string(ERR_get_error(), NULL));
    }

done:
    return sslctx;
}

SSL *new_ssl(int fd)
{
    SSL *ssl;

    if (!(ssl = SSL_new(sslctx)))
        bye("SSL_new(): %s.", ERR_error_string(ERR_get_error(), NULL));
    if (!SSL_set_fd(ssl, fd))
        bye("SSL_set_fd(): %s.", ERR_error_string(ERR_get_error(), NULL));

    return ssl;
}

/* Match a hostname against the contents of a dNSName field of the
   subjectAltName extension, if present. This is the preferred place for a
   certificate to store its domain name, as opposed to in the commonName field.
   It has the advantage that multiple names can be stored, so that one
   certificate can match both "example.com" and "www.example.com". This function
   doesn't do wildcard matching. */
static int cert_match_dnsname(X509 *cert, const char *hostname)
{
    X509_EXTENSION *ext;
    void *ext_str;
    X509V3_EXT_METHOD *method;
    STACK_OF(CONF_VALUE) *nvalstack;
    CONF_VALUE *nval;
    int i;

    i = X509_get_ext_by_NID(cert, NID_subject_alt_name, 0);
    if (i < 0)
        return 0;
    ext = X509_get_ext(cert, i);

    /* See the function X509V3_EXT_print in the OpenSSL source for this method
       of getting a string value from an extension. */
    method = X509V3_EXT_get(ext);
    if (method == NULL)
        return 0;
#if (OPENSSL_VERSION_NUMBER > 0x00907000L)
    if (method->it != NULL) {
        ext_str = ASN1_item_d2i(NULL,
            (const unsigned char **) &ext->value->data,
            ext->value->length, ASN1_ITEM_ptr(method->it));
    } else {
        ext_str = method->d2i(NULL,
            (const unsigned char **) &ext->value->data,
            ext->value->length);
    }
#else
    ext_str = method->d2i(NULL,
        (const unsigned char **) &ext->value->data,
        ext->value->length);
#endif
    if (ext_str == NULL)
        return 0;

    /* See X509V3_EXT_val_prn. */
    nvalstack = method->i2v(method, ext_str, NULL);
    if (nvalstack == NULL)
        return 0;
    /* Look for a dNSName field with a matching hostname. There may be more than
       one dNSName field. */
    for (i = 0; i < sk_CONF_VALUE_num(nvalstack); i++) {
        nval = sk_CONF_VALUE_value(nvalstack, i);
        if (o.debug > 1)
            logdebug("Checking certificate DNS name \"%s\" against \"%s\".\n", nval->value, hostname);
        if (nval->name != NULL && strcmp(nval->name, "DNS") == 0
            && strcmp(nval->value, hostname) == 0) {
            return 1;
        }
    }

    return 0;
}

/* Match a hostname against the contents of the commonName field of a
   certificate. */
static int cert_match_commonname(X509 *cert, const char *hostname)
{
    X509_NAME *subject;
    char buf[256];
    int n;

    subject = X509_get_subject_name(cert);
    if (subject == NULL)
        return 0;

    /* First check if the buffer is big enough. */
    n = X509_NAME_get_text_by_NID(subject, NID_commonName, NULL, 0);
    if (n > sizeof(buf) - 1) {
        if (o.verbose)
            logdebug("Can't get certificate commonName; need %d bytes, have %d.\n", n, sizeof(buf));
        return 0;
    }
    n = X509_NAME_get_text_by_NID(subject, NID_commonName, buf, sizeof(buf));
    if (n < 0 || n > sizeof(buf) - 1)
        return 0;
    /* Check the name length because I'm concerned about someone somehow
       embedding a '\0' in the subject and matching against a shorter name. */
    if (n == strlen(hostname) && strcmp(buf, hostname) == 0)
        return 1;

    if (o.verbose)
        loguser("Certificate verification error: Connected to \"%s\", but certificate is for \"%s\".\n", hostname, buf);

    return 0;
}

/* Verify a host's name against the name in its certificate after connection.
   If the verify mode is SSL_VERIFY_NONE, always returns true. Returns nonzero
   on success. */
int ssl_post_connect_check(SSL *ssl, const char *hostname)
{
    X509 *cert = NULL;

    if (SSL_get_verify_mode(ssl) == SSL_VERIFY_NONE)
        return 1;

    if (hostname == NULL)
        return 0;

    cert = SSL_get_peer_certificate(ssl);
    if (cert == NULL)
        return 0;

    /* RFC 2818 (HTTP Over TLS): If a subjectAltName extension of type dNSName
       is present, that MUST be used as the identity. Otherwise, the (most
       specific) Common Name field in the Subject field of the certificate MUST
       be used. Although the use of the Common Name is existing practice, it is
       deprecated and Certification Authorities are encouraged to use the
       dNSName instead. */
    if (!cert_match_dnsname(cert, hostname) && !cert_match_commonname(cert, hostname)) {
        X509_free(cert);
        return 0;
    }

    X509_free(cert);

    return SSL_get_verify_result(ssl) == X509_V_OK;
}

/* Generate a self-signed certificate and matching RSA keypair. References for
   this code are the book Network Programming with OpenSSL, chapter 10, section
   "Making Certificates"; and apps/req.c in the OpenSSL source. */
static int ssl_gen_cert(X509 **cert, EVP_PKEY **key)
{
    RSA *rsa;
    X509_NAME *subj;
    X509_EXTENSION *ext;
    X509V3_CTX ctx;
    const char *commonName = "localhost";
    char dNSName[128];
    int rc;

    *cert = NULL;
    *key = NULL;

    /* Generate a private key. */
    *key = EVP_PKEY_new();
    if (*key == NULL)
        goto err;
    do {
        rsa = RSA_generate_key(DEFAULT_KEY_BITS, RSA_F4, NULL, NULL);
        if (rsa == NULL)
            goto err;
        rc = RSA_check_key(rsa);
    } while (rc == 0);
    if (rc == -1)
        bye("Error generating RSA key: %s", ERR_error_string(ERR_get_error(), NULL));
    if (EVP_PKEY_assign_RSA(*key, rsa) == 0) {
        RSA_free(rsa);
        goto err;
    }

    /* Generate a certificate. */
    *cert = X509_new();
    if (*cert == NULL)
        goto err;
    if (X509_set_version(*cert, 2) == 0) /* Version 3. */
        goto err;
    ASN1_INTEGER_set(X509_get_serialNumber(*cert), get_random_u32());

    /* Set the commonName. */
    subj = X509_get_subject_name(*cert);
    if (o.target != NULL)
        commonName = o.target;
    if (X509_NAME_add_entry_by_txt(subj, "commonName", MBSTRING_ASC,
        (unsigned char *) commonName, -1, -1, 0) == 0) {
        goto err;
    }

    /* Set the dNSName. */
    rc = Snprintf(dNSName, sizeof(dNSName), "DNS:%s", commonName);
    if (rc < 0 || rc >= sizeof(dNSName))
        goto err;
    X509V3_set_ctx(&ctx, *cert, *cert, NULL, NULL, 0);
    ext = X509V3_EXT_conf(NULL, &ctx, "subjectAltName", dNSName);
    if (ext == NULL)
        goto err;
    if (X509_add_ext(*cert, ext, -1) == 0)
        goto err;

    /* Set a comment. */
    ext = X509V3_EXT_conf(NULL, &ctx, "nsComment", CERTIFICATE_COMMENT);
    if (ext == NULL)
        goto err;
    if (X509_add_ext(*cert, ext, -1) == 0)
        goto err;

    if (X509_set_issuer_name(*cert, X509_get_subject_name(*cert)) == 0
        || X509_gmtime_adj(X509_get_notBefore(*cert), 0) == 0
        || X509_gmtime_adj(X509_get_notAfter(*cert), DEFAULT_CERT_DURATION) == 0
        || X509_set_pubkey(*cert, *key) == 0) {
        goto err;
    }

    /* Sign it. */
    if (X509_sign(*cert, *key, EVP_sha1()) == 0)
        goto err;

    return 1;

err:
    if (*cert != NULL)
        X509_free(*cert);
    if (*key != NULL)
        EVP_PKEY_free(*key);

    return 0;
}

/* Calculate a SHA-1 fingerprint of a certificate and format it as a
   human-readable string. Returns strbuf or NULL on error. */
char *ssl_cert_fp_str_sha1(const X509 *cert, char *strbuf, size_t len)
{
    unsigned char binbuf[SHA1_BYTES];
    size_t n;
    char *p;
    unsigned int i;

    if (len < SHA1_STRING_LENGTH + 1)
        return NULL;
    n = sizeof(binbuf);
    if (X509_digest(cert, EVP_sha1(), binbuf, &n) != 1)
        return NULL;

    p = strbuf;
    for (i = 0; i < n; i++) {
        if (i > 0 && i % 2 == 0)
            *p++ = ' ';
        Snprintf(p, 3, "%02X", binbuf[i]);
        p += 2;
    }
    assert(p - strbuf <= len);
    *p = '\0';

    return strbuf;
}
#endif
