summaryrefslogtreecommitdiffstats
path: root/src/core/network-openssl.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/network-openssl.c')
-rw-r--r--src/core/network-openssl.c944
1 files changed, 944 insertions, 0 deletions
diff --git a/src/core/network-openssl.c b/src/core/network-openssl.c
new file mode 100644
index 0000000..9956f21
--- /dev/null
+++ b/src/core/network-openssl.c
@@ -0,0 +1,944 @@
+/*
+ network-ssl.c : SSL support
+
+ Copyright (C) 2002 vjt
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "module.h"
+#include <irssi/src/core/network.h>
+#include <irssi/src/core/network-openssl.h>
+#include <irssi/src/core/net-sendbuffer.h>
+#include <irssi/src/core/misc.h>
+#include <irssi/src/core/servers.h>
+#include <irssi/src/core/signals.h>
+#include <irssi/src/core/tls.h>
+
+#include <openssl/crypto.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+#include <openssl/pem.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+
+/* OpenSSL 1.1.0 introduced some backward-incompatible changes to the api */
+#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) && \
+ (!defined(LIBRESSL_VERSION_NUMBER) || LIBRESSL_VERSION_NUMBER < 0x2070000fL)
+/* The two functions below could be already defined if OPENSSL_API_COMPAT is
+ * below the 1.1.0 version so let's do a clean start */
+#undef X509_get_notBefore
+#undef X509_get_notAfter
+#define X509_get_notBefore(x) X509_get0_notBefore(x)
+#define X509_get_notAfter(x) X509_get0_notAfter(x)
+#define ASN1_STRING_data(x) ASN1_STRING_get0_data(x)
+#endif
+
+/* OpenSSL 1.1.0 also introduced some useful additions to the api */
+#if (OPENSSL_VERSION_NUMBER >= 0x10002000L)
+#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \
+ (defined (LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x2070000fL)
+static int X509_STORE_up_ref(X509_STORE *vfy)
+{
+ int n;
+
+ n = CRYPTO_add(&vfy->references, 1, CRYPTO_LOCK_X509_STORE);
+ g_assert(n > 1);
+
+ return (n > 1) ? 1 : 0;
+}
+#endif
+#endif
+
+/* ssl i/o channel object */
+typedef struct
+{
+ GIOChannel pad;
+ gint fd;
+ GIOChannel *giochan;
+ SSL *ssl;
+ SSL_CTX *ctx;
+ unsigned int verify:1;
+ SERVER_REC *server;
+ int port;
+} GIOSSLChannel;
+
+static int ssl_inited = FALSE;
+/* https://github.com/irssi/irssi/issues/820 */
+#if (OPENSSL_VERSION_NUMBER >= 0x10002000L)
+static X509_STORE *store = NULL;
+#endif
+
+static void irssi_ssl_free(GIOChannel *handle)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+ g_io_channel_unref(chan->giochan);
+ SSL_free(chan->ssl);
+ SSL_CTX_free(chan->ctx);
+ g_free(chan);
+}
+
+/* Checks if the given string has internal NUL characters. */
+static gboolean has_internal_nul(const char* str, int len) {
+ /* Remove trailing nul characters. They would give false alarms */
+ while (len > 0 && str[len-1] == 0)
+ len--;
+ return strlen(str) != len;
+}
+
+/* tls_dns_name - Extract valid DNS name from subjectAltName value */
+static const char *tls_dns_name(const GENERAL_NAME * gn)
+{
+ const char *dnsname;
+
+ /* We expect the OpenSSL library to construct GEN_DNS extension objects as
+ ASN1_IA5STRING values. Check we got the right union member. */
+ if (ASN1_STRING_type(gn->d.ia5) != V_ASN1_IA5STRING) {
+ g_warning("Invalid ASN1 value type in subjectAltName");
+ return NULL;
+ }
+
+ /* Safe to treat as an ASCII string possibly holding a DNS name */
+ dnsname = (char *) ASN1_STRING_data(gn->d.ia5);
+
+ if (has_internal_nul(dnsname, ASN1_STRING_length(gn->d.ia5))) {
+ g_warning("Internal NUL in subjectAltName");
+ return NULL;
+ }
+
+ return dnsname;
+}
+
+/* tls_text_name - extract certificate property value by name */
+static char *tls_text_name(X509_NAME *name, int nid)
+{
+ int pos;
+ X509_NAME_ENTRY *entry;
+ ASN1_STRING *entry_str;
+ int utf8_length;
+ unsigned char *utf8_value;
+ char *result;
+
+ if (name == 0 || (pos = X509_NAME_get_index_by_NID(name, nid, -1)) < 0) {
+ return NULL;
+ }
+
+ entry = X509_NAME_get_entry(name, pos);
+ g_return_val_if_fail(entry != NULL, NULL);
+ entry_str = X509_NAME_ENTRY_get_data(entry);
+ g_return_val_if_fail(entry_str != NULL, NULL);
+
+ /* Convert everything into UTF-8. It's up to OpenSSL to do something
+ reasonable when converting ASCII formats that contain non-ASCII
+ content. */
+ if ((utf8_length = ASN1_STRING_to_UTF8(&utf8_value, entry_str)) < 0) {
+ g_warning("Error decoding ASN.1 type=%d", ASN1_STRING_type(entry_str));
+ return NULL;
+ }
+
+ if (has_internal_nul((char *)utf8_value, utf8_length)) {
+ g_warning("NUL character in hostname in certificate");
+ OPENSSL_free(utf8_value);
+ return NULL;
+ }
+
+ result = g_strdup((char *) utf8_value);
+ OPENSSL_free(utf8_value);
+ return result;
+}
+
+
+/** check if a hostname in the certificate matches the hostname we used for the connection */
+static gboolean match_hostname(const char *cert_hostname, const char *hostname)
+{
+ const char *hostname_left;
+
+ if (!strcasecmp(cert_hostname, hostname)) { /* exact match */
+ return TRUE;
+ } else if (cert_hostname[0] == '*' && cert_hostname[1] == '.' && cert_hostname[2] != 0) { /* wildcard match */
+ /* The initial '*' matches exactly one hostname component */
+ hostname_left = strchr(hostname, '.');
+ if (hostname_left != NULL && ! strcasecmp(hostname_left + 1, cert_hostname + 2)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/* based on verify_extract_name from tls_client.c in postfix */
+static gboolean irssi_ssl_verify_hostname(X509 *cert, const char *hostname)
+{
+ int gen_index, gen_count;
+ gboolean matched = FALSE, has_dns_name = FALSE;
+ const char *cert_dns_name;
+ char *cert_subject_cn;
+ const GENERAL_NAME *gn;
+ STACK_OF(GENERAL_NAME) * gens;
+
+ /* Verify the dNSName(s) in the peer certificate against the hostname. */
+ gens = X509_get_ext_d2i(cert, NID_subject_alt_name, 0, 0);
+ if (gens) {
+ gen_count = sk_GENERAL_NAME_num(gens);
+ for (gen_index = 0; gen_index < gen_count && !matched; ++gen_index) {
+ gn = sk_GENERAL_NAME_value(gens, gen_index);
+ if (gn->type != GEN_DNS)
+ continue;
+
+ /* Even if we have an invalid DNS name, we still ultimately
+ ignore the CommonName, because subjectAltName:DNS is
+ present (though malformed). */
+ has_dns_name = TRUE;
+ cert_dns_name = tls_dns_name(gn);
+ if (cert_dns_name && *cert_dns_name) {
+ matched = match_hostname(cert_dns_name, hostname);
+ }
+ }
+
+ /* Free stack *and* member GENERAL_NAME objects */
+ sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free);
+ }
+
+ if (has_dns_name) {
+ if (! matched) {
+ /* The CommonName in the issuer DN is obsolete when SubjectAltName is available. */
+ g_warning("None of the Subject Alt Names in the certificate match hostname '%s'", hostname);
+ }
+ return matched;
+ } else { /* No subjectAltNames, look at CommonName */
+ cert_subject_cn = tls_text_name(X509_get_subject_name(cert), NID_commonName);
+ if (cert_subject_cn && *cert_subject_cn) {
+ matched = match_hostname(cert_subject_cn, hostname);
+ if (! matched) {
+ g_warning("SSL certificate common name '%s' doesn't match host name '%s'", cert_subject_cn, hostname);
+ }
+ } else {
+ g_warning("No subjectAltNames and no valid common name in certificate");
+ }
+ g_free(cert_subject_cn);
+ }
+
+ return matched;
+}
+
+static gboolean irssi_ssl_verify(SSL *ssl, SSL_CTX *ctx, const char* hostname, int port, X509 *cert, SERVER_REC *server, TLS_REC *tls_rec)
+{
+ long result;
+
+ result = SSL_get_verify_result(ssl);
+ if (result != X509_V_OK) {
+ g_warning("Could not verify TLS servers certificate: %s", X509_verify_cert_error_string(result));
+ return FALSE;
+ } else if (! irssi_ssl_verify_hostname(cert, hostname)){
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static GIOStatus irssi_ssl_read(GIOChannel *handle, gchar *buf, gsize len, gsize *ret, GError **gerr)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+ gint ret1, err;
+ const char *errstr;
+ gchar *errmsg;
+
+ ERR_clear_error();
+ ret1 = SSL_read(chan->ssl, buf, len);
+ if(ret1 <= 0)
+ {
+ *ret = 0;
+ err = SSL_get_error(chan->ssl, ret1);
+ if(err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
+ return G_IO_STATUS_AGAIN;
+ else if(err == SSL_ERROR_ZERO_RETURN)
+ return G_IO_STATUS_EOF;
+ else if (err == SSL_ERROR_SYSCALL)
+ {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ if (errstr == NULL && ret1 == -1)
+ errstr = strerror(errno);
+ if (errstr == NULL)
+ errstr = "server closed connection unexpectedly";
+ }
+ else
+ {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ if (errstr == NULL)
+ errstr = "unknown SSL error";
+ }
+ errmsg = g_strdup_printf("SSL read error: %s", errstr);
+ *gerr = g_error_new_literal(G_IO_CHANNEL_ERROR, G_IO_CHANNEL_ERROR_FAILED,
+ errmsg);
+ g_free(errmsg);
+ return G_IO_STATUS_ERROR;
+ }
+ else
+ {
+ *ret = ret1;
+ return G_IO_STATUS_NORMAL;
+ }
+ /*UNREACH*/
+ return G_IO_STATUS_ERROR;
+}
+
+static GIOStatus irssi_ssl_write(GIOChannel *handle, const gchar *buf, gsize len, gsize *ret, GError **gerr)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+ gint ret1, err;
+ const char *errstr;
+ gchar *errmsg;
+
+ ERR_clear_error();
+ ret1 = SSL_write(chan->ssl, (const char *)buf, len);
+ if(ret1 <= 0)
+ {
+ *ret = 0;
+ err = SSL_get_error(chan->ssl, ret1);
+ if(err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
+ return G_IO_STATUS_AGAIN;
+ else if(err == SSL_ERROR_ZERO_RETURN)
+ errstr = "server closed connection";
+ else if (err == SSL_ERROR_SYSCALL)
+ {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ if (errstr == NULL && ret1 == -1)
+ errstr = strerror(errno);
+ if (errstr == NULL)
+ errstr = "server closed connection unexpectedly";
+ }
+ else
+ {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ if (errstr == NULL)
+ errstr = "unknown SSL error";
+ }
+ errmsg = g_strdup_printf("SSL write error: %s", errstr);
+ *gerr = g_error_new_literal(G_IO_CHANNEL_ERROR, G_IO_CHANNEL_ERROR_FAILED,
+ errmsg);
+ g_free(errmsg);
+ return G_IO_STATUS_ERROR;
+ }
+ else
+ {
+ *ret = ret1;
+ return G_IO_STATUS_NORMAL;
+ }
+ /*UNREACH*/
+ return G_IO_STATUS_ERROR;
+}
+
+static GIOStatus irssi_ssl_seek(GIOChannel *handle, gint64 offset, GSeekType type, GError **gerr)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+
+ return chan->giochan->funcs->io_seek(handle, offset, type, gerr);
+}
+
+static GIOStatus irssi_ssl_close(GIOChannel *handle, GError **gerr)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+
+ return chan->giochan->funcs->io_close(handle, gerr);
+}
+
+static GSource *irssi_ssl_create_watch(GIOChannel *handle, GIOCondition cond)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+
+ return chan->giochan->funcs->io_create_watch(handle, cond);
+}
+
+static GIOStatus irssi_ssl_set_flags(GIOChannel *handle, GIOFlags flags, GError **gerr)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+
+ return chan->giochan->funcs->io_set_flags(handle, flags, gerr);
+}
+
+static GIOFlags irssi_ssl_get_flags(GIOChannel *handle)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+
+ return chan->giochan->funcs->io_get_flags(handle);
+}
+
+static GIOFuncs irssi_ssl_channel_funcs = {
+ irssi_ssl_read,
+ irssi_ssl_write,
+ irssi_ssl_seek,
+ irssi_ssl_close,
+ irssi_ssl_create_watch,
+ irssi_ssl_free,
+ irssi_ssl_set_flags,
+ irssi_ssl_get_flags
+};
+
+gboolean irssi_ssl_init(void)
+{
+#if (OPENSSL_VERSION_NUMBER >= 0x10002000L)
+ int success;
+#endif
+
+#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) && !defined(LIBRESSL_VERSION_NUMBER)
+ if (!OPENSSL_init_ssl(OPENSSL_INIT_SSL_DEFAULT, NULL)) {
+ g_error("Could not initialize OpenSSL");
+ return FALSE;
+ }
+#else
+ SSL_library_init();
+ SSL_load_error_strings();
+ OpenSSL_add_all_algorithms();
+#endif
+
+#if (OPENSSL_VERSION_NUMBER >= 0x10002000L)
+ store = X509_STORE_new();
+ if (store == NULL) {
+ g_error("Could not initialize OpenSSL: X509_STORE_new() failed");
+ return FALSE;
+ }
+
+ success = X509_STORE_set_default_paths(store);
+ if (success == 0) {
+ g_warning("Could not load default certificates");
+ X509_STORE_free(store);
+ store = NULL;
+ /* Don't return an error; the user might have their own cafile/capath. */
+ }
+#endif
+
+ ssl_inited = TRUE;
+
+ return TRUE;
+}
+
+static int get_pem_password_callback(char *buffer, int max_length, int rwflag, void *pass)
+{
+ char *password;
+ size_t length;
+
+ if (pass == NULL)
+ return 0;
+
+ password = (char *)pass;
+ length = strlen(pass);
+
+ if (length > max_length)
+ return 0;
+
+ memcpy(buffer, password, length + 1);
+ return length;
+}
+
+static GIOChannel *irssi_ssl_get_iochannel(GIOChannel *handle, int port, SERVER_REC *server)
+{
+ GIOSSLChannel *chan;
+ GIOChannel *gchan;
+ int fd;
+ SSL *ssl;
+ SSL_CTX *ctx = NULL;
+
+ const char *mycert = server->connrec->tls_cert;
+ const char *mypkey = server->connrec->tls_pkey;
+ const char *mypass = server->connrec->tls_pass;
+ const char *cafile = server->connrec->tls_cafile;
+ const char *capath = server->connrec->tls_capath;
+ const char *ciphers = server->connrec->tls_ciphers;
+ gboolean verify = server->connrec->tls_verify;
+
+ g_return_val_if_fail(handle != NULL, NULL);
+
+ if(!ssl_inited && !irssi_ssl_init())
+ return NULL;
+
+ if(!(fd = g_io_channel_unix_get_fd(handle)))
+ return NULL;
+
+ ERR_clear_error();
+ ctx = SSL_CTX_new(SSLv23_client_method());
+ if (ctx == NULL) {
+ g_error("Could not allocate memory for SSL context");
+ return NULL;
+ }
+ SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+ SSL_CTX_set_default_passwd_cb(ctx, get_pem_password_callback);
+ SSL_CTX_set_default_passwd_cb_userdata(ctx, (void *)mypass);
+
+ if (ciphers != NULL && ciphers[0] != '\0') {
+ if (SSL_CTX_set_cipher_list(ctx, ciphers) != 1)
+ g_warning("No valid SSL cipher suite could be selected");
+ }
+
+ if (mycert && *mycert) {
+ char *scert = NULL, *spkey = NULL;
+ FILE *fp;
+ scert = convert_home(mycert);
+ if (mypkey && *mypkey)
+ spkey = convert_home(mypkey);
+
+ if ((fp = fopen(scert, "r"))) {
+ X509 *cert;
+ /* Let's parse the certificate by hand instead of using
+ * SSL_CTX_use_certificate_file so that we can validate
+ * some parts of it. */
+ cert = PEM_read_X509(fp, NULL, get_pem_password_callback, (void *)mypass);
+ if (cert != NULL) {
+ /* Only the expiration date is checked right now */
+ if (X509_cmp_current_time(X509_get_notAfter(cert)) <= 0 ||
+ X509_cmp_current_time(X509_get_notBefore(cert)) >= 0)
+ g_warning("The client certificate is expired");
+
+ ERR_clear_error();
+ if (! SSL_CTX_use_certificate(ctx, cert))
+ g_warning("Loading of client certificate '%s' failed: %s", mycert, ERR_reason_error_string(ERR_get_error()));
+ else if (! SSL_CTX_use_PrivateKey_file(ctx, spkey ? spkey : scert, SSL_FILETYPE_PEM))
+ g_warning("Loading of private key '%s' failed: %s", mypkey ? mypkey : mycert, ERR_reason_error_string(ERR_get_error()));
+ else if (! SSL_CTX_check_private_key(ctx))
+ g_warning("Private key does not match the certificate");
+
+ X509_free(cert);
+ } else
+ g_warning("Loading of client certificate '%s' failed: %s", mycert, ERR_reason_error_string(ERR_get_error()));
+
+ fclose(fp);
+ } else
+ g_warning("Could not find client certificate '%s'", scert);
+ g_free(scert);
+ g_free(spkey);
+ }
+
+ if ((cafile && *cafile) || (capath && *capath)) {
+ char *scafile = NULL;
+ char *scapath = NULL;
+ if (cafile && *cafile)
+ scafile = convert_home(cafile);
+ if (capath && *capath)
+ scapath = convert_home(capath);
+ if (! SSL_CTX_load_verify_locations(ctx, scafile, scapath)) {
+ g_warning("Could not load CA list for verifying TLS server certificate");
+ g_free(scafile);
+ g_free(scapath);
+ SSL_CTX_free(ctx);
+ return NULL;
+ }
+ g_free(scafile);
+ g_free(scapath);
+ verify = TRUE;
+ }
+#if (OPENSSL_VERSION_NUMBER >= 0x10002000L)
+ else if (store != NULL) {
+ /* Make sure to increment the refcount every time the store is
+ * used, that's essential not to get it free'd by OpenSSL when
+ * the SSL_CTX is destroyed. */
+ X509_STORE_up_ref(store);
+ SSL_CTX_set_cert_store(ctx, store);
+ }
+#else
+ else {
+ if (!SSL_CTX_set_default_verify_paths(ctx))
+ g_warning("Could not load default certificates");
+ }
+#endif
+
+ if(!(ssl = SSL_new(ctx)))
+ {
+ g_warning("Failed to allocate SSL structure");
+ SSL_CTX_free(ctx);
+ return NULL;
+ }
+
+ if(!SSL_set_fd(ssl, fd))
+ {
+ g_warning("Failed to associate socket to SSL stream");
+ SSL_free(ssl);
+ SSL_CTX_free(ctx);
+ return NULL;
+ }
+
+#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
+ SSL_set_tlsext_host_name(ssl, server->connrec->address);
+#endif
+
+ SSL_set_mode(ssl, SSL_MODE_ENABLE_PARTIAL_WRITE |
+ SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+
+ chan = g_new0(GIOSSLChannel, 1);
+ chan->fd = fd;
+ chan->giochan = handle;
+ chan->ssl = ssl;
+ chan->ctx = ctx;
+ chan->server = server;
+ chan->port = port;
+ chan->verify = verify;
+
+ gchan = (GIOChannel *)chan;
+ gchan->funcs = &irssi_ssl_channel_funcs;
+ g_io_channel_init(gchan);
+ gchan->is_readable = gchan->is_writeable = TRUE;
+ gchan->use_buffer = FALSE;
+
+ return gchan;
+}
+
+static void set_cipher_info(TLS_REC *tls, SSL *ssl)
+{
+ g_return_if_fail(tls != NULL);
+ g_return_if_fail(ssl != NULL);
+
+ tls_rec_set_protocol_version(tls, SSL_get_version(ssl));
+
+ tls_rec_set_cipher(tls, SSL_CIPHER_get_name(SSL_get_current_cipher(ssl)));
+ tls_rec_set_cipher_size(tls, SSL_get_cipher_bits(ssl, NULL));
+}
+
+static gboolean set_pubkey_info(TLS_REC *tls, X509 *cert, unsigned char *cert_fingerprint, size_t cert_fingerprint_size, unsigned char *public_key_fingerprint, size_t public_key_fingerprint_size)
+{
+ gboolean ret = TRUE;
+ EVP_PKEY *pubkey = NULL;
+ char *cert_fingerprint_hex = NULL;
+ char *public_key_fingerprint_hex = NULL;
+
+ BIO *bio = NULL;
+ char buffer[128];
+ ssize_t length;
+
+ g_return_val_if_fail(tls != NULL, FALSE);
+ g_return_val_if_fail(cert != NULL, FALSE);
+
+ pubkey = X509_get_pubkey(cert);
+
+ cert_fingerprint_hex = binary_to_hex(cert_fingerprint, cert_fingerprint_size);
+ tls_rec_set_certificate_fingerprint(tls, cert_fingerprint_hex);
+ tls_rec_set_certificate_fingerprint_algorithm(tls, "SHA256");
+
+ /* Show algorithm. */
+ switch (EVP_PKEY_id(pubkey)) {
+ case EVP_PKEY_RSA:
+ tls_rec_set_public_key_algorithm(tls, "RSA");
+ break;
+
+ case EVP_PKEY_DSA:
+ tls_rec_set_public_key_algorithm(tls, "DSA");
+ break;
+
+ case EVP_PKEY_EC:
+ tls_rec_set_public_key_algorithm(tls, "EC");
+ break;
+
+ default:
+ tls_rec_set_public_key_algorithm(tls, "Unknown");
+ break;
+ }
+
+ public_key_fingerprint_hex = binary_to_hex(public_key_fingerprint, public_key_fingerprint_size);
+ tls_rec_set_public_key_fingerprint(tls, public_key_fingerprint_hex);
+ tls_rec_set_public_key_size(tls, EVP_PKEY_bits(pubkey));
+ tls_rec_set_public_key_fingerprint_algorithm(tls, "SHA256");
+
+ /* Read the NotBefore timestamp. */
+ bio = BIO_new(BIO_s_mem());
+ ASN1_TIME_print(bio, X509_get_notBefore(cert));
+ length = BIO_read(bio, buffer, sizeof(buffer));
+ if (length < 0) {
+ ret = FALSE;
+ BIO_free(bio);
+ goto done;
+ }
+ buffer[length] = '\0';
+ BIO_free(bio);
+ tls_rec_set_not_before(tls, buffer);
+
+ /* Read the NotAfter timestamp. */
+ bio = BIO_new(BIO_s_mem());
+ ASN1_TIME_print(bio, X509_get_notAfter(cert));
+ length = BIO_read(bio, buffer, sizeof(buffer));
+ if (length < 0) {
+ ret = FALSE;
+ BIO_free(bio);
+ goto done;
+ }
+ buffer[length] = '\0';
+ BIO_free(bio);
+ tls_rec_set_not_after(tls, buffer);
+
+done:
+ g_free(cert_fingerprint_hex);
+ g_free(public_key_fingerprint_hex);
+ EVP_PKEY_free(pubkey);
+
+ return ret;
+}
+
+static void set_peer_cert_chain_info(TLS_REC *tls, SSL *ssl)
+{
+ int nid;
+ char *key = NULL;
+ char *value = NULL;
+ STACK_OF(X509) *chain = NULL;
+ int i;
+ int j;
+ TLS_CERT_REC *cert_rec = NULL;
+ X509_NAME *name = NULL;
+ X509_NAME_ENTRY *entry = NULL;
+ TLS_CERT_ENTRY_REC *tls_cert_entry_rec = NULL;
+ ASN1_STRING *data = NULL;
+
+ g_return_if_fail(tls != NULL);
+ g_return_if_fail(ssl != NULL);
+
+ chain = SSL_get_peer_cert_chain(ssl);
+
+ if (chain == NULL)
+ return;
+
+ for (i = 0; i < sk_X509_num(chain); i++) {
+ cert_rec = tls_cert_create_rec();
+
+ /* Subject. */
+ name = X509_get_subject_name(sk_X509_value(chain, i));
+
+ for (j = 0; j < X509_NAME_entry_count(name); j++) {
+ entry = X509_NAME_get_entry(name, j);
+
+ nid = OBJ_obj2nid(X509_NAME_ENTRY_get_object(entry));
+ key = (char *)OBJ_nid2sn(nid);
+
+ if (key == NULL)
+ key = (char *)OBJ_nid2ln(nid);
+
+ data = X509_NAME_ENTRY_get_data(entry);
+ value = (char *)ASN1_STRING_data(data);
+
+ tls_cert_entry_rec = tls_cert_entry_create_rec(key, value);
+ tls_cert_rec_append_subject_entry(cert_rec, tls_cert_entry_rec);
+ }
+
+ /* Issuer. */
+ name = X509_get_issuer_name(sk_X509_value(chain, i));
+
+ for (j = 0; j < X509_NAME_entry_count(name); j++) {
+ entry = X509_NAME_get_entry(name, j);
+
+ nid = OBJ_obj2nid(X509_NAME_ENTRY_get_object(entry));
+ key = (char *)OBJ_nid2sn(nid);
+
+ if (key == NULL)
+ key = (char *)OBJ_nid2ln(nid);
+
+ data = X509_NAME_ENTRY_get_data(entry);
+ value = (char *)ASN1_STRING_data(data);
+
+ tls_cert_entry_rec = tls_cert_entry_create_rec(key, value);
+ tls_cert_rec_append_issuer_entry(cert_rec, tls_cert_entry_rec);
+ }
+
+ tls_rec_append_cert(tls, cert_rec);
+ }
+}
+
+static void set_server_temporary_key_info(TLS_REC *tls, SSL *ssl)
+{
+#ifdef SSL_get_server_tmp_key
+ /* Show ephemeral key information. */
+ EVP_PKEY *ephemeral_key = NULL;
+
+ /* OPENSSL_NO_EC is for solaris 11.3 (2016), github ticket #598 */
+#ifndef OPENSSL_NO_EC
+ EC_KEY *ec_key = NULL;
+#endif
+ char *ephemeral_key_algorithm = NULL;
+ char *cname = NULL;
+ int nid;
+
+ g_return_if_fail(tls != NULL);
+ g_return_if_fail(ssl != NULL);
+
+ if (SSL_get_server_tmp_key(ssl, &ephemeral_key)) {
+ switch (EVP_PKEY_id(ephemeral_key)) {
+ case EVP_PKEY_DH:
+ tls_rec_set_ephemeral_key_algorithm(tls, "DH");
+ tls_rec_set_ephemeral_key_size(tls, EVP_PKEY_bits(ephemeral_key));
+ break;
+
+#ifndef OPENSSL_NO_EC
+ case EVP_PKEY_EC:
+ ec_key = EVP_PKEY_get1_EC_KEY(ephemeral_key);
+ nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec_key));
+ EC_KEY_free(ec_key);
+ cname = (char *)OBJ_nid2sn(nid);
+ ephemeral_key_algorithm = g_strdup_printf("ECDH: %s", cname);
+
+ tls_rec_set_ephemeral_key_algorithm(tls, ephemeral_key_algorithm);
+ tls_rec_set_ephemeral_key_size(tls, EVP_PKEY_bits(ephemeral_key));
+
+ g_free_and_null(ephemeral_key_algorithm);
+ break;
+#endif
+
+ default:
+ tls_rec_set_ephemeral_key_algorithm(tls, "Unknown");
+ tls_rec_set_ephemeral_key_size(tls, EVP_PKEY_bits(ephemeral_key));
+ break;
+ }
+
+ EVP_PKEY_free(ephemeral_key);
+ }
+#endif /* SSL_get_server_tmp_key. */
+}
+
+GIOChannel *net_connect_ip_ssl(IPADDR *ip, int port, IPADDR *my_ip, SERVER_REC *server)
+{
+ GIOChannel *handle, *ssl_handle;
+
+ handle = net_connect_ip(ip, port, my_ip);
+ if (handle == NULL)
+ return NULL;
+ ssl_handle = irssi_ssl_get_iochannel(handle, port, server);
+ if (ssl_handle == NULL)
+ g_io_channel_unref(handle);
+ return ssl_handle;
+}
+
+GIOChannel *net_start_ssl(SERVER_REC *server)
+{
+ GIOChannel *handle, *ssl_handle;
+
+ g_return_val_if_fail(server != NULL, NULL);
+
+ handle = net_sendbuffer_handle(server->handle);
+ if (handle == NULL)
+ return NULL;
+
+ ssl_handle = irssi_ssl_get_iochannel(handle, server->connrec->port, server);
+ return ssl_handle;
+}
+
+
+int irssi_ssl_handshake(GIOChannel *handle)
+{
+ GIOSSLChannel *chan = (GIOSSLChannel *)handle;
+ int ret, err;
+ const char *errstr = NULL;
+ X509 *cert = NULL;
+ X509_PUBKEY *pubkey = NULL;
+ int pubkey_size = 0;
+ unsigned char *pubkey_der = NULL;
+ unsigned char *pubkey_der_tmp = NULL;
+ unsigned char pubkey_fingerprint[EVP_MAX_MD_SIZE];
+ unsigned int pubkey_fingerprint_size;
+ unsigned char cert_fingerprint[EVP_MAX_MD_SIZE];
+ unsigned int cert_fingerprint_size;
+ const char *pinned_cert_fingerprint = chan->server->connrec->tls_pinned_cert;
+ const char *pinned_pubkey_fingerprint = chan->server->connrec->tls_pinned_pubkey;
+ TLS_REC *tls = NULL;
+
+ ERR_clear_error();
+ ret = SSL_connect(chan->ssl);
+ if (ret <= 0) {
+ err = SSL_get_error(chan->ssl, ret);
+ switch (err) {
+ case SSL_ERROR_WANT_READ:
+ return 1;
+ case SSL_ERROR_WANT_WRITE:
+ return 3;
+ case SSL_ERROR_ZERO_RETURN:
+ g_warning("SSL handshake failed: %s", "server closed connection");
+ return -1;
+ case SSL_ERROR_SYSCALL:
+ errstr = ERR_reason_error_string(ERR_get_error());
+ if (errstr == NULL && ret == -1 && errno)
+ errstr = strerror(errno);
+ g_warning("SSL handshake failed: %s", errstr != NULL ? errstr : "server closed connection unexpectedly");
+ return -1;
+ default:
+ errstr = ERR_reason_error_string(ERR_get_error());
+ g_warning("SSL handshake failed: %s", errstr != NULL ? errstr : "unknown SSL error");
+ return -1;
+ }
+ }
+
+ cert = SSL_get_peer_certificate(chan->ssl);
+ if (cert == NULL) {
+ g_warning("TLS server supplied no certificate");
+ ret = 0;
+ goto done;
+ }
+
+ pubkey = X509_get_X509_PUBKEY(cert);
+ if (pubkey == NULL) {
+ g_warning("TLS server supplied no certificate public key");
+ ret = 0;
+ goto done;
+ }
+
+ if (! X509_digest(cert, EVP_sha256(), cert_fingerprint, &cert_fingerprint_size)) {
+ g_warning("Unable to generate certificate fingerprint");
+ ret = 0;
+ goto done;
+ }
+
+ pubkey_size = i2d_X509_PUBKEY(pubkey, NULL);
+ pubkey_der = pubkey_der_tmp = g_new(unsigned char, pubkey_size);
+ i2d_X509_PUBKEY(pubkey, &pubkey_der_tmp);
+
+ EVP_Digest(pubkey_der, pubkey_size, pubkey_fingerprint, &pubkey_fingerprint_size, EVP_sha256(), 0);
+
+ tls = tls_create_rec();
+ set_cipher_info(tls, chan->ssl);
+ if (! set_pubkey_info(tls, cert, cert_fingerprint, cert_fingerprint_size, pubkey_fingerprint, pubkey_fingerprint_size)) {
+ g_warning("Couldn't set pubkey information");
+ ret = 0;
+ goto done;
+ }
+ set_peer_cert_chain_info(tls, chan->ssl);
+ set_server_temporary_key_info(tls, chan->ssl);
+
+ /* Emit the TLS rec. */
+ signal_emit("tls handshake finished", 2, chan->server, tls);
+
+ ret = 1;
+
+ if (pinned_cert_fingerprint != NULL && pinned_cert_fingerprint[0] != '\0') {
+ ret = g_ascii_strcasecmp(pinned_cert_fingerprint, tls->certificate_fingerprint) == 0;
+
+ if (! ret) {
+ g_warning(" Pinned certificate mismatch");
+ goto done;
+ }
+ }
+
+ if (pinned_pubkey_fingerprint != NULL && pinned_pubkey_fingerprint[0] != '\0') {
+ ret = g_ascii_strcasecmp(pinned_pubkey_fingerprint, tls->public_key_fingerprint) == 0;
+
+ if (! ret) {
+ g_warning(" Pinned public key mismatch");
+ goto done;
+ }
+ }
+
+ if (chan->verify) {
+ ret = irssi_ssl_verify(chan->ssl, chan->ctx, chan->server->connrec->address, chan->port, cert, chan->server, tls);
+
+ if (! ret) {
+ /* irssi_ssl_verify emits a warning itself. */
+ goto done;
+ }
+ }
+
+done:
+ tls_rec_free(tls);
+ X509_free(cert);
+ g_free(pubkey_der);
+
+ return ret ? 0 : -1;
+}