diff --git a/CMakeLists.txt b/CMakeLists.txt index 74cf6814..bb4ea6d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -895,7 +895,6 @@ ENDIF() # POSIX-specific checks CHECK_FUNCTION_EXISTS(getaddrinfo VMIME_HAVE_GETADDRINFO) -CHECK_FUNCTION_EXISTS(getnameinfo VMIME_HAVE_GETNAMEINFO) CHECK_FUNCTION_EXISTS(gettid VMIME_HAVE_GETTID) CHECK_FUNCTION_EXISTS(syscall VMIME_HAVE_SYSCALL) diff --git a/cmake/config.hpp.cmake b/cmake/config.hpp.cmake index f1d38a6d..ed486718 100644 --- a/cmake/config.hpp.cmake +++ b/cmake/config.hpp.cmake @@ -73,7 +73,6 @@ typedef unsigned @VMIME_64BIT_TYPE@ vmime_uint64; #cmakedefine01 VMIME_HAVE_PTHREAD #cmakedefine01 VMIME_HAVE_GETADDRINFO #cmakedefine01 VMIME_HAVE_GETADDRINFO_A -#cmakedefine01 VMIME_HAVE_GETNAMEINFO #cmakedefine01 VMIME_HAVE_GETTID #cmakedefine01 VMIME_HAVE_SYSCALL #cmakedefine01 VMIME_HAVE_SYSCALL_GETTID diff --git a/src/vmime/net/socket.hpp b/src/vmime/net/socket.hpp index a236a9f3..c5923848 100644 --- a/src/vmime/net/socket.hpp +++ b/src/vmime/net/socket.hpp @@ -149,9 +149,13 @@ class VMIME_EXPORT socket : public object { */ virtual unsigned int getStatus() const = 0; - /** Return the hostname of peer this socket is connected to. + /** Return the name of the peer this socket is connected to, as it + * was specified when connecting (host name or numeric address). * - * @return name of the peer, or numeric address if it cannot be found + * No reverse DNS lookup is performed on numeric addresses, as this + * name is used to verify the identity of the server with TLS. + * + * @return name of the peer */ virtual const string getPeerName() const = 0; diff --git a/src/vmime/net/tls/TLSSocket.cpp b/src/vmime/net/tls/TLSSocket.cpp index fbca0820..175f9a3c 100644 --- a/src/vmime/net/tls/TLSSocket.cpp +++ b/src/vmime/net/tls/TLSSocket.cpp @@ -35,6 +35,42 @@ namespace net { namespace tls { +// static +bool TLSSocket::isIPAddress(const string& host) { + + // A host name cannot contain ':', so this is an IPv6 address + if (host.find(':') != string::npos) { + return true; + } + + // Check for an IPv4 address in dotted-decimal notation + int parts = 0; + string::size_type pos = 0; + + while (true) { + + unsigned int value = 0; + int digits = 0; + + for ( ; pos < host.length() && host[pos] >= '0' && host[pos] <= '9' ; ++pos, ++digits) { + value = value * 10 + static_cast (host[pos] - '0'); + } + + if (digits == 0 || digits > 3 || value > 255) { + return false; + } + + if (++parts == 4 || pos == host.length() || host[pos] != '.') { + break; + } + + ++pos; // skip '.' + } + + return parts == 4 && pos == host.length(); +} + + } // tls } // net } // vmime diff --git a/src/vmime/net/tls/TLSSocket.hpp b/src/vmime/net/tls/TLSSocket.hpp index ca50aa8e..5408dfeb 100644 --- a/src/vmime/net/tls/TLSSocket.hpp +++ b/src/vmime/net/tls/TLSSocket.hpp @@ -75,6 +75,15 @@ class VMIME_EXPORT TLSSocket : public socket { * has not been performed yet */ virtual shared_ptr getPeerCertificates() = 0; + + /** Test whether the specified host is an IPv4 or IPv6 address + * rather than a host name. IP addresses must not be sent in + * the Server Name Indication (SNI) extension (RFC 6066). + * + * @param host host name or address + * @return true if the host is an IP address, false otherwise + */ + static bool isIPAddress(const string& host); }; diff --git a/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.cpp b/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.cpp index 97466fc5..f5b14ce2 100644 --- a/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.cpp +++ b/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.cpp @@ -318,8 +318,11 @@ void TLSSocket_GnuTLS::handshake() { // Start handshaking process try { string peerName = getPeerName(); - - gnutls_server_name_set(*m_session->m_gnutlsSession, GNUTLS_NAME_DNS, peerName.c_str(), peerName.size()); + + // IP addresses are not permitted in SNI (RFC 6066) + if (!isIPAddress(peerName)) { + gnutls_server_name_set(*m_session->m_gnutlsSession, GNUTLS_NAME_DNS, peerName.c_str(), peerName.size()); + } while (true) { diff --git a/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.cpp b/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.cpp index bbda81b3..cf82909e 100644 --- a/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.cpp +++ b/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.cpp @@ -168,7 +168,12 @@ void TLSSocket_OpenSSL::createSSLHandle() { } SSL_set_bio(m_ssl, sockBio, sockBio); - SSL_set_tlsext_host_name(m_ssl, peerName.c_str()); + + // IP addresses are not permitted in SNI (RFC 6066) + if (!isIPAddress(peerName)) { + SSL_set_tlsext_host_name(m_ssl, peerName.c_str()); + } + SSL_set_connect_state(m_ssl); SSL_set_mode(m_ssl, SSL_MODE_AUTO_RETRY | SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); diff --git a/src/vmime/platforms/posix/posixSocket.cpp b/src/vmime/platforms/posix/posixSocket.cpp index 8e8bbd60..319f9fdb 100644 --- a/src/vmime/platforms/posix/posixSocket.cpp +++ b/src/vmime/platforms/posix/posixSocket.cpp @@ -541,35 +541,6 @@ void posixSocket::disconnect() { } -static bool isNumericAddress(const char* address) { - -#if VMIME_HAVE_GETADDRINFO - - struct addrinfo hint, *info = NULL; - memset(&hint, 0, sizeof(hint)); - - hint.ai_family = AF_UNSPEC; - hint.ai_flags = AI_NUMERICHOST; - - if (getaddrinfo(address, 0, &hint, &info) == 0) { - - freeaddrinfo(info); - return true; - - } else { - - return false; - } - -#else - - return inet_addr(address) != INADDR_NONE; - -#endif - -} - - const string posixSocket::getPeerAddress() const { // Get address of connected peer @@ -601,53 +572,9 @@ const string posixSocket::getPeerAddress() const { const string posixSocket::getPeerName() const { - // Get address of connected peer - sockaddr_storage peer; - socklen_t peerLen = sizeof(peer); - - if (getpeername(m_desc, reinterpret_cast (&peer), &peerLen) != 0) { - throwSocketError(errno); - } - - // If server address as specified when connecting is a numeric - // address, try to get a host name for it - if (isNumericAddress(m_serverAddress.c_str())) { - -#if VMIME_HAVE_GETNAMEINFO - - char host[NI_MAXHOST + 1]; - char service[NI_MAXSERV + 1]; - - if (getnameinfo(reinterpret_cast (&peer), peerLen, - host, sizeof(host), service, sizeof(service), - /* flags */ NI_NAMEREQD) == 0) { - - return string(host); - } - -#else - - struct hostent *hp; - const void* addr; - socklen_t addrLen; - - if (peer.ss_family == AF_INET6) { - addr = &reinterpret_cast (&peer)->sin6_addr; - addrLen = sizeof(in6_addr); - } else { - addr = &reinterpret_cast (&peer)->sin_addr; - addrLen = sizeof(in_addr); - } - - if ((hp = gethostbyaddr(addr, addrLen, peer.ss_family)) != NULL) { - - return string(hp->h_name); - } - -#endif - - } - + // Do not try to get a host name for a numeric address: the peer name is + // used as the TLS reference identity, and it must not be derived from an + // insecure source such as a reverse DNS lookup return m_serverAddress; } diff --git a/src/vmime/platforms/windows/windowsSocket.cpp b/src/vmime/platforms/windows/windowsSocket.cpp index 6d87e955..2f6498f3 100644 --- a/src/vmime/platforms/windows/windowsSocket.cpp +++ b/src/vmime/platforms/windows/windowsSocket.cpp @@ -173,26 +173,6 @@ void windowsSocket::disconnect() { } -static bool isNumericAddress(const char* address) { - - struct addrinfo hint, *info = NULL; - memset(&hint, 0, sizeof(hint)); - - hint.ai_family = AF_UNSPEC; - hint.ai_flags = AI_NUMERICHOST; - - if (getaddrinfo(address, 0, &hint, &info) == 0) { - - freeaddrinfo(info); - return true; - - } else { - - return false; - } -} - - const string windowsSocket::getPeerAddress() const { // Get address of connected peer @@ -218,27 +198,9 @@ const string windowsSocket::getPeerAddress() const { const string windowsSocket::getPeerName() const { - // Get address of connected peer - sockaddr_storage peer; - socklen_t peerLen = sizeof(peer); - - getpeername(m_desc, reinterpret_cast (&peer), &peerLen); - - // If server address as specified when connecting is a numeric - // address, try to get a host name for it - if (isNumericAddress(m_serverAddress.c_str())) { - - char host[NI_MAXHOST + 1]; - char service[NI_MAXSERV + 1]; - - if (getnameinfo(reinterpret_cast (&peer), peerLen, - host, sizeof(host), service, sizeof(service), - /* flags */ NI_NAMEREQD) == 0) { - - return string(host); - } - } - + // Do not try to get a host name for a numeric address: the peer name is + // used as the TLS reference identity, and it must not be derived from an + // insecure source such as a reverse DNS lookup return m_serverAddress; } diff --git a/src/vmime/security/cert/openssl/X509Certificate_OpenSSL.cpp b/src/vmime/security/cert/openssl/X509Certificate_OpenSSL.cpp index bd5a75dd..7f7f64c8 100644 --- a/src/vmime/security/cert/openssl/X509Certificate_OpenSSL.cpp +++ b/src/vmime/security/cert/openssl/X509Certificate_OpenSSL.cpp @@ -433,6 +433,14 @@ bool X509Certificate_OpenSSL::verifyHostName( std::vector * nonMatchingNames ) const { + // An IP address must match an IP address in subject alternative names + // (X509_check_ip_asc() returns -2 if hostname is not an IP address) + const int ipMatch = X509_check_ip_asc(m_data->cert, hostname.c_str(), 0); + + if (ipMatch != -2) { + return ipMatch == 1; + } + // First, check subject common name against hostname char CNBuffer[1024]; CNBuffer[sizeof(CNBuffer) - 1] = '\0'; diff --git a/tests/net/tls/TLSSocketTest.cpp b/tests/net/tls/TLSSocketTest.cpp new file mode 100644 index 00000000..e2f6c01c --- /dev/null +++ b/tests/net/tls/TLSSocketTest.cpp @@ -0,0 +1,82 @@ +// +// VMime library (http://www.vmime.org) +// Copyright (C) 2002 Vincent Richard +// +// 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 3 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. +// +// Linking this library statically or dynamically with other modules is making +// a combined work based on this library. Thus, the terms and conditions of +// the GNU General Public License cover the whole combination. +// + +#include "tests/testUtils.hpp" + + +#if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_TLS_SUPPORT + + +#include "vmime/net/tls/TLSSocket.hpp" + + +using vmime::net::tls::TLSSocket; + + +VMIME_TEST_SUITE_BEGIN(TLSSocketTest) + + VMIME_TEST_LIST_BEGIN + VMIME_TEST(testIsIPAddress_IPv4) + VMIME_TEST(testIsIPAddress_IPv6) + VMIME_TEST(testIsIPAddress_HostName) + VMIME_TEST(testIsIPAddress_Invalid) + VMIME_TEST_LIST_END + + + void testIsIPAddress_IPv4() { + + VASSERT_TRUE("1", TLSSocket::isIPAddress("192.0.2.1")); + VASSERT_TRUE("2", TLSSocket::isIPAddress("0.0.0.0")); + VASSERT_TRUE("3", TLSSocket::isIPAddress("255.255.255.255")); + } + + void testIsIPAddress_IPv6() { + + VASSERT_TRUE("1", TLSSocket::isIPAddress("2001:db8::1")); + VASSERT_TRUE("2", TLSSocket::isIPAddress("::1")); + VASSERT_TRUE("3", TLSSocket::isIPAddress("::ffff:192.0.2.1")); + } + + void testIsIPAddress_HostName() { + + VASSERT_FALSE("1", TLSSocket::isIPAddress("mail.vmime.org")); + VASSERT_FALSE("2", TLSSocket::isIPAddress("localhost")); + VASSERT_FALSE("3", TLSSocket::isIPAddress("192.vmime.org")); + VASSERT_FALSE("4", TLSSocket::isIPAddress("192.0.2.1.vmime.org")); + } + + void testIsIPAddress_Invalid() { + + VASSERT_FALSE("1", TLSSocket::isIPAddress("")); + VASSERT_FALSE("2", TLSSocket::isIPAddress("192.0.2")); + VASSERT_FALSE("3", TLSSocket::isIPAddress("192.0.2.1.5")); + VASSERT_FALSE("4", TLSSocket::isIPAddress("192.0.2.1.")); + VASSERT_FALSE("5", TLSSocket::isIPAddress("192..2.1")); + VASSERT_FALSE("6", TLSSocket::isIPAddress("192.0.2.256")); + VASSERT_FALSE("7", TLSSocket::isIPAddress("192.0.2.0001")); + } + +VMIME_TEST_SUITE_END + + +#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_TLS_SUPPORT diff --git a/tests/platforms/posix/posixSocketTest.cpp b/tests/platforms/posix/posixSocketTest.cpp index 9e13d27b..33fe393e 100644 --- a/tests/platforms/posix/posixSocketTest.cpp +++ b/tests/platforms/posix/posixSocketTest.cpp @@ -44,8 +44,7 @@ class loopbackServer { explicit loopbackServer(const char* address) : m_address(address), m_desc(-1), - m_port(0), - m_addrLen(sizeof(m_addr)) { + m_port(0) { struct addrinfo hint, *info = NULL; memset(&hint, 0, sizeof(hint)); @@ -58,12 +57,15 @@ class loopbackServer { return; } + sockaddr_storage addr; + socklen_t addrLen = sizeof(addr); + m_desc = ::socket(info->ai_family, info->ai_socktype, info->ai_protocol); if (m_desc != -1 && (::bind(m_desc, info->ai_addr, info->ai_addrlen) != 0 || ::listen(m_desc, 1) != 0 || - ::getsockname(m_desc, reinterpret_cast (&m_addr), &m_addrLen) != 0)) { + ::getsockname(m_desc, reinterpret_cast (&addr), &addrLen) != 0)) { ::close(m_desc); m_desc = -1; @@ -75,10 +77,10 @@ class loopbackServer { return; } - if (m_addr.ss_family == AF_INET6) { - m_port = ntohs(reinterpret_cast (&m_addr)->sin6_port); + if (addr.ss_family == AF_INET6) { + m_port = ntohs(reinterpret_cast (&addr)->sin6_port); } else { - m_port = ntohs(reinterpret_cast (&m_addr)->sin_port); + m_port = ntohs(reinterpret_cast (&addr)->sin_port); } } @@ -104,28 +106,11 @@ class loopbackServer { return m_port; } - // Host name the resolver gives for the listening address, or the - // address itself if it has none - const vmime::string getHostName() const { - - char host[NI_MAXHOST]; - - if (getnameinfo(reinterpret_cast (&m_addr), m_addrLen, - host, sizeof(host), NULL, 0, NI_NAMEREQD) == 0) { - - return host; - } - - return m_address; - } - private: const vmime::string m_address; int m_desc; vmime::port_t m_port; - sockaddr_storage m_addr; - socklen_t m_addrLen; }; @@ -174,7 +159,8 @@ VMIME_TEST_SUITE_BEGIN(posixSocketTest) loopbackServer server("127.0.0.1"); VASSERT_TRUE("Listen", server.isListening()); - VASSERT_EQ("Name", server.getHostName(), connectTo(server)->getPeerName()); + // No reverse DNS lookup on numeric addresses + VASSERT_EQ("Name", "127.0.0.1", connectTo(server)->getPeerName()); } void testGetPeerNameIPv6() { @@ -185,7 +171,8 @@ VMIME_TEST_SUITE_BEGIN(posixSocketTest) return; // IPv6 not available on this host } - VASSERT_EQ("Name", server.getHostName(), connectTo(server)->getPeerName()); + // No reverse DNS lookup on numeric addresses + VASSERT_EQ("Name", "::1", connectTo(server)->getPeerName()); } VMIME_TEST_SUITE_END diff --git a/tests/security/cert/X509CertificateTest.cpp b/tests/security/cert/X509CertificateTest.cpp new file mode 100644 index 00000000..28b144a2 --- /dev/null +++ b/tests/security/cert/X509CertificateTest.cpp @@ -0,0 +1,100 @@ +// +// VMime library (http://www.vmime.org) +// Copyright (C) 2002 Vincent Richard +// +// 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 3 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. +// +// Linking this library statically or dynamically with other modules is making +// a combined work based on this library. Thus, the terms and conditions of +// the GNU General Public License cover the whole combination. +// + +#include "tests/testUtils.hpp" + + +#if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_TLS_SUPPORT + + +#include "vmime/security/cert/X509Certificate.hpp" + + +// Self-signed certificate for CN=mail.vmime.org, with subject alternative +// names DNS:mail.vmime.org, IP:192.0.2.1 and IP:2001:db8::1 +static const char testCertificatePEM[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIBvTCCAWSgAwIBAgIUYrH3DcV2gtgaaXI2HoVgAlX5y0UwCgYIKoZIzj0EAwIw\n" + "GTEXMBUGA1UEAwwObWFpbC52bWltZS5vcmcwIBcNMjYxMDEwMTczMDIyWhgPMjEy\n" + "NjA5MTYxNzMwMjJaMBkxFzAVBgNVBAMMDm1haWwudm1pbWUub3JnMFkwEwYHKoZI\n" + "zj0CAQYIKoZIzj0DAQcDQgAEl8CflV/LHMqyY99K6GEj5NULOdEGgEN1q9ZqX8mo\n" + "FgMnz2NgQcojSfjQZMiiY0TxVjfI19Swy2ZguwkI2UPsHaOBhzCBhDAdBgNVHQ4E\n" + "FgQUi6ALUpBpLCujEvuanLdVY2rv1V4wHwYDVR0jBBgwFoAUi6ALUpBpLCujEvua\n" + "nLdVY2rv1V4wDwYDVR0TAQH/BAUwAwEB/zAxBgNVHREEKjAogg5tYWlsLnZtaW1l\n" + "Lm9yZ4cEwAACAYcQIAENuAAAAAAAAAAAAAAAATAKBggqhkjOPQQDAgNHADBEAiAy\n" + "3LiV2LL/eSryByB0crI1iB7lPcxisjWcs/J8r/yK8AIgCWeDkeuzkhhRDP71Vcdy\n" + "Gaor91P2kUwL1MRIKAbkK/w=\n" + "-----END CERTIFICATE-----\n"; + + + +VMIME_TEST_SUITE_BEGIN(X509CertificateTest) + + VMIME_TEST_LIST_BEGIN + VMIME_TEST(testVerifyHostName) + VMIME_TEST(testVerifyHostNameIPv4) + VMIME_TEST(testVerifyHostNameIPv6) + VMIME_TEST_LIST_END + + + static vmime::shared_ptr importTestCertificate() { + + vmime::shared_ptr cert = + vmime::security::cert::X509Certificate::import( + reinterpret_cast (testCertificatePEM), + sizeof(testCertificatePEM) - 1 + ); + + VASSERT_NOT_NULL("Import", cert); + + return cert; + } + + void testVerifyHostName() { + + vmime::shared_ptr cert = importTestCertificate(); + + VASSERT_TRUE("Match", cert->verifyHostName("mail.vmime.org")); + VASSERT_FALSE("No match", cert->verifyHostName("www.vmime.org")); + } + + void testVerifyHostNameIPv4() { + + vmime::shared_ptr cert = importTestCertificate(); + + VASSERT_TRUE("Match", cert->verifyHostName("192.0.2.1")); + VASSERT_FALSE("No match", cert->verifyHostName("192.0.2.2")); + } + + void testVerifyHostNameIPv6() { + + vmime::shared_ptr cert = importTestCertificate(); + + VASSERT_TRUE("Match", cert->verifyHostName("2001:db8::1")); + VASSERT_FALSE("No match", cert->verifyHostName("2001:db8::2")); + } + +VMIME_TEST_SUITE_END + + +#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_TLS_SUPPORT