From 1394cad30fcac7eb21adb9158dfcfab10e9f53d4 Mon Sep 17 00:00:00 2001 From: Oscar Koeroo Date: Sat, 3 Nov 2012 02:06:51 +0100 Subject: [PATCH] SSL: Several SSL-backend related fixes axTLS: This will make the axTLS backend perform the RFC2818 checks, honoring the VERIFYHOST setting similar to the OpenSSL backend. Generic for OpenSSL and axTLS: Move the hostcheck and cert_hostcheck functions from the lib/ssluse.c files to make them genericly available for both the OpenSSL, axTLS and other SSL backends. They are now in the new lib/hostcheck.c file. CyaSSL: CyaSSL now also has the RFC2818 checks enabled by default. There is a limitation that the verifyhost can not be enabled exclusively on the Subject CN field comparison. This SSL backend will thus behave like the NSS and the GnuTLS (meaning: RFC2818 ok, or bust). In other words: setting verifyhost to 0 or 1 will disable the Subject Alt Names checks too. Schannel: Updated the schannel information messages: Split the IP address usage message from the verifyhost setting and changed the message about disabling SNI (Server Name Indication, used in HTTP virtual hosting) into a message stating that the Subject Alternative Names checks are being disabled when verifyhost is set to 0 or 1. As a side effect of switching off the RFC2818 related servername checks with SCH_CRED_NO_SERVERNAME_CHECK (http://msdn.microsoft.com/en-us/library/aa923430.aspx) the SNI feature is being disabled. This effect is not documented in MSDN, but Wireshark output clearly shows the effect (details on the libcurl maillist). PolarSSL: Fix the prototype change in PolarSSL of ssl_set_session() and the move of the peer_cert from the ssl_context to the ssl_session. Found this change in the PolarSSL SVN between r1316 and r1317 where the POLARSSL_VERSION_NUMBER was at 0x01010100. But to accommodate the Ubuntu PolarSSL version 1.1.4 the check is to discriminate between lower then PolarSSL version 1.2.0 and 1.2.0 and higher. Note: The PolarSSL SVN trunk jumped from version 1.1.1 to 1.2.0. Generic: All the SSL backends are fixed and checked to work with the ssl.verifyhost as a boolean, which is an internal API change. --- lib/Makefile.inc | 5 ++- lib/axtls.c | 66 ++++++++++++++++++++++++++++---- lib/curl_darwinssl.c | 10 +++-- lib/curl_schannel.c | 18 ++++++--- lib/cyassl.c | 47 ++++++++++++++++++++--- lib/hostcheck.c | 91 ++++++++++++++++++++++++++++++++++++++++++++ lib/hostcheck.h | 32 ++++++++++++++++ lib/polarssl.c | 20 ++++++++++ lib/ssluse.c | 70 ++-------------------------------- 9 files changed, 270 insertions(+), 89 deletions(-) create mode 100644 lib/hostcheck.c create mode 100644 lib/hostcheck.h diff --git a/lib/Makefile.inc b/lib/Makefile.inc index e0044ec65..fcb8c28ec 100644 --- a/lib/Makefile.inc +++ b/lib/Makefile.inc @@ -24,7 +24,7 @@ CSOURCES = file.c timeval.c base64.c hostip.c progress.c formdata.c \ idn_win32.c http_negotiate_sspi.c cyassl.c http_proxy.c non-ascii.c \ asyn-ares.c asyn-thread.c curl_gssapi.c curl_ntlm.c curl_ntlm_wb.c \ curl_ntlm_core.c curl_ntlm_msgs.c curl_sasl.c curl_schannel.c \ - curl_multibyte.c curl_darwinssl.c + curl_multibyte.c curl_darwinssl.c hostcheck.c HHEADERS = arpa_telnet.h netrc.h file.h timeval.h qssl.h hostip.h \ progress.h formdata.h cookie.h http.h sendf.h ftp.h url.h dict.h \ @@ -41,4 +41,5 @@ HHEADERS = arpa_telnet.h netrc.h file.h timeval.h qssl.h hostip.h \ warnless.h curl_hmac.h polarssl.h curl_rtmp.h curl_gethostname.h \ gopher.h axtls.h cyassl.h http_proxy.h non-ascii.h asyn.h curl_ntlm.h \ curl_gssapi.h curl_ntlm_wb.h curl_ntlm_core.h curl_ntlm_msgs.h \ - curl_sasl.h curl_schannel.h curl_multibyte.h curl_darwinssl.h + curl_sasl.h curl_schannel.h curl_multibyte.h curl_darwinssl.h \ + hostcheck.h diff --git a/lib/axtls.c b/lib/axtls.c index e37aed50a..ea94c6cf9 100644 --- a/lib/axtls.c +++ b/lib/axtls.c @@ -47,6 +47,8 @@ #include "curl_memory.h" /* The last #include file should be: */ #include "memdebug.h" +#include "hostcheck.h" + /* SSL_read is opied from axTLS compat layer */ static int SSL_read(SSL *ssl, void *buf, int num) @@ -150,7 +152,11 @@ Curl_axtls_connect(struct connectdata *conn, int i, ssl_fcn_return; const uint8_t *ssl_sessionid; size_t ssl_idsize; - const char *x509; + const char *peer_CN; + uint32_t dns_altname_index; + const char *dns_altname; + int8_t found_subject_alt_names = 0; + int8_t found_subject_alt_name_matching_conn = 0; /* Assuming users will not compile in custom key/cert to axTLS */ uint32_t client_option = SSL_NO_DEFAULT_KEY|SSL_SERVER_VERIFY_LATER; @@ -296,19 +302,65 @@ Curl_axtls_connect(struct connectdata *conn, /* Here, gtls.c does issuer verification. axTLS has no straightforward * equivalent, so omitting for now.*/ - /* See if common name was set in server certificate */ - x509 = ssl_get_cert_dn(ssl, SSL_X509_CERT_COMMON_NAME); - if(x509 == NULL) - infof(data, "error fetching CN from cert\n"); - /* Here, gtls.c does the following * 1) x509 hostname checking per RFC2818. axTLS doesn't support this, but - * it seems useful. Omitting for now. + * it seems useful. This is now implemented, by Oscar Koeroo * 2) checks cert validity based on time. axTLS does this in ssl_verify_cert * 3) displays a bunch of cert information. axTLS doesn't support most of * this, but a couple fields are available. */ + + /* There is no (DNS) Altnames count in the version 1.4.8 API. There is a + risk of an inifite loop */ + for(dns_altname_index = 0; ; dns_altname_index++) { + dns_altname = ssl_get_cert_subject_alt_dnsname(ssl, dns_altname_index); + if(dns_altname == NULL) { + break; + } + found_subject_alt_names = 1; + + infof(data, "\tComparing subject alt name DNS with hostname: %s <-> %s\n", + dns_altname, conn->host.name); + if(Curl_cert_hostcheck(dns_altname, conn->host.name)) { + found_subject_alt_name_matching_conn = 1; + break; + } + } + + /* RFC2818 checks */ + if(found_subject_alt_names && !found_subject_alt_name_matching_conn) { + /* Break connection ! */ + Curl_axtls_close(conn, sockindex); + failf(data, "\tsubjectAltName(s) do not match %s\n", conn->host.dispname); + return CURLE_PEER_FAILED_VERIFICATION; + } + else if(found_subject_alt_names == 0) { + /* Per RFC2818, when no Subject Alt Names were available, examine the peer + CN as a legacy fallback */ + peer_CN = ssl_get_cert_dn(ssl, SSL_X509_CERT_COMMON_NAME); + if(peer_CN == NULL) { + /* Similar behaviour to the OpenSSL interface */ + Curl_axtls_close(conn, sockindex); + failf(data, "unable to obtain common name from peer certificate"); + return CURLE_PEER_FAILED_VERIFICATION; + } + else { + if(!Curl_cert_hostcheck((const char *)peer_CN, conn->host.name)) { + if(data->set.ssl.verifyhost) { + /* Break connection ! */ + Curl_axtls_close(conn, sockindex); + failf(data, "\tcommon name \"%s\" does not match \"%s\"\n", + peer_CN, conn->host.dispname); + return CURLE_PEER_FAILED_VERIFICATION; + } + else + infof(data, "\tcommon name \"%s\" does not match \"%s\"\n", + peer_CN, conn->host.dispname); + } + } + } + /* General housekeeping */ conn->ssl[sockindex].state = ssl_connection_complete; conn->ssl[sockindex].ssl = ssl; diff --git a/lib/curl_darwinssl.c b/lib/curl_darwinssl.c index 334944f09..3cc278a1f 100644 --- a/lib/curl_darwinssl.c +++ b/lib/curl_darwinssl.c @@ -803,6 +803,8 @@ static CURLcode darwinssl_connect_step1(struct connectdata *conn, } #endif /* defined(__MAC_10_6) || defined(__IPHONE_5_0) */ + /* If this is a domain name and not an IP address, then configure SNI. + * Also: the verifyhost setting influences SNI usage */ /* If this is a domain name and not an IP address, then configure SNI: */ if((0 == Curl_inet_pton(AF_INET, conn->host.name, &addr)) && #ifdef ENABLE_IPV6 @@ -862,7 +864,6 @@ darwinssl_connect_step2(struct connectdata *conn, int sockindex) connssl->connecting_state = connssl->ssl_direction ? ssl_connect_2_writing : ssl_connect_2_reading; return CURLE_OK; - break; case errSSLServerAuthCompleted: /* the documentation says we need to call SSLHandshake() again */ @@ -874,13 +875,16 @@ darwinssl_connect_step2(struct connectdata *conn, int sockindex) case errSSLCertExpired: failf(data, "SSL certificate problem: OSStatus %d", err); return CURLE_SSL_CACERT; - break; + + case errSSLHostNameMismatch: + failf(data, "SSL certificate peer verification failed, the " + "certificate did not match \"%s\"\n", conn->host.dispname); + return CURLE_PEER_FAILED_VERIFICATION; default: failf(data, "Unknown SSL protocol error in connection to %s:%d", conn->host.name, err); return CURLE_SSL_CONNECT_ERROR; - break; } } else { diff --git a/lib/curl_schannel.c b/lib/curl_schannel.c index ef6b1adb0..8cb3af80f 100644 --- a/lib/curl_schannel.c +++ b/lib/curl_schannel.c @@ -156,14 +156,22 @@ schannel_connect_step1(struct connectdata *conn, int sockindex) infof(data, "schannel: disable server certificate revocation checks\n"); } - if(Curl_inet_pton(AF_INET, conn->host.name, &addr) || + if(Curl_inet_pton(AF_INET, conn->host.name, &addr) #ifdef ENABLE_IPV6 - Curl_inet_pton(AF_INET6, conn->host.name, &addr6) || + || Curl_inet_pton(AF_INET6, conn->host.name, &addr6) #endif - !data->set.ssl.verifyhost) { + ) { schannel_cred.dwFlags |= SCH_CRED_NO_SERVERNAME_CHECK; - infof(data, "schannel: using IP address, disable SNI servername " - "check\n"); + infof(data, "schannel: using IP address, SNI is being disabled by " + "disabling the servername check against the " + "subject names in server certificates.\n"); + } + + if(!data->set.ssl.verifyhost) { + schannel_cred.dwFlags |= SCH_CRED_NO_SERVERNAME_CHECK; + infof(data, "schannel: verifyhost setting prevents Schannel from " + "comparing the supplied target name with the subject " + "names in server certificates. Also disables SNI.\n"); } switch(data->set.ssl.version) { diff --git a/lib/cyassl.c b/lib/cyassl.c index 4c517802f..3639532b2 100644 --- a/lib/cyassl.c +++ b/lib/cyassl.c @@ -53,6 +53,8 @@ #include "curl_memory.h" /* The last #include file should be: */ #include "memdebug.h" +#include +#include static Curl_recv cyassl_recv; @@ -237,6 +239,13 @@ cyassl_connect_step2(struct connectdata *conn, conn->recv[sockindex] = cyassl_recv; conn->send[sockindex] = cyassl_send; + /* Enable RFC2818 checks */ + if(data->set.ssl.verifyhost) { + ret = CyaSSL_check_domain_name(conssl->handle, conn->host.name); + if(ret == SSL_FAILURE) + return CURLE_OUT_OF_MEMORY; + } + ret = SSL_connect(conssl->handle); if(ret != 1) { char error_buffer[80]; @@ -246,15 +255,43 @@ cyassl_connect_step2(struct connectdata *conn, conssl->connecting_state = ssl_connect_2_reading; return CURLE_OK; } - - if(SSL_ERROR_WANT_WRITE == detail) { + else if(SSL_ERROR_WANT_WRITE == detail) { conssl->connecting_state = ssl_connect_2_writing; return CURLE_OK; } - - failf(data, "SSL_connect failed with error %d: %s", detail, + /* There is no easy way to override only the CN matching. + * This will enable the override of both mismatching SubjectAltNames + * as also mismatching CN fields */ + else if(DOMAIN_NAME_MISMATCH == detail) { +#if 1 + failf(data, "\tsubject alt name(s) or common name do not match \"%s\"\n", + conn->host.dispname); + return CURLE_PEER_FAILED_VERIFICATION; +#else + /* When the CyaSSL_check_domain_name() is used and you desire to continue + * on a DOMAIN_NAME_MISMATCH, i.e. 'data->set.ssl.verifyhost == 0', + * CyaSSL version 2.4.0 will fail with an INCOMPLETE_DATA error. The only + * way to do this is currently to switch the CyaSSL_check_domain_name() + * in and out based on the 'data->set.ssl.verifyhost' value. */ + if(data->set.ssl.verifyhost) { + failf(data, + "\tsubject alt name(s) or common name do not match \"%s\"\n", + conn->host.dispname); + return CURLE_PEER_FAILED_VERIFICATION; + } + else { + infof(data, + "\tsubject alt name(s) and/or common name do not match \"%s\"\n", + conn->host.dispname); + return CURLE_OK; + } +#endif + } + else { + failf(data, "SSL_connect failed with error %d: %s", detail, ERR_error_string(detail, error_buffer)); - return CURLE_SSL_CONNECT_ERROR; + return CURLE_SSL_CONNECT_ERROR; + } } conssl->connecting_state = ssl_connect_3; diff --git a/lib/hostcheck.c b/lib/hostcheck.c new file mode 100644 index 000000000..c8c4ac30c --- /dev/null +++ b/lib/hostcheck.c @@ -0,0 +1,91 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) 1998 - 2012, Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at http://curl.haxx.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ***************************************************************************/ + +#include "setup.h" + +#include "hostcheck.h" +#include "rawstr.h" + +/* + * Match a hostname against a wildcard pattern. + * E.g. + * "foo.host.com" matches "*.host.com". + * + * We use the matching rule described in RFC6125, section 6.4.3. + * http://tools.ietf.org/html/rfc6125#section-6.4.3 + */ + +int Curl_hostmatch(const char *hostname, const char *pattern) +{ + const char *pattern_label_end, *pattern_wildcard, *hostname_label_end; + int wildcard_enabled; + size_t prefixlen, suffixlen; + pattern_wildcard = strchr(pattern, '*'); + if(pattern_wildcard == NULL) + return Curl_raw_equal(pattern, hostname) ? + CURL_HOST_MATCH : CURL_HOST_NOMATCH; + + /* We require at least 2 dots in pattern to avoid too wide wildcard + match. */ + wildcard_enabled = 1; + pattern_label_end = strchr(pattern, '.'); + if(pattern_label_end == NULL || strchr(pattern_label_end+1, '.') == NULL || + pattern_wildcard > pattern_label_end || + Curl_raw_nequal(pattern, "xn--", 4)) { + wildcard_enabled = 0; + } + if(!wildcard_enabled) + return Curl_raw_equal(pattern, hostname) ? + CURL_HOST_MATCH : CURL_HOST_NOMATCH; + + hostname_label_end = strchr(hostname, '.'); + if(hostname_label_end == NULL || + !Curl_raw_equal(pattern_label_end, hostname_label_end)) + return CURL_HOST_NOMATCH; + + /* The wildcard must match at least one character, so the left-most + label of the hostname is at least as large as the left-most label + of the pattern. */ + if(hostname_label_end - hostname < pattern_label_end - pattern) + return CURL_HOST_NOMATCH; + + prefixlen = pattern_wildcard - pattern; + suffixlen = pattern_label_end - (pattern_wildcard+1); + return Curl_raw_nequal(pattern, hostname, prefixlen) && + Curl_raw_nequal(pattern_wildcard+1, hostname_label_end - suffixlen, + suffixlen) ? + CURL_HOST_MATCH : CURL_HOST_NOMATCH; +} + +int Curl_cert_hostcheck(const char *match_pattern, const char *hostname) +{ + if(!match_pattern || !*match_pattern || + !hostname || !*hostname) /* sanity check */ + return 0; + + if(Curl_raw_equal(hostname, match_pattern)) /* trivial case */ + return 1; + + if(Curl_hostmatch(hostname,match_pattern) == CURL_HOST_MATCH) + return 1; + return 0; +} diff --git a/lib/hostcheck.h b/lib/hostcheck.h new file mode 100644 index 000000000..9019ccbae --- /dev/null +++ b/lib/hostcheck.h @@ -0,0 +1,32 @@ +#ifndef __HOSTCHECK_H +#define __HOSTCHECK_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) 1998 - 2012, Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at http://curl.haxx.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ***************************************************************************/ + +#include + +#define CURL_HOST_NOMATCH 0 +#define CURL_HOST_MATCH 1 +int Curl_hostmatch(const char *hostname, const char *pattern); +int Curl_cert_hostcheck(const char *match_pattern, const char *hostname); + +#endif diff --git a/lib/polarssl.c b/lib/polarssl.c index a2f61d23d..b20b4a397 100644 --- a/lib/polarssl.c +++ b/lib/polarssl.c @@ -212,8 +212,15 @@ polarssl_connect_step1(struct connectdata *conn, infof(data, "PolarSSL re-using session\n"); } +/* PolarSSL SVN revision r1316 to r1317, matching <1.2.0 is to cover Ubuntu's + 1.1.4 version and the like */ +#if POLARSSL_VERSION_NUMBER<0x01020000 ssl_set_session(&connssl->ssl, 1, 600, &connssl->ssn); +#else + ssl_set_session(&connssl->ssl, + &connssl->ssn); +#endif ssl_set_ca_chain(&connssl->ssl, &connssl->cacert, @@ -306,12 +313,25 @@ polarssl_connect_step2(struct connectdata *conn, return CURLE_PEER_FAILED_VERIFICATION; } +/* PolarSSL SVN revision r1316 to r1317, matching <1.2.0 is to cover Ubuntu's + 1.1.4 version and the like */ +#if POLARSSL_VERSION_NUMBER<0x01020000 if(conn->ssl[sockindex].ssl.peer_cert) { +#else + if(ssl_get_peer_cert(&(connssl->ssl))) { +#endif /* If the session was resumed, there will be no peer certs */ memset(buffer, 0, sizeof(buffer)); +/* PolarSSL SVN revision r1316 to r1317, matching <1.2.0 is to cover Ubuntu's + 1.1.4 version and the like */ +#if POLARSSL_VERSION_NUMBER<0x01020000 if(x509parse_cert_info(buffer, sizeof(buffer), (char *)"* ", conn->ssl[sockindex].ssl.peer_cert) != -1) +#else + if(x509parse_cert_info(buffer, sizeof(buffer), (char *)"* ", + ssl_get_peer_cert(&(connssl->ssl))) != -1) +#endif infof(data, "Dumping cert info:\n%s\n", buffer); } diff --git a/lib/ssluse.c b/lib/ssluse.c index 6f0e6bf6b..7c4c9269a 100644 --- a/lib/ssluse.c +++ b/lib/ssluse.c @@ -50,6 +50,7 @@ #include "select.h" #include "sslgen.h" #include "rawstr.h" +#include "hostcheck.h" #define _MPRINTF_REPLACE /* use the internal *printf() functions */ #include @@ -1039,71 +1040,6 @@ static int asn1_output(const ASN1_UTCTIME *tm, /* ====================================================== */ -/* - * Match a hostname against a wildcard pattern. - * E.g. - * "foo.host.com" matches "*.host.com". - * - * We use the matching rule described in RFC6125, section 6.4.3. - * http://tools.ietf.org/html/rfc6125#section-6.4.3 - */ -#define HOST_NOMATCH 0 -#define HOST_MATCH 1 - -static int hostmatch(const char *hostname, const char *pattern) -{ - const char *pattern_label_end, *pattern_wildcard, *hostname_label_end; - int wildcard_enabled; - size_t prefixlen, suffixlen; - pattern_wildcard = strchr(pattern, '*'); - if(pattern_wildcard == NULL) { - return Curl_raw_equal(pattern, hostname) ? HOST_MATCH : HOST_NOMATCH; - } - /* We require at least 2 dots in pattern to avoid too wide wildcard - match. */ - wildcard_enabled = 1; - pattern_label_end = strchr(pattern, '.'); - if(pattern_label_end == NULL || strchr(pattern_label_end+1, '.') == NULL || - pattern_wildcard > pattern_label_end || - Curl_raw_nequal(pattern, "xn--", 4)) { - wildcard_enabled = 0; - } - if(!wildcard_enabled) { - return Curl_raw_equal(pattern, hostname) ? HOST_MATCH : HOST_NOMATCH; - } - hostname_label_end = strchr(hostname, '.'); - if(hostname_label_end == NULL || - !Curl_raw_equal(pattern_label_end, hostname_label_end)) { - return HOST_NOMATCH; - } - /* The wildcard must match at least one character, so the left-most - label of the hostname is at least as large as the left-most label - of the pattern. */ - if(hostname_label_end - hostname < pattern_label_end - pattern) { - return HOST_NOMATCH; - } - prefixlen = pattern_wildcard - pattern; - suffixlen = pattern_label_end - (pattern_wildcard+1); - return Curl_raw_nequal(pattern, hostname, prefixlen) && - Curl_raw_nequal(pattern_wildcard+1, hostname_label_end - suffixlen, - suffixlen) ? - HOST_MATCH : HOST_NOMATCH; -} - -static int -cert_hostcheck(const char *match_pattern, const char *hostname) -{ - if(!match_pattern || !*match_pattern || - !hostname || !*hostname) /* sanity check */ - return 0; - - if(Curl_raw_equal(hostname, match_pattern)) /* trivial case */ - return 1; - - if(hostmatch(hostname,match_pattern) == HOST_MATCH) - return 1; - return 0; -} /* Quote from RFC2818 section 3.1 "Server Identity" @@ -1192,7 +1128,7 @@ static CURLcode verifyhost(struct connectdata *conn, if((altlen == strlen(altptr)) && /* if this isn't true, there was an embedded zero in the name string and we cannot match it. */ - cert_hostcheck(altptr, conn->host.name)) + Curl_cert_hostcheck(altptr, conn->host.name)) matched = 1; else matched = 0; @@ -1291,7 +1227,7 @@ static CURLcode verifyhost(struct connectdata *conn, "SSL: unable to obtain common name from peer certificate"); res = CURLE_PEER_FAILED_VERIFICATION; } - else if(!cert_hostcheck((const char *)peer_CN, conn->host.name)) { + else if(!Curl_cert_hostcheck((const char *)peer_CN, conn->host.name)) { failf(data, "SSL: certificate subject name '%s' does not match " "target host name '%s'", peer_CN, conn->host.dispname); res = CURLE_PEER_FAILED_VERIFICATION;