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
This commit is contained in:
Matt DeBoer 2019-09-22 18:06:15 -07:00 committed by Benjamin Sergeant
parent ed4be773a2
commit 408ee41990
11 changed files with 222 additions and 70 deletions

View File

@ -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

View File

@ -8,15 +8,15 @@
#ifdef IXWEBSOCKET_USE_TLS
# ifdef IXWEBSOCKET_USE_MBED_TLS
# include <ixwebsocket/IXSocketMbedTLS.h>
# elif __APPLE__
# include <ixwebsocket/IXSocketAppleSSL.h>
# elif defined(_WIN32)
# include <ixwebsocket/IXSocketSChannel.h>
# elif defined(IXWEBSOCKET_USE_OPEN_SSL)
# include <ixwebsocket/IXSocketOpenSSL.h>
# endif
#ifdef IXWEBSOCKET_USE_MBED_TLS
#include <ixwebsocket/IXSocketMbedTLS.h>
#elif __APPLE__
#include <ixwebsocket/IXSocketAppleSSL.h>
#elif defined(_WIN32)
#include <ixwebsocket/IXSocketSChannel.h>
#elif defined(IXWEBSOCKET_USE_OPEN_SSL)
#include <ixwebsocket/IXSocketOpenSSL.h>
#endif
#else
@ -27,7 +27,8 @@
namespace ix
{
std::shared_ptr<Socket> createSocket(bool tls,
std::string& errorMsg)
std::string& errorMsg,
const SocketTLSOptions& tlsOptions)
{
errorMsg.clear();
std::shared_ptr<Socket> socket;
@ -39,15 +40,15 @@ namespace ix
else
{
#ifdef IXWEBSOCKET_USE_TLS
# if defined(IXWEBSOCKET_USE_MBED_TLS)
socket = std::make_shared<SocketMbedTLS>();
# elif defined(__APPLE__)
socket = std::make_shared<SocketAppleSSL>();
# elif defined(_WIN32)
socket = std::make_shared<SocketSChannel>();
# else
socket = std::make_shared<SocketOpenSSL>();
# endif
#if defined(IXWEBSOCKET_USE_MBED_TLS)
socket = std::make_shared<SocketMbedTLS>(tlsOptions);
#elif defined(__APPLE__)
socket = std::make_shared<SocketAppleSSL>(tlsOptions);
#elif defined(_WIN32)
socket = std::make_shared<SocketSChannel>(tlsOptions);
#else
socket = std::make_shared<SocketOpenSSL>(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<Socket> createSocket(int fd,
std::string& errorMsg)
std::shared_ptr<Socket> createSocket(int fd, std::string& errorMsg)
{
errorMsg.clear();
@ -75,4 +75,4 @@ namespace ix
return socket;
}
}
} // namespace ix

View File

@ -10,10 +10,12 @@
#include <memory>
#include <string>
#include "IXSocketTLSOptions.h"
namespace ix
{
class Socket;
std::shared_ptr<Socket> createSocket(bool tls, std::string& errorMsg);
std::shared_ptr<Socket> createSocket(bool tls, std::string& errorMsg, const SocketTLSOptions& tlsOptions);
std::shared_ptr<Socket> createSocket(int fd, std::string& errorMsg);
} // namespace ix

View File

@ -7,14 +7,12 @@
*/
#include "IXSocketOpenSSL.h"
#include "IXSocketConnect.h"
#include <cassert>
#include <openssl/x509v3.h>
#include <fnmatch.h>
#include <errno.h>
#include <fnmatch.h>
#include <openssl/x509v3.h>
#define socketerrno errno
namespace ix
@ -22,9 +20,10 @@ namespace ix
std::atomic<bool> 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; i<sk_GENERAL_NAME_num(san_names); i++)
for (int i = 0; i < sk_GENERAL_NAME_num(san_names); i++)
{
const GENERAL_NAME *sk_name = sk_GENERAL_NAME_value(san_names, i);
const GENERAL_NAME* sk_name = sk_GENERAL_NAME_value(san_names, i);
if (sk_name->type == 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

View File

@ -8,6 +8,7 @@
#include "IXCancellationRequest.h"
#include "IXSocket.h"
#include "IXSocketTLSOptions.h"
#include <mutex>
#include <openssl/bio.h>
#include <openssl/conf.h>
@ -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;

View File

@ -0,0 +1,34 @@
/*
* IXSocketTLSOptions.h
* Author: Benjamin Sergeant
* Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved.
*/
#include <assert.h>
#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

View File

@ -0,0 +1,32 @@
/*
* IXSocketTLSOptions.h
* Author: Benjamin Sergeant
* Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved.
*/
#pragma once
#include <string>
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

View File

@ -73,6 +73,12 @@ namespace ix
_perMessageDeflateOptions = perMessageDeflateOptions;
}
void WebSocket::setTLSOptions(const SocketTLSOptions& socketTLSOptions)
{
std::lock_guard<std::mutex> lock(_configMutex);
_socketTLSOptions = socketTLSOptions;
}
const WebSocketPerMessageDeflateOptions& WebSocket::getPerMessageDeflateOptions() const
{
std::lock_guard<std::mutex> lock(_configMutex);
@ -173,6 +179,7 @@ namespace ix
{
std::lock_guard<std::mutex> lock(_configMutex);
_ws.configure(_perMessageDeflateOptions,
_socketTLSOptions,
_enablePong,
_pingIntervalSecs,
_pingTimeoutSecs);
@ -198,6 +205,7 @@ namespace ix
{
std::lock_guard<std::mutex> lock(_configMutex);
_ws.configure(_perMessageDeflateOptions,
_socketTLSOptions,
_enablePong,
_pingIntervalSecs,
_pingTimeoutSecs);

View File

@ -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;

View File

@ -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)
{

View File

@ -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<bool> _enablePerMessageDeflate;
// Used to control TLS connection behavior
SocketTLSOptions _socketTLSOptions;
// Used to cancel dns lookup + socket connect + http upgrade
std::atomic<bool> _requestInitCancellation;