From 408ee41990270357167de8e57f9b4fb0a3e6d6f3 Mon Sep 17 00:00:00 2001 From: Matt DeBoer Date: Sun, 22 Sep 2019 18:06:15 -0700 Subject: [PATCH] WIP: support configurable certificates/keys, and root trust CAs (#114) * wip: tls options implemented in openssl * update naming, remove #define guard * assert compiled with USE_TLS for tls options * apply autoformatter * include tls options impl * style cleanup; auto ssl_err * ssl_err -> sslErr * be explicit about SSL_VERIFY_NONE --- CMakeLists.txt | 2 + ixwebsocket/IXSocketFactory.cpp | 44 ++++---- ixwebsocket/IXSocketFactory.h | 4 +- ixwebsocket/IXSocketOpenSSL.cpp | 146 ++++++++++++++++++--------- ixwebsocket/IXSocketOpenSSL.h | 5 +- ixwebsocket/IXSocketTLSOptions.cpp | 34 +++++++ ixwebsocket/IXSocketTLSOptions.h | 32 ++++++ ixwebsocket/IXWebSocket.cpp | 8 ++ ixwebsocket/IXWebSocket.h | 7 ++ ixwebsocket/IXWebSocketTransport.cpp | 5 +- ixwebsocket/IXWebSocketTransport.h | 5 + 11 files changed, 222 insertions(+), 70 deletions(-) create mode 100644 ixwebsocket/IXSocketTLSOptions.cpp create mode 100644 ixwebsocket/IXSocketTLSOptions.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f3a7b8d8..f3ac839c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ set( IXWEBSOCKET_SOURCES ixwebsocket/IXSocketConnect.cpp ixwebsocket/IXSocketFactory.cpp ixwebsocket/IXSocketServer.cpp + ixwebsocket/IXSocketTLSOptions.cpp ixwebsocket/IXUrlParser.cpp ixwebsocket/IXUserAgent.cpp ixwebsocket/IXWebSocket.cpp @@ -67,6 +68,7 @@ set( IXWEBSOCKET_HEADERS ixwebsocket/IXSocketConnect.h ixwebsocket/IXSocketFactory.h ixwebsocket/IXSocketServer.h + ixwebsocket/IXSocketTLSOptions.h ixwebsocket/IXUrlParser.h ixwebsocket/IXUtf8Validator.h ixwebsocket/IXUserAgent.h diff --git a/ixwebsocket/IXSocketFactory.cpp b/ixwebsocket/IXSocketFactory.cpp index 5b29f25a..8e10b730 100644 --- a/ixwebsocket/IXSocketFactory.cpp +++ b/ixwebsocket/IXSocketFactory.cpp @@ -8,15 +8,15 @@ #ifdef IXWEBSOCKET_USE_TLS -# ifdef IXWEBSOCKET_USE_MBED_TLS -# include -# elif __APPLE__ -# include -# elif defined(_WIN32) -# include -# elif defined(IXWEBSOCKET_USE_OPEN_SSL) -# include -# endif +#ifdef IXWEBSOCKET_USE_MBED_TLS +#include +#elif __APPLE__ +#include +#elif defined(_WIN32) +#include +#elif defined(IXWEBSOCKET_USE_OPEN_SSL) +#include +#endif #else @@ -27,7 +27,8 @@ namespace ix { std::shared_ptr createSocket(bool tls, - std::string& errorMsg) + std::string& errorMsg, + const SocketTLSOptions& tlsOptions) { errorMsg.clear(); std::shared_ptr socket; @@ -39,15 +40,15 @@ namespace ix else { #ifdef IXWEBSOCKET_USE_TLS -# if defined(IXWEBSOCKET_USE_MBED_TLS) - socket = std::make_shared(); -# elif defined(__APPLE__) - socket = std::make_shared(); -# elif defined(_WIN32) - socket = std::make_shared(); -# else - socket = std::make_shared(); -# endif +#if defined(IXWEBSOCKET_USE_MBED_TLS) + socket = std::make_shared(tlsOptions); +#elif defined(__APPLE__) + socket = std::make_shared(tlsOptions); +#elif defined(_WIN32) + socket = std::make_shared(tlsOptions); +#else + socket = std::make_shared(tlsOptions); +#endif #else errorMsg = "TLS support is not enabled on this platform."; return nullptr; @@ -62,8 +63,7 @@ namespace ix return socket; } - std::shared_ptr createSocket(int fd, - std::string& errorMsg) + std::shared_ptr createSocket(int fd, std::string& errorMsg) { errorMsg.clear(); @@ -75,4 +75,4 @@ namespace ix return socket; } -} +} // namespace ix diff --git a/ixwebsocket/IXSocketFactory.h b/ixwebsocket/IXSocketFactory.h index 09724450..49d5f839 100644 --- a/ixwebsocket/IXSocketFactory.h +++ b/ixwebsocket/IXSocketFactory.h @@ -10,10 +10,12 @@ #include #include +#include "IXSocketTLSOptions.h" + namespace ix { class Socket; - std::shared_ptr createSocket(bool tls, std::string& errorMsg); + std::shared_ptr createSocket(bool tls, std::string& errorMsg, const SocketTLSOptions& tlsOptions); std::shared_ptr createSocket(int fd, std::string& errorMsg); } // namespace ix diff --git a/ixwebsocket/IXSocketOpenSSL.cpp b/ixwebsocket/IXSocketOpenSSL.cpp index 502c43a1..c30db9d2 100644 --- a/ixwebsocket/IXSocketOpenSSL.cpp +++ b/ixwebsocket/IXSocketOpenSSL.cpp @@ -7,14 +7,12 @@ */ #include "IXSocketOpenSSL.h" + #include "IXSocketConnect.h" - #include - -#include - -#include #include +#include +#include #define socketerrno errno namespace ix @@ -22,9 +20,10 @@ namespace ix std::atomic SocketOpenSSL::_openSSLInitializationSuccessful(false); std::once_flag SocketOpenSSL::_openSSLInitFlag; - SocketOpenSSL::SocketOpenSSL(int fd) : Socket(fd), - _ssl_connection(nullptr), - _ssl_context(nullptr) + SocketOpenSSL::SocketOpenSSL(const SocketTLSOptions& tlsOptions, int fd) + : Socket(fd) + , _ssl_connection(nullptr) + , _ssl_context(nullptr) { std::call_once(_openSSLInitFlag, &SocketOpenSSL::openSSLInitialize, this); } @@ -114,14 +113,17 @@ namespace ix SSL_CTX* ctx = SSL_CTX_new(_ssl_method); if (ctx) { - // To skip verification, pass in SSL_VERIFY_NONE - SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, - [](int preverify, X509_STORE_CTX*) -> int - { - return preverify; - }); + if (!_tlsOptions.isPeerVerifyDisabled()) + { + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, [](int preverify, X509_STORE_CTX*) -> int { + return preverify; + }); + + SSL_CTX_set_verify_depth(ctx, 4); + } else { + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr); + } - SSL_CTX_set_verify_depth(ctx, 4); SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); } return ctx; @@ -130,16 +132,16 @@ namespace ix /** * Check whether a hostname matches a pattern */ - bool SocketOpenSSL::checkHost(const std::string& host, const char *pattern) + bool SocketOpenSSL::checkHost(const std::string& host, const char* pattern) { return fnmatch(pattern, host.c_str(), 0) != FNM_NOMATCH; } - bool SocketOpenSSL::openSSLCheckServerCert(SSL *ssl, + bool SocketOpenSSL::openSSLCheckServerCert(SSL* ssl, const std::string& hostname, std::string& errMsg) { - X509 *server_cert = SSL_get_peer_certificate(ssl); + X509* server_cert = SSL_get_peer_certificate(ssl); if (server_cert == nullptr) { errMsg = "OpenSSL failed - peer didn't present a X509 certificate."; @@ -149,18 +151,17 @@ namespace ix #if OPENSSL_VERSION_NUMBER < 0x10100000L // Check server name bool hostname_verifies_ok = false; - STACK_OF(GENERAL_NAME) *san_names = - (STACK_OF(GENERAL_NAME)*) X509_get_ext_d2i((X509 *)server_cert, - NID_subject_alt_name, NULL, NULL); + STACK_OF(GENERAL_NAME)* san_names = (STACK_OF(GENERAL_NAME)*) X509_get_ext_d2i( + (X509*) server_cert, NID_subject_alt_name, NULL, NULL); if (san_names) { - for (int i=0; itype == GEN_DNS) { - char *name = (char *)ASN1_STRING_data(sk_name->d.dNSName); - if ((size_t)ASN1_STRING_length(sk_name->d.dNSName) == strlen(name) && + char* name = (char*) ASN1_STRING_data(sk_name->d.dNSName); + if ((size_t) ASN1_STRING_length(sk_name->d.dNSName) == strlen(name) && checkHost(hostname, name)) { hostname_verifies_ok = true; @@ -173,20 +174,20 @@ namespace ix if (!hostname_verifies_ok) { - int cn_pos = X509_NAME_get_index_by_NID(X509_get_subject_name((X509 *)server_cert), - NID_commonName, -1); + int cn_pos = X509_NAME_get_index_by_NID( + X509_get_subject_name((X509*) server_cert), NID_commonName, -1); if (cn_pos) { - X509_NAME_ENTRY *cn_entry = X509_NAME_get_entry( - X509_get_subject_name((X509 *)server_cert), cn_pos); + X509_NAME_ENTRY* cn_entry = + X509_NAME_get_entry(X509_get_subject_name((X509*) server_cert), cn_pos); if (cn_entry) { - ASN1_STRING *cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry); - char *cn = (char *)ASN1_STRING_data(cn_asn1); + ASN1_STRING* cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry); + char* cn = (char*) ASN1_STRING_data(cn_asn1); - if ((size_t)ASN1_STRING_length(cn_asn1) == strlen(cn) && - checkHost(hostname, cn)) + if ((size_t) ASN1_STRING_length(cn_asn1) == strlen(cn) && + checkHost(hostname, cn)) { hostname_verifies_ok = true; } @@ -265,12 +266,63 @@ namespace ix } ERR_clear_error(); - int cert_load_result = SSL_CTX_set_default_verify_paths(_ssl_context); - if (cert_load_result == 0) + if (_tlsOptions.isUsingClientCert()) { - unsigned long ssl_err = ERR_get_error(); - errMsg = "OpenSSL failed - SSL_CTX_default_verify_paths loading failed: "; - errMsg += ERR_error_string(ssl_err, nullptr); + if (SSL_CTX_use_certificate_chain_file(_ssl_context, + _tlsOptions.certFile.c_str()) != 1) + { + auto sslErr = ERR_get_error(); + errMsg = "OpenSSL failed - SSL_CTX_use_certificate_chain_file(\"" + + _tlsOptions.certFile + "\") failed: "; + errMsg += ERR_error_string(sslErr, nullptr); + } + else if (SSL_CTX_use_PrivateKey_file( + _ssl_context, _tlsOptions.keyFile.c_str(), SSL_FILETYPE_PEM) != 1) + { + auto sslErr = ERR_get_error(); + errMsg = "OpenSSL failed - SSL_CTX_use_PrivateKey_file(\"" + + _tlsOptions.keyFile + "\") failed: "; + errMsg += ERR_error_string(sslErr, nullptr); + } + } + + + ERR_clear_error(); + if (!_tlsOptions.isPeerVerifyDisabled()) + { + if (_tlsOptions.isUsingSystemDefaults()) + { + if (SSL_CTX_set_default_verify_paths(_ssl_context) == 0) + { + auto sslErr = ERR_get_error(); + errMsg = "OpenSSL failed - SSL_CTX_default_verify_paths loading failed: "; + errMsg += ERR_error_string(sslErr, nullptr); + } + } + else + { + const char* root_ca_file = _tlsOptions.caFile.c_str(); + STACK_OF(X509_NAME) * rootCAs; + rootCAs = SSL_load_client_CA_file(root_ca_file); + if (rootCAs == NULL) + { + auto sslErr = ERR_get_error(); + errMsg = "OpenSSL failed - SSL_load_client_CA_file('" + _tlsOptions.caFile + + "') failed: "; + errMsg += ERR_error_string(sslErr, nullptr); + } + else + { + SSL_CTX_set_client_CA_list(_ssl_context, rootCAs); + if (SSL_CTX_load_verify_locations(_ssl_context, root_ca_file, NULL) != 1) + { + auto sslErr = ERR_get_error(); + errMsg = "OpenSSL failed - SSL_CTX_load_verify_locations(\"" + + _tlsOptions.caFile + "\") failed: "; + errMsg += ERR_error_string(sslErr, nullptr); + } + } + } } _ssl_connection = SSL_new(_ssl_context); @@ -291,10 +343,9 @@ namespace ix // (The docs say that this should work from 1.0.2, and is the default from // 1.1.0, but it does not. To be on the safe side, the manual test below is // enabled for all versions prior to 1.1.0.) - X509_VERIFY_PARAM *param = SSL_get0_param(_ssl_connection); + X509_VERIFY_PARAM* param = SSL_get0_param(_ssl_connection); X509_VERIFY_PARAM_set1_host(param, host.c_str(), 0); #endif - handshakeSuccessful = openSSLHandshake(host, errMsg); } @@ -342,13 +393,18 @@ namespace ix ssize_t write_result = SSL_write(_ssl_connection, buf + sent, (int) nbyte); int reason = SSL_get_error(_ssl_connection, (int) write_result); - if (reason == SSL_ERROR_NONE) { + if (reason == SSL_ERROR_NONE) + { nbyte -= write_result; sent += write_result; - } else if (reason == SSL_ERROR_WANT_READ || reason == SSL_ERROR_WANT_WRITE) { + } + else if (reason == SSL_ERROR_WANT_READ || reason == SSL_ERROR_WANT_WRITE) + { errno = EWOULDBLOCK; return -1; - } else { + } + else + { return -1; } } @@ -357,7 +413,7 @@ namespace ix ssize_t SocketOpenSSL::send(const std::string& buffer) { - return send((char*)&buffer[0], buffer.size()); + return send((char*) &buffer[0], buffer.size()); } ssize_t SocketOpenSSL::recv(void* buf, size_t nbyte) @@ -389,4 +445,4 @@ namespace ix } } -} +} // namespace ix diff --git a/ixwebsocket/IXSocketOpenSSL.h b/ixwebsocket/IXSocketOpenSSL.h index 5afe4125..f2888f56 100644 --- a/ixwebsocket/IXSocketOpenSSL.h +++ b/ixwebsocket/IXSocketOpenSSL.h @@ -8,6 +8,7 @@ #include "IXCancellationRequest.h" #include "IXSocket.h" +#include "IXSocketTLSOptions.h" #include #include #include @@ -20,7 +21,7 @@ namespace ix class SocketOpenSSL final : public Socket { public: - SocketOpenSSL(int fd = -1); + SocketOpenSSL(const SocketTLSOptions& tlsOptions, int fd = -1); ~SocketOpenSSL(); virtual bool connect(const std::string& host, @@ -44,6 +45,8 @@ namespace ix SSL* _ssl_connection; SSL_CTX* _ssl_context; const SSL_METHOD* _ssl_method; + SocketTLSOptions _tlsOptions; + mutable std::mutex _mutex; // OpenSSL routines are not thread-safe static std::once_flag _openSSLInitFlag; diff --git a/ixwebsocket/IXSocketTLSOptions.cpp b/ixwebsocket/IXSocketTLSOptions.cpp new file mode 100644 index 00000000..abdee1ef --- /dev/null +++ b/ixwebsocket/IXSocketTLSOptions.cpp @@ -0,0 +1,34 @@ +/* + * IXSocketTLSOptions.h + * Author: Benjamin Sergeant + * Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved. + */ + +#include +#include "IXSocketTLSOptions.h" + +namespace ix +{ + + SocketTLSOptions::SocketTLSOptions() { +#ifndef IXWEBSOCKET_USE_TLS + assert(false && "To use TLS features the library must be compiled with USE_TLS"); +#endif + } + + bool SocketTLSOptions::isUsingClientCert() const + { + return !certFile.empty() && !keyFile.empty(); + } + + bool SocketTLSOptions::isUsingSystemDefaults() const + { + return caFile == "SYSTEM"; + } + + bool SocketTLSOptions::isPeerVerifyDisabled() const + { + return caFile != "NONE"; + } + +} // namespace ix diff --git a/ixwebsocket/IXSocketTLSOptions.h b/ixwebsocket/IXSocketTLSOptions.h new file mode 100644 index 00000000..4d049ac8 --- /dev/null +++ b/ixwebsocket/IXSocketTLSOptions.h @@ -0,0 +1,32 @@ +/* + * IXSocketTLSOptions.h + * Author: Benjamin Sergeant + * Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved. + */ + +#pragma once + +#include + +namespace ix +{ + struct SocketTLSOptions + { + SocketTLSOptions(); + + // the certificate presented to peers + std::string certFile; + // the key used for signing/encryption + std::string keyFile; + // the ca certificate (or certificate bundle) file containing + // certificates to be trusted by peers; use 'SYSTEM' to + // leverage the system defaults, use 'NONE' to disable peer verification + std::string caFile = "SYSTEM"; + + bool isUsingClientCert() const; + + bool isUsingSystemDefaults() const; + + bool isPeerVerifyDisabled() const; + }; +} // namespace ix diff --git a/ixwebsocket/IXWebSocket.cpp b/ixwebsocket/IXWebSocket.cpp index ad6c2409..09bf2525 100644 --- a/ixwebsocket/IXWebSocket.cpp +++ b/ixwebsocket/IXWebSocket.cpp @@ -73,6 +73,12 @@ namespace ix _perMessageDeflateOptions = perMessageDeflateOptions; } + void WebSocket::setTLSOptions(const SocketTLSOptions& socketTLSOptions) + { + std::lock_guard lock(_configMutex); + _socketTLSOptions = socketTLSOptions; + } + const WebSocketPerMessageDeflateOptions& WebSocket::getPerMessageDeflateOptions() const { std::lock_guard lock(_configMutex); @@ -173,6 +179,7 @@ namespace ix { std::lock_guard lock(_configMutex); _ws.configure(_perMessageDeflateOptions, + _socketTLSOptions, _enablePong, _pingIntervalSecs, _pingTimeoutSecs); @@ -198,6 +205,7 @@ namespace ix { std::lock_guard lock(_configMutex); _ws.configure(_perMessageDeflateOptions, + _socketTLSOptions, _enablePong, _pingIntervalSecs, _pingTimeoutSecs); diff --git a/ixwebsocket/IXWebSocket.h b/ixwebsocket/IXWebSocket.h index c2bf898d..41dadaaf 100644 --- a/ixwebsocket/IXWebSocket.h +++ b/ixwebsocket/IXWebSocket.h @@ -10,6 +10,9 @@ #pragma once #include "IXProgressCallback.h" +#ifdef IXWEBSOCKET_USE_TLS +#include "IXSocketTLSOptions.h" +#endif #include "IXWebSocketCloseConstants.h" #include "IXWebSocketErrorInfo.h" #include "IXWebSocketHttpHeaders.h" @@ -49,6 +52,7 @@ namespace ix void setExtraHeaders(const WebSocketHttpHeaders& headers); void setPerMessageDeflateOptions( const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions); + void setTLSOptions(const SocketTLSOptions& socketTLSOptions); void setHeartBeatPeriod(int heartBeatPeriodSecs); void setPingInterval(int pingIntervalSecs); // alias of setHeartBeatPeriod void setPingTimeout(int pingTimeoutSecs); @@ -119,6 +123,9 @@ namespace ix WebSocketHttpHeaders _extraHeaders; WebSocketPerMessageDeflateOptions _perMessageDeflateOptions; + + SocketTLSOptions _socketTLSOptions; + mutable std::mutex _configMutex; // protect all config variables access OnMessageCallback _onMessageCallback; diff --git a/ixwebsocket/IXWebSocketTransport.cpp b/ixwebsocket/IXWebSocketTransport.cpp index 2a5d6707..2d65a5ab 100644 --- a/ixwebsocket/IXWebSocketTransport.cpp +++ b/ixwebsocket/IXWebSocketTransport.cpp @@ -32,6 +32,7 @@ // Adapted from https://github.com/dhbaird/easywsclient // +#include "IXSocketTLSOptions.h" #include "IXWebSocketTransport.h" #include "IXWebSocketHandshake.h" #include "IXWebSocketHttpHeaders.h" @@ -103,12 +104,14 @@ namespace ix } void WebSocketTransport::configure(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions, + const SocketTLSOptions& socketTLSOptions, bool enablePong, int pingIntervalSecs, int pingTimeoutSecs) { _perMessageDeflateOptions = perMessageDeflateOptions; _enablePerMessageDeflate = _perMessageDeflateOptions.enabled(); + _socketTLSOptions = socketTLSOptions; _enablePong = enablePong; _pingIntervalSecs = pingIntervalSecs; _pingTimeoutSecs = pingTimeoutSecs; @@ -147,7 +150,7 @@ namespace ix std::string errorMsg; bool tls = protocol == "wss"; - _socket = createSocket(tls, errorMsg); + _socket = createSocket(tls, errorMsg, _socketTLSOptions); if (!_socket) { diff --git a/ixwebsocket/IXWebSocketTransport.h b/ixwebsocket/IXWebSocketTransport.h index 3d4cc7a2..e3e9ae02 100644 --- a/ixwebsocket/IXWebSocketTransport.h +++ b/ixwebsocket/IXWebSocketTransport.h @@ -12,6 +12,7 @@ #include "IXCancellationRequest.h" #include "IXProgressCallback.h" +#include "IXSocketTLSOptions.h" #include "IXWebSocketCloseConstants.h" #include "IXWebSocketHandshake.h" #include "IXWebSocketHttpHeaders.h" @@ -72,6 +73,7 @@ namespace ix ~WebSocketTransport(); void configure(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions, + const SocketTLSOptions& socketTLSOptions, bool enablePong, int pingIntervalSecs, int pingTimeoutSecs); @@ -181,6 +183,9 @@ namespace ix WebSocketPerMessageDeflateOptions _perMessageDeflateOptions; std::atomic _enablePerMessageDeflate; + // Used to control TLS connection behavior + SocketTLSOptions _socketTLSOptions; + // Used to cancel dns lookup + socket connect + http upgrade std::atomic _requestInitCancellation;