diff --git a/src/core/network-openssl.c b/src/core/network-openssl.c index 92832ba2..5a9c9bc7 100644 --- a/src/core/network-openssl.c +++ b/src/core/network-openssl.c @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -39,6 +40,7 @@ typedef struct SSL *ssl; SSL_CTX *ctx; unsigned int verify:1; + const char *hostname; } GIOSSLChannel; static SSL_CTX *ssl_ctx = NULL; @@ -53,7 +55,149 @@ static void irssi_ssl_free(GIOChannel *handle) g_free(chan); } -static gboolean irssi_ssl_verify(SSL *ssl, SSL_CTX *ctx, X509 *cert) +/* 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"); + } + free(cert_subject_cn); + } + + return matched; +} + +static gboolean irssi_ssl_verify(SSL *ssl, SSL_CTX *ctx, const char* hostname, X509 *cert) { if (SSL_get_verify_result(ssl) != X509_V_OK) { unsigned char md[EVP_MAX_MD_SIZE]; @@ -89,6 +233,8 @@ static gboolean irssi_ssl_verify(SSL *ssl, SSL_CTX *ctx, X509 *cert) } } return FALSE; + } else if (! irssi_ssl_verify_hostname(cert, hostname)){ + return FALSE; } return TRUE; } @@ -241,7 +387,7 @@ static gboolean irssi_ssl_init(void) } -static GIOChannel *irssi_ssl_get_iochannel(GIOChannel *handle, const char *mycert, const char *mypkey, const char *cafile, const char *capath, gboolean verify) +static GIOChannel *irssi_ssl_get_iochannel(GIOChannel *handle, const char *hostname, const char *mycert, const char *mypkey, const char *cafile, const char *capath, gboolean verify) { GIOSSLChannel *chan; GIOChannel *gchan; @@ -326,6 +472,7 @@ static GIOChannel *irssi_ssl_get_iochannel(GIOChannel *handle, const char *mycer chan->ssl = ssl; chan->ctx = ctx; chan->verify = verify; + chan->hostname = hostname; gchan = (GIOChannel *)chan; gchan->funcs = &irssi_ssl_channel_funcs; @@ -336,14 +483,14 @@ static GIOChannel *irssi_ssl_get_iochannel(GIOChannel *handle, const char *mycer return gchan; } -GIOChannel *net_connect_ip_ssl(IPADDR *ip, int port, IPADDR *my_ip, const char *cert, const char *pkey, const char *cafile, const char *capath, gboolean verify) +GIOChannel *net_connect_ip_ssl(IPADDR *ip, int port, const char* hostname, IPADDR *my_ip, const char *cert, const char *pkey, const char *cafile, const char *capath, gboolean verify) { GIOChannel *handle, *ssl_handle; handle = net_connect_ip(ip, port, my_ip); if (handle == NULL) return NULL; - ssl_handle = irssi_ssl_get_iochannel(handle, cert, pkey, cafile, capath, verify); + ssl_handle = irssi_ssl_get_iochannel(handle, hostname, cert, pkey, cafile, capath, verify); if (ssl_handle == NULL) g_io_channel_unref(handle); return ssl_handle; @@ -385,7 +532,7 @@ int irssi_ssl_handshake(GIOChannel *handle) g_warning("SSL server supplied no certificate"); return -1; } - ret = !chan->verify || irssi_ssl_verify(chan->ssl, chan->ctx, cert); + ret = !chan->verify || irssi_ssl_verify(chan->ssl, chan->ctx, chan->hostname, cert); X509_free(cert); return ret ? 0 : -1; } diff --git a/src/core/network.h b/src/core/network.h index 65505eae..8583724c 100644 --- a/src/core/network.h +++ b/src/core/network.h @@ -47,7 +47,7 @@ int net_ip_compare(IPADDR *ip1, IPADDR *ip2); /* Connect to socket */ GIOChannel *net_connect(const char *addr, int port, IPADDR *my_ip); /* Connect to socket with ip address and SSL*/ -GIOChannel *net_connect_ip_ssl(IPADDR *ip, int port, IPADDR *my_ip, const char *cert, const char *pkey, const char *cafile, const char *capath, gboolean verify); +GIOChannel *net_connect_ip_ssl(IPADDR *ip, int port, const char* hostname, IPADDR *my_ip, const char *cert, const char *pkey, const char *cafile, const char *capath, gboolean verify); int irssi_ssl_handshake(GIOChannel *handle); /* Connect to socket with ip address */ GIOChannel *net_connect_ip(IPADDR *ip, int port, IPADDR *my_ip); diff --git a/src/core/servers.c b/src/core/servers.c index d5844e7b..017a2036 100644 --- a/src/core/servers.c +++ b/src/core/servers.c @@ -224,7 +224,7 @@ static void server_real_connect(SERVER_REC *server, IPADDR *ip, port = server->connrec->proxy != NULL ? server->connrec->proxy_port : server->connrec->port; handle = server->connrec->use_ssl ? - net_connect_ip_ssl(ip, port, own_ip, server->connrec->ssl_cert, server->connrec->ssl_pkey, + net_connect_ip_ssl(ip, port, server->connrec->address, own_ip, server->connrec->ssl_cert, server->connrec->ssl_pkey, server->connrec->ssl_cafile, server->connrec->ssl_capath, server->connrec->ssl_verify) : net_connect_ip(ip, port, own_ip); } else {