diff options
author | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2018-01-29 13:12:49 -0500 |
---|---|---|
committer | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2018-01-29 13:12:49 -0500 |
commit | f627a7ee4e2c864013212d401aa108ad24aa9c4a (patch) | |
tree | 9dac0081448c5c4e1c554dec570e9b21b60240e6 /src/mongo | |
parent | 6b1dec440cf18fdfa9f7aa285f6f3b4fcfd1fd86 (diff) | |
download | mongo-f627a7ee4e2c864013212d401aa108ad24aa9c4a.tar.gz |
SERVER-32748 Split ssl_manager.cpp into openssl specific and general components.
Diffstat (limited to 'src/mongo')
-rw-r--r-- | src/mongo/util/net/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager.cpp | 1490 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_none.cpp | 43 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_openssl.cpp | 1524 |
4 files changed, 1569 insertions, 1490 deletions
diff --git a/src/mongo/util/net/SConscript b/src/mongo/util/net/SConscript index 4344e2a3a27..d2ee1dbb387 100644 --- a/src/mongo/util/net/SConscript +++ b/src/mongo/util/net/SConscript @@ -1,6 +1,7 @@ # -*- mode: python; -*- Import('env') +Import('ssl_provider') env = env.Clone() @@ -20,6 +21,7 @@ env.Library( "sockaddr.cpp", "socket_exception.cpp", "ssl_manager.cpp", + 'ssl_manager_%s.cpp' % (ssl_provider), "ssl_options.cpp", "thread_idle_callback.cpp", ], diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp index 5c5e3f7fc18..c0f805eefa8 100644 --- a/src/mongo/util/net/ssl_manager.cpp +++ b/src/mongo/util/net/ssl_manager.cpp @@ -25,78 +25,28 @@ * then also delete it in the license file. */ -#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork - #include "mongo/platform/basic.h" #include "mongo/util/net/ssl_manager.h" #include <boost/algorithm/string.hpp> -#include <boost/date_time/posix_time/posix_time.hpp> -#include <fstream> -#include <iostream> -#include <sstream> -#include <stack> #include <string> #include <vector> -#include "mongo/base/init.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/config.h" #include "mongo/db/server_parameters.h" -#include "mongo/platform/atomic_word.h" -#include "mongo/stdx/memory.h" #include "mongo/transport/session.h" -#include "mongo/util/concurrency/mutex.h" -#include "mongo/util/debug_util.h" -#include "mongo/util/exit.h" -#include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" -#include "mongo/util/net/private/ssl_expiration.h" -#include "mongo/util/net/sock.h" -#include "mongo/util/net/socket_exception.h" #include "mongo/util/net/ssl_options.h" #include "mongo/util/net/ssl_types.h" -#include "mongo/util/scopeguard.h" #include "mongo/util/text.h" -#ifdef MONGO_CONFIG_SSL -#include <openssl/asn1.h> -#include <openssl/asn1t.h> -#include <openssl/evp.h> -#include <openssl/x509_vfy.h> -#include <openssl/x509v3.h> -#if defined(_WIN32) -#include <wincrypt.h> -#elif defined(__APPLE__) -#include <Security/Security.h> -#endif -#endif namespace mongo { namespace { -std::string removeFQDNRoot(std::string name) { - if (name.back() == '.') { - name.pop_back(); - } - return name; -}; - - -// Because the hostname having a slash is used by `mongo::SockAddr` to determine if a hostname is a -// Unix Domain Socket endpoint, this function uses the same logic. (See -// `mongo::SockAddr::Sockaddr(StringData, int, sa_family_t)`). A user explicitly specifying a Unix -// Domain Socket in the present working directory, through a code path which supplies `sa_family_t` -// as `AF_UNIX` will cause this code to lie. This will, in turn, cause the -// `SSLManager::parseAndValidatePeerCertificate` code to believe a socket is a host, which will then -// cause a connection failure if and only if that domain socket also has a certificate for SSL and -// the connection is an SSL connection. -bool isUnixDomainSocket(const std::string& hostname) { - return end(hostname) != std::find(begin(hostname), end(hostname), '/'); -} - const transport::Session::Decoration<SSLPeerInfo> peerInfoForSession = transport::Session::declareDecoration<SSLPeerInfo>(); @@ -150,456 +100,8 @@ public: return Status::OK(); } } openSSLCipherConfig; -} // namespace mongo #ifdef MONGO_CONFIG_SSL -namespace mongo { -namespace { - -// If the underlying SSL supports auto-configuration of ECDH parameters, this function will select -// it, otherwise this function will do nothing. -void setECDHModeAuto(SSL_CTX* const ctx) { -#ifdef MONGO_CONFIG_HAVE_SSL_SET_ECDH_AUTO - ::SSL_CTX_set_ecdh_auto(ctx, true); -#endif - std::ignore = ctx; -} - -struct DHFreer { - void operator()(DH* const dh) noexcept { - if (dh) { - ::DH_free(dh); - } - } -}; -using UniqueDHParams = std::unique_ptr<DH, DHFreer>; - -struct BIOFree { - void operator()(BIO* const p) noexcept { - // Assumes that BIO_free succeeds. - if (p) { - ::BIO_free(p); - } - } -}; -using UniqueBIO = std::unique_ptr<BIO, BIOFree>; - -UniqueBIO makeUniqueMemBio(std::vector<std::uint8_t>& v) { - UniqueBIO rv(::BIO_new_mem_buf(v.data(), v.size())); - if (!rv) { - class ssl_bad_alloc : public std::bad_alloc { - private: - std::string message; - - public: - explicit ssl_bad_alloc(std::string m) : message(std::move(m)) {} - - const char* what() const noexcept override { - return message.c_str(); - } - }; - throw ssl_bad_alloc(str::stream() - << "Error allocating SSL BIO: " - << SSLManagerInterface::getSSLErrorMessage(ERR_get_error())); - } - return rv; -} - -// Old copies of OpenSSL will not have constants to disable protocols they don't support. -// Define them to values we can OR together safely to generically disable these protocols across -// all versions of OpenSSL. -#ifndef SSL_OP_NO_TLSv1_1 -#define SSL_OP_NO_TLSv1_1 0 -#endif -#ifndef SSL_OP_NO_TLSv1_2 -#define SSL_OP_NO_TLSv1_2 0 -#endif - -// clang-format off -#ifndef MONGO_CONFIG_HAVE_ASN1_ANY_DEFINITIONS -// Copies of OpenSSL before 1.0.0 do not have ASN1_SEQUENCE_ANY, ASN1_SET_ANY, or the helper -// functions which let us deserialize these objects. We must polyfill the definitions to interact -// with ASN1 objects so stored. -typedef STACK_OF(ASN1_TYPE) ASN1_SEQUENCE_ANY; - -ASN1_ITEM_TEMPLATE(ASN1_SEQUENCE_ANY) = - ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SEQUENCE_OF, 0, ASN1_SEQUENCE_ANY, ASN1_ANY) -ASN1_ITEM_TEMPLATE_END(ASN1_SEQUENCE_ANY) - -ASN1_ITEM_TEMPLATE(ASN1_SET_ANY) = - ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SET_OF, 0, ASN1_SET_ANY, ASN1_ANY) -ASN1_ITEM_TEMPLATE_END(ASN1_SET_ANY) - -IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname(ASN1_SEQUENCE_ANY, ASN1_SEQUENCE_ANY, - ASN1_SEQUENCE_ANY) -IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname(ASN1_SEQUENCE_ANY, ASN1_SET_ANY, ASN1_SET_ANY) -; // clang format needs to see a semicolon or it will start formatting unrelated code -#endif // MONGO_CONFIG_NEEDS_ASN1_ANY_DEFINITIONS -// clang-format on - -#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) -// Copies of OpenSSL after 1.1.0 define new functions for interaction with -// X509 structure. We must polyfill used definitions to interact with older -// OpenSSL versions. -const STACK_OF(X509_EXTENSION) * X509_get0_extensions(const X509* peerCert) { - return peerCert->cert_info->extensions; -} -#endif - -/** - * Multithreaded Support for SSL. - * - * In order to allow OpenSSL to work in a multithreaded environment, you - * may need to provide some callbacks for it to use for locking. The following code - * sets up a vector of mutexes and provides a thread unique ID number. - * The so-called SSLThreadInfo class encapsulates most of the logic required for - * OpenSSL multithreaded support. - * - * OpenSSL before version 1.1.0 requires applications provide a callback which emits a thread - * identifier. This ID is used to store thread specific ERR information. When a thread is - * terminated, it must call ERR_remove_state or ERR_remove_thread_state. These functions may - * themselves invoke the application provided callback. These IDs are stored in a hashtable with - * a questionable hash function. They must be uniformly distributed to prevent collisions. - */ -class SSLThreadInfo { -public: - static unsigned long getID() { - struct CallErrRemoveState { - explicit CallErrRemoveState(ThreadIDManager& manager, unsigned long id) - : _manager(manager), id(id) {} - - ~CallErrRemoveState() { - ERR_remove_state(0); - _manager.releaseID(id); - }; - - ThreadIDManager& _manager; - unsigned long id; - }; - - // NOTE: This logic is fully intentional. Because ERR_remove_state (called within - // the destructor of the kRemoveStateFromThread object) re-enters this function, - // we must have a two phase protection, otherwise we would access a thread local - // during its destruction. - static thread_local boost::optional<CallErrRemoveState> threadLocalState; - if (!threadLocalState) { - threadLocalState.emplace(_idManager, _idManager.reserveID()); - } - - return threadLocalState->id; - } - - static void lockingCallback(int mode, int type, const char* file, int line) { - if (mode & CRYPTO_LOCK) { - _mutex[type]->lock(); - } else { - _mutex[type]->unlock(); - } - } - - static void init() { - CRYPTO_set_id_callback(&SSLThreadInfo::getID); - CRYPTO_set_locking_callback(&SSLThreadInfo::lockingCallback); - - while ((int)_mutex.size() < CRYPTO_num_locks()) { - _mutex.emplace_back(stdx::make_unique<stdx::recursive_mutex>()); - } - } - -private: - SSLThreadInfo() = delete; - - // Note: see SERVER-8734 for why we are using a recursive mutex here. - // Once the deadlock fix in OpenSSL is incorporated into most distros of - // Linux, this can be changed back to a nonrecursive mutex. - static std::vector<std::unique_ptr<stdx::recursive_mutex>> _mutex; - - class ThreadIDManager { - public: - unsigned long reserveID() { - stdx::unique_lock<stdx::mutex> lock(_idMutex); - if (!_idLast.empty()) { - unsigned long ret = _idLast.top(); - _idLast.pop(); - return ret; - } - return ++_idNext; - } - - void releaseID(unsigned long id) { - stdx::unique_lock<stdx::mutex> lock(_idMutex); - _idLast.push(id); - } - - private: - // Machinery for producing IDs that are unique for the life of a thread. - stdx::mutex _idMutex; // Protects _idNext and _idLast. - unsigned long _idNext = 0; // Stores the next thread ID to use, if none already allocated. - std::stack<unsigned long, std::vector<unsigned long>> - _idLast; // Stores old thread IDs, for reuse. - }; - static ThreadIDManager _idManager; -}; -std::vector<std::unique_ptr<stdx::recursive_mutex>> SSLThreadInfo::_mutex; -SSLThreadInfo::ThreadIDManager SSLThreadInfo::_idManager; - -namespace { -// We only want to free SSL_CTX objects if they have been populated. OpenSSL seems to perform this -// check before freeing them, but because it does not document this, we should protect ourselves. -void free_ssl_context(SSL_CTX* ctx) { - if (ctx != nullptr) { - SSL_CTX_free(ctx); - } -} -} // namespace - -//////////////////////////////////////////////////////////////// - -SimpleMutex sslManagerMtx; -SSLManagerInterface* theSSLManager = NULL; -using UniqueSSLContext = std::unique_ptr<SSL_CTX, decltype(&free_ssl_context)>; -static const int BUFFER_SIZE = 8 * 1024; -static const int DATE_LEN = 128; - -class SSLManager : public SSLManagerInterface { -public: - explicit SSLManager(const SSLParams& params, bool isServer); - - /** - * Initializes an OpenSSL context according to the provided settings. Only settings which are - * acceptable on non-blocking connections are set. - */ - Status initSSLContext(SSL_CTX* context, - const SSLParams& params, - ConnectionDirection direction) final; - - virtual SSLConnection* connect(Socket* socket); - - virtual SSLConnection* accept(Socket* socket, const char* initialBytes, int len); - - virtual SSLPeerInfo parseAndValidatePeerCertificateDeprecated(const SSLConnection* conn, - const std::string& remoteHost); - - StatusWith<boost::optional<SSLPeerInfo>> parseAndValidatePeerCertificate( - SSL* conn, const std::string& remoteHost) final; - - virtual const SSLConfiguration& getSSLConfiguration() const { - return _sslConfiguration; - } - - virtual int SSL_read(SSLConnection* conn, void* buf, int num); - - virtual int SSL_write(SSLConnection* conn, const void* buf, int num); - - virtual unsigned long ERR_get_error(); - - virtual char* ERR_error_string(unsigned long e, char* buf); - - virtual int SSL_get_error(const SSLConnection* conn, int ret); - - virtual int SSL_shutdown(SSLConnection* conn); - - virtual void SSL_free(SSLConnection* conn); - -private: - const int _rolesNid = OBJ_create(mongodbRolesOID.identifier.c_str(), - mongodbRolesOID.shortDescription.c_str(), - mongodbRolesOID.longDescription.c_str()); - UniqueSSLContext _serverContext; // SSL context for incoming connections - UniqueSSLContext _clientContext; // SSL context for outgoing connections - bool _weakValidation; - bool _allowInvalidCertificates; - bool _allowInvalidHostnames; - SSLConfiguration _sslConfiguration; - - /** - * creates an SSL object to be used for this file descriptor. - * caller must SSL_free it. - */ - SSL* _secure(SSL_CTX* context, int fd); - - /** - * Given an error code from an SSL-type IO function, logs an - * appropriate message and throws a NetworkException. - */ - MONGO_COMPILER_NORETURN void _handleSSLError(int code, int ret); - - /* - * Init the SSL context using parameters provided in params. This SSL context will - * be configured for blocking send/receive. - */ - bool _initSynchronousSSLContext(UniqueSSLContext* context, - const SSLParams& params, - ConnectionDirection direction); - - /* - * Converts time from OpenSSL return value to unsigned long long - * representing the milliseconds since the epoch. - */ - unsigned long long _convertASN1ToMillis(ASN1_TIME* t); - - /* - * Parse and store x509 subject name from the PEM keyfile. - * For server instances check that PEM certificate is not expired - * and extract server certificate notAfter date. - * @param keyFile referencing the PEM file to be read. - * @param subjectName as a pointer to the subject name variable being set. - * @param serverNotAfter a Date_t object pointer that is valued if the - * date is to be checked (as for a server certificate) and null otherwise. - * @return bool showing if the function was successful. - */ - bool _parseAndValidateCertificate(const std::string& keyFile, - const std::string& keyPassword, - std::string* subjectName, - Date_t* serverNotAfter); - - - StatusWith<stdx::unordered_set<RoleName>> _parsePeerRoles(X509* peerCert) const; - - /** @return true if was successful, otherwise false */ - bool _setupPEM(SSL_CTX* context, const std::string& keyFile, const std::string& password); - - /* - * Set up an SSL context for certificate validation by loading a CA - */ - Status _setupCA(SSL_CTX* context, const std::string& caFile); - - /* - * Set up an SSL context for certificate validation by loading the system's CA store - */ - Status _setupSystemCA(SSL_CTX* context); - - /* - * Import a certificate revocation list into an SSL context - * for use with validating certificates - */ - bool _setupCRL(SSL_CTX* context, const std::string& crlFile); - - /* - * sub function for checking the result of an SSL operation - */ - bool _doneWithSSLOp(SSLConnection* conn, int status); - - /* - * Send and receive network data - */ - void _flushNetworkBIO(SSLConnection* conn); - - /** - * Callbacks for SSL functions. - */ - static int password_cb(char* buf, int num, int rwflag, void* userdata); - static int verify_cb(int ok, X509_STORE_CTX* ctx); -}; - -void setupFIPS() { -// Turn on FIPS mode if requested, OPENSSL_FIPS must be defined by the OpenSSL headers -#if defined(MONGO_CONFIG_HAVE_FIPS_MODE_SET) - int status = FIPS_mode_set(1); - if (!status) { - severe() << "can't activate FIPS mode: " - << SSLManagerInterface::getSSLErrorMessage(ERR_get_error()); - fassertFailedNoTrace(16703); - } - log() << "FIPS 140-2 mode activated"; -#else - severe() << "this version of mongodb was not compiled with FIPS support"; - fassertFailedNoTrace(17089); -#endif -} - -} // namespace - -// Global variable indicating if this is a server or a client instance -bool isSSLServer = false; - - -MONGO_INITIALIZER(SetupOpenSSL)(InitializerContext*) { - SSL_library_init(); - SSL_load_error_strings(); - ERR_load_crypto_strings(); - - if (sslGlobalParams.sslFIPSMode) { - setupFIPS(); - } - - // Add all digests and ciphers to OpenSSL's internal table - // so that encryption/decryption is backwards compatible - OpenSSL_add_all_algorithms(); - - // Setup OpenSSL multithreading callbacks and mutexes - SSLThreadInfo::init(); - - return Status::OK(); -} - -MONGO_INITIALIZER_WITH_PREREQUISITES(SSLManager, ("SetupOpenSSL"))(InitializerContext*) { - stdx::lock_guard<SimpleMutex> lck(sslManagerMtx); - if (!isSSLServer || (sslGlobalParams.sslMode.load() != SSLParams::SSLMode_disabled)) { - theSSLManager = new SSLManager(sslGlobalParams, isSSLServer); - } - return Status::OK(); -} - -std::unique_ptr<SSLManagerInterface> SSLManagerInterface::create(const SSLParams& params, - bool isServer) { - return stdx::make_unique<SSLManager>(params, isServer); -} - -SSLManagerInterface* getSSLManager() { - stdx::lock_guard<SimpleMutex> lck(sslManagerMtx); - if (theSSLManager) - return theSSLManager; - return NULL; -} - -std::string getCertificateSubjectName(X509* cert) { - std::string result; - - BIO* out = BIO_new(BIO_s_mem()); - uassert(16884, "unable to allocate BIO memory", NULL != out); - ON_BLOCK_EXIT(BIO_free, out); - - if (X509_NAME_print_ex(out, X509_get_subject_name(cert), 0, XN_FLAG_RFC2253) >= 0) { - if (BIO_number_written(out) > 0) { - result.resize(BIO_number_written(out)); - BIO_read(out, &result[0], result.size()); - } - } else { - log() << "failed to convert subject name to RFC2253 format"; - } - - return result; -} - -SSLConnection::SSLConnection(SSL_CTX* context, Socket* sock, const char* initialBytes, int len) - : socket(sock) { - ssl = SSL_new(context); - - std::string sslErr = - NULL != getSSLManager() ? getSSLManager()->getSSLErrorMessage(ERR_get_error()) : ""; - massert(15861, "Error creating new SSL object " + sslErr, ssl); - - BIO_new_bio_pair(&internalBIO, BUFFER_SIZE, &networkBIO, BUFFER_SIZE); - SSL_set_bio(ssl, internalBIO, internalBIO); - - if (len > 0) { - int toBIO = BIO_write(networkBIO, initialBytes, len); - if (toBIO != len) { - LOG(3) << "Failed to write initial network data to the SSL BIO layer"; - throwSocketError(SocketErrorKind::RECV_ERROR, socket->remoteString()); - } - } -} - -SSLConnection::~SSLConnection() { - if (ssl) { // The internalBIO is automatically freed as part of SSL_free - SSL_free(ssl); - } - if (networkBIO) { - BIO_free(networkBIO); - } -} namespace { void canonicalizeClusterDN(std::vector<std::string>* dn) { @@ -647,998 +149,6 @@ BSONObj SSLConfiguration::getServerStatusBSON() const { SSLManagerInterface::~SSLManagerInterface() {} -SSLManager::SSLManager(const SSLParams& params, bool isServer) - : _serverContext(nullptr, free_ssl_context), - _clientContext(nullptr, free_ssl_context), - _weakValidation(params.sslWeakCertificateValidation), - _allowInvalidCertificates(params.sslAllowInvalidCertificates), - _allowInvalidHostnames(params.sslAllowInvalidHostnames) { - if (!_initSynchronousSSLContext(&_clientContext, params, ConnectionDirection::kOutgoing)) { - uasserted(16768, "ssl initialization problem"); - } - - // pick the certificate for use in outgoing connections, - std::string clientPEM, clientPassword; - if (!isServer || params.sslClusterFile.empty()) { - // We are either a client, or a server without a cluster key, - // so use the PEM key file, if specified - clientPEM = params.sslPEMKeyFile; - clientPassword = params.sslPEMKeyPassword; - } else { - // We are a server with a cluster key, so use the cluster key file - clientPEM = params.sslClusterFile; - clientPassword = params.sslClusterPassword; - } - - if (!clientPEM.empty()) { - if (!_parseAndValidateCertificate( - clientPEM, clientPassword, &_sslConfiguration.clientSubjectName, NULL)) { - uasserted(16941, "ssl initialization problem"); - } - } - // SSL server specific initialization - if (isServer) { - if (!_initSynchronousSSLContext(&_serverContext, params, ConnectionDirection::kIncoming)) { - uasserted(16562, "ssl initialization problem"); - } - - if (!_parseAndValidateCertificate(params.sslPEMKeyFile, - params.sslPEMKeyPassword, - &_sslConfiguration.serverSubjectName, - &_sslConfiguration.serverCertificateExpirationDate)) { - uasserted(16942, "ssl initialization problem"); - } - - static CertificateExpirationMonitor task = - CertificateExpirationMonitor(_sslConfiguration.serverCertificateExpirationDate); - } -} - -int SSLManager::password_cb(char* buf, int num, int rwflag, void* userdata) { - // Unless OpenSSL misbehaves, num should always be positive - fassert(17314, num > 0); - invariant(userdata); - auto pw = static_cast<const std::string*>(userdata); - - const size_t copied = pw->copy(buf, num - 1); - buf[copied] = '\0'; - return copied; -} - -int SSLManager::verify_cb(int ok, X509_STORE_CTX* ctx) { - return 1; // always succeed; we will catch the error in our get_verify_result() call -} - -int SSLManager::SSL_read(SSLConnection* conn, void* buf, int num) { - int status; - do { - status = ::SSL_read(conn->ssl, buf, num); - } while (!_doneWithSSLOp(conn, status)); - - if (status <= 0) - _handleSSLError(SSL_get_error(conn, status), status); - return status; -} - -int SSLManager::SSL_write(SSLConnection* conn, const void* buf, int num) { - int status; - do { - status = ::SSL_write(conn->ssl, buf, num); - } while (!_doneWithSSLOp(conn, status)); - - if (status <= 0) - _handleSSLError(SSL_get_error(conn, status), status); - return status; -} - -unsigned long SSLManager::ERR_get_error() { - return ::ERR_get_error(); -} - -char* SSLManager::ERR_error_string(unsigned long e, char* buf) { - return ::ERR_error_string(e, buf); -} - -int SSLManager::SSL_get_error(const SSLConnection* conn, int ret) { - return ::SSL_get_error(conn->ssl, ret); -} - -int SSLManager::SSL_shutdown(SSLConnection* conn) { - int status; - do { - status = ::SSL_shutdown(conn->ssl); - } while (!_doneWithSSLOp(conn, status)); - - if (status < 0) - _handleSSLError(SSL_get_error(conn, status), status); - return status; -} - -void SSLManager::SSL_free(SSLConnection* conn) { - return ::SSL_free(conn->ssl); -} - -Status SSLManager::initSSLContext(SSL_CTX* context, - const SSLParams& params, - ConnectionDirection direction) { - // SSL_OP_ALL - Activate all bug workaround options, to support buggy client SSL's. - // SSL_OP_NO_SSLv2 - Disable SSL v2 support - // SSL_OP_NO_SSLv3 - Disable SSL v3 support - long supportedProtocols = SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3; - - // Set the supported TLS protocols. Allow --sslDisabledProtocols to disable selected - // ciphers. - for (const SSLParams::Protocols& protocol : params.sslDisabledProtocols) { - if (protocol == SSLParams::Protocols::TLS1_0) { - supportedProtocols |= SSL_OP_NO_TLSv1; - } else if (protocol == SSLParams::Protocols::TLS1_1) { - supportedProtocols |= SSL_OP_NO_TLSv1_1; - } else if (protocol == SSLParams::Protocols::TLS1_2) { - supportedProtocols |= SSL_OP_NO_TLSv1_2; - } - } - ::SSL_CTX_set_options(context, supportedProtocols); - - // HIGH - Enable strong ciphers - // !EXPORT - Disable export ciphers (40/56 bit) - // !aNULL - Disable anonymous auth ciphers - // @STRENGTH - Sort ciphers based on strength - std::string cipherConfig = "HIGH:!EXPORT:!aNULL@STRENGTH"; - - // Allow the cipher configuration string to be overriden by --sslCipherConfig - if (!params.sslCipherConfig.empty()) { - cipherConfig = params.sslCipherConfig; - } - - if (0 == ::SSL_CTX_set_cipher_list(context, cipherConfig.c_str())) { - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Can not set supported cipher suites: " - << getSSLErrorMessage(ERR_get_error())); - } - - // We use the address of the context as the session id context. - if (0 == ::SSL_CTX_set_session_id_context( - context, reinterpret_cast<unsigned char*>(&context), sizeof(context))) { - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Can not store ssl session id context: " - << getSSLErrorMessage(ERR_get_error())); - } - - if (direction == ConnectionDirection::kOutgoing && !params.sslClusterFile.empty()) { - ::EVP_set_pw_prompt("Enter cluster certificate passphrase"); - if (!_setupPEM(context, params.sslClusterFile, params.sslClusterPassword)) { - return Status(ErrorCodes::InvalidSSLConfiguration, "Can not set up ssl clusterFile."); - } - } else if (!params.sslPEMKeyFile.empty()) { - // Use the pemfile for everything else - ::EVP_set_pw_prompt("Enter PEM passphrase"); - if (!_setupPEM(context, params.sslPEMKeyFile, params.sslPEMKeyPassword)) { - return Status(ErrorCodes::InvalidSSLConfiguration, "Can not set up PEM key file."); - } - } - - const auto status = - params.sslCAFile.empty() ? _setupSystemCA(context) : _setupCA(context, params.sslCAFile); - if (!status.isOK()) - return status; - - if (!params.sslCRLFile.empty()) { - if (!_setupCRL(context, params.sslCRLFile)) { - return Status(ErrorCodes::InvalidSSLConfiguration, "Can not set up CRL file."); - } - } - - if (!params.sslPEMTempDHParam.empty()) { - try { - std::ifstream dhparamPemFile(params.sslPEMTempDHParam, std::ios_base::binary); - if (dhparamPemFile.fail() || dhparamPemFile.bad()) { - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Cannot open PEM DHParams file."); - } - - std::vector<std::uint8_t> paramData{std::istreambuf_iterator<char>(dhparamPemFile), - std::istreambuf_iterator<char>()}; - auto bio = makeUniqueMemBio(paramData); - - UniqueDHParams dhparams(::PEM_read_bio_DHparams(bio.get(), nullptr, nullptr, nullptr)); - if (!dhparams) { - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Error reading DHParams file." - << getSSLErrorMessage(ERR_get_error())); - } - - if (::SSL_CTX_set_tmp_dh(context, dhparams.get()) != 1) { - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Failure to set PFS DH parameters: " - << getSSLErrorMessage(ERR_get_error())); - } - } catch (const std::exception& ex) { - return Status(ErrorCodes::InvalidSSLConfiguration, ex.what()); - } - } - - // We always set ECDH mode anyhow, if available. - setECDHModeAuto(context); - - return Status::OK(); -} - -bool SSLManager::_initSynchronousSSLContext(UniqueSSLContext* contextPtr, - const SSLParams& params, - ConnectionDirection direction) { - *contextPtr = UniqueSSLContext(SSL_CTX_new(SSLv23_method()), free_ssl_context); - - uassertStatusOK(initSSLContext(contextPtr->get(), params, direction)); - - // If renegotiation is needed, don't return from recv() or send() until it's successful. - // Note: this is for blocking sockets only. - SSL_CTX_set_mode(contextPtr->get(), SSL_MODE_AUTO_RETRY); - - return true; -} - -unsigned long long SSLManager::_convertASN1ToMillis(ASN1_TIME* asn1time) { - BIO* outBIO = BIO_new(BIO_s_mem()); - int timeError = ASN1_TIME_print(outBIO, asn1time); - ON_BLOCK_EXIT(BIO_free, outBIO); - - if (timeError <= 0) { - error() << "ASN1_TIME_print failed or wrote no data."; - return 0; - } - - char dateChar[DATE_LEN]; - timeError = BIO_gets(outBIO, dateChar, DATE_LEN); - if (timeError <= 0) { - error() << "BIO_gets call failed to transfer contents to buf"; - return 0; - } - - // Ensure that day format is two digits for parsing. - // Jun 8 17:00:03 2014 becomes Jun 08 17:00:03 2014. - if (dateChar[4] == ' ') { - dateChar[4] = '0'; - } - - std::istringstream inStringStream((std::string(dateChar, 20))); - boost::posix_time::time_input_facet* inputFacet = - new boost::posix_time::time_input_facet("%b %d %H:%M:%S %Y"); - - inStringStream.imbue(std::locale(std::cout.getloc(), inputFacet)); - boost::posix_time::ptime posixTime; - inStringStream >> posixTime; - - const boost::gregorian::date epoch = boost::gregorian::date(1970, boost::gregorian::Jan, 1); - - return (posixTime - boost::posix_time::ptime(epoch)).total_milliseconds(); -} - -bool SSLManager::_parseAndValidateCertificate(const std::string& keyFile, - const std::string& keyPassword, - std::string* subjectName, - Date_t* serverCertificateExpirationDate) { - BIO* inBIO = BIO_new(BIO_s_file()); - if (inBIO == NULL) { - error() << "failed to allocate BIO object: " << getSSLErrorMessage(ERR_get_error()); - return false; - } - - ON_BLOCK_EXIT(BIO_free, inBIO); - if (BIO_read_filename(inBIO, keyFile.c_str()) <= 0) { - error() << "cannot read key file when setting subject name: " << keyFile << ' ' - << getSSLErrorMessage(ERR_get_error()); - return false; - } - - // Callback will not manipulate the password, so const_cast is safe. - X509* x509 = PEM_read_bio_X509(inBIO, - NULL, - &SSLManager::password_cb, - const_cast<void*>(static_cast<const void*>(&keyPassword))); - if (x509 == NULL) { - error() << "cannot retrieve certificate from keyfile: " << keyFile << ' ' - << getSSLErrorMessage(ERR_get_error()); - return false; - } - ON_BLOCK_EXIT(X509_free, x509); - - *subjectName = getCertificateSubjectName(x509); - if (serverCertificateExpirationDate != NULL) { - unsigned long long notBeforeMillis = _convertASN1ToMillis(X509_get_notBefore(x509)); - if (notBeforeMillis == 0) { - error() << "date conversion failed"; - return false; - } - - unsigned long long notAfterMillis = _convertASN1ToMillis(X509_get_notAfter(x509)); - if (notAfterMillis == 0) { - error() << "date conversion failed"; - return false; - } - - if ((notBeforeMillis > curTimeMillis64()) || (curTimeMillis64() > notAfterMillis)) { - severe() << "The provided SSL certificate is expired or not yet valid."; - fassertFailedNoTrace(28652); - } - - *serverCertificateExpirationDate = Date_t::fromMillisSinceEpoch(notAfterMillis); - } - - return true; -} - -bool SSLManager::_setupPEM(SSL_CTX* context, - const std::string& keyFile, - const std::string& password) { - if (SSL_CTX_use_certificate_chain_file(context, keyFile.c_str()) != 1) { - error() << "cannot read certificate file: " << keyFile << ' ' - << getSSLErrorMessage(ERR_get_error()); - return false; - } - - BIO* inBio = BIO_new(BIO_s_file()); - if (!inBio) { - error() << "failed to allocate BIO object: " << getSSLErrorMessage(ERR_get_error()); - return false; - } - const auto bioGuard = MakeGuard([&inBio]() { BIO_free(inBio); }); - - if (BIO_read_filename(inBio, keyFile.c_str()) <= 0) { - error() << "cannot read PEM key file: " << keyFile << ' ' - << getSSLErrorMessage(ERR_get_error()); - return false; - } - - // If password is empty, use default OpenSSL callback, which uses the terminal - // to securely request the password interactively from the user. - decltype(&SSLManager::password_cb) password_cb = nullptr; - void* userdata = nullptr; - if (!password.empty()) { - password_cb = &SSLManager::password_cb; - // SSLManager::password_cb will not manipulate the password, so const_cast is safe. - userdata = const_cast<void*>(static_cast<const void*>(&password)); - } - EVP_PKEY* privateKey = PEM_read_bio_PrivateKey(inBio, nullptr, password_cb, userdata); - if (!privateKey) { - error() << "cannot read PEM key file: " << keyFile << ' ' - << getSSLErrorMessage(ERR_get_error()); - return false; - } - const auto privateKeyGuard = MakeGuard([&privateKey]() { EVP_PKEY_free(privateKey); }); - - if (SSL_CTX_use_PrivateKey(context, privateKey) != 1) { - error() << "cannot use PEM key file: " << keyFile << ' ' - << getSSLErrorMessage(ERR_get_error()); - return false; - } - - // Verify that the certificate and the key go together. - if (SSL_CTX_check_private_key(context) != 1) { - error() << "SSL certificate validation: " << getSSLErrorMessage(ERR_get_error()); - return false; - } - - return true; -} - -Status SSLManager::_setupCA(SSL_CTX* context, const std::string& caFile) { - // Set the list of CAs sent to clients - STACK_OF(X509_NAME)* certNames = SSL_load_client_CA_file(caFile.c_str()); - if (certNames == NULL) { - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "cannot read certificate authority file: " << caFile << " " - << getSSLErrorMessage(ERR_get_error())); - } - SSL_CTX_set_client_CA_list(context, certNames); - - // Load trusted CA - if (SSL_CTX_load_verify_locations(context, caFile.c_str(), NULL) != 1) { - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "cannot read certificate authority file: " << caFile << " " - << getSSLErrorMessage(ERR_get_error())); - } - - // Set SSL to require peer (client) certificate verification - // if a certificate is presented - SSL_CTX_set_verify(context, SSL_VERIFY_PEER, &SSLManager::verify_cb); - _sslConfiguration.hasCA = true; - return Status::OK(); -} - -inline Status checkX509_STORE_error() { - const auto errCode = ERR_peek_last_error(); - if (ERR_GET_LIB(errCode) != ERR_LIB_X509 || - ERR_GET_REASON(errCode) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { - return {ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Error adding certificate to X509 store: " - << ERR_reason_error_string(errCode)}; - } - return Status::OK(); -} - -#if defined(_WIN32) -// This imports the certificates in a given Windows certificate store into an X509_STORE for -// openssl to use during certificate validation. -Status importCertStoreToX509_STORE(const wchar_t* storeName, - DWORD storeLocation, - X509_STORE* verifyStore) { - HCERTSTORE systemStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, - 0, - NULL, - storeLocation | CERT_STORE_READONLY_FLAG, - const_cast<LPWSTR>(storeName)); - if (systemStore == NULL) { - return {ErrorCodes::InvalidSSLConfiguration, - str::stream() << "error opening system CA store: " << errnoWithDescription()}; - } - auto systemStoreGuard = MakeGuard([systemStore]() { CertCloseStore(systemStore, 0); }); - - PCCERT_CONTEXT certCtx = NULL; - while ((certCtx = CertEnumCertificatesInStore(systemStore, certCtx)) != NULL) { - auto certBytes = static_cast<const unsigned char*>(certCtx->pbCertEncoded); - X509* x509Obj = d2i_X509(NULL, &certBytes, certCtx->cbCertEncoded); - if (x509Obj == NULL) { - return {ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Error parsing X509 object from Windows certificate store" - << SSLManagerInterface::getSSLErrorMessage(ERR_get_error())}; - } - const auto x509ObjGuard = MakeGuard([&x509Obj]() { X509_free(x509Obj); }); - - if (X509_STORE_add_cert(verifyStore, x509Obj) != 1) { - auto status = checkX509_STORE_error(); - if (!status.isOK()) - return status; - } - } - int lastError = GetLastError(); - if (lastError != CRYPT_E_NOT_FOUND) { - return {ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Error enumerating certificates: " - << errnoWithDescription(lastError)}; - } - - return Status::OK(); -} -#elif defined(__APPLE__) - -template <typename T> -class CFTypeRefHolder { -public: - explicit CFTypeRefHolder(T ptr) : ref(static_cast<CFTypeRef>(ptr)) {} - ~CFTypeRefHolder() { - CFRelease(ref); - } - operator T() { - return static_cast<T>(ref); - } - -private: - CFTypeRef ref = nullptr; -}; -template <typename T> -CFTypeRefHolder<T> makeCFTypeRefHolder(T ptr) { - return CFTypeRefHolder<T>(ptr); -} - -std::string OSStatusToString(OSStatus status) { - auto errMsg = makeCFTypeRefHolder(SecCopyErrorMessageString(status, NULL)); - return std::string{CFStringGetCStringPtr(errMsg, kCFStringEncodingUTF8)}; -} - -Status importKeychainToX509_STORE(X509_STORE* verifyStore) { - CFArrayRef result; - OSStatus status; - - // This copies all the certificates trusted by the system (regardless of what keychain they're - // attached to) into a CFArray. - if ((status = SecTrustCopyAnchorCertificates(&result)) != 0) { - return {ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Error enumerating certificates: " << OSStatusToString(status)}; - } - const auto resultGuard = makeCFTypeRefHolder(result); - - for (CFIndex i = 0; i < CFArrayGetCount(result); i++) { - SecCertificateRef cert = - static_cast<SecCertificateRef>(const_cast<void*>(CFArrayGetValueAtIndex(result, i))); - - auto rawData = makeCFTypeRefHolder(SecCertificateCopyData(cert)); - if (!rawData) { - return {ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Error enumerating certificates: " - << OSStatusToString(status)}; - } - const uint8_t* rawDataPtr = CFDataGetBytePtr(rawData); - - // Parse an openssl X509 object from each returned certificate - X509* x509Cert = d2i_X509(nullptr, &rawDataPtr, CFDataGetLength(rawData)); - if (!x509Cert) { - return {ErrorCodes::InvalidSSLConfiguration, - str::stream() << "Error parsing X509 certificate from system keychain: " - << ERR_reason_error_string(ERR_peek_last_error())}; - } - const auto x509CertGuard = MakeGuard([&x509Cert]() { X509_free(x509Cert); }); - - // Add the parsed X509 object to the X509_STORE verification store - if (X509_STORE_add_cert(verifyStore, x509Cert) != 1) { - auto status = checkX509_STORE_error(); - if (!status.isOK()) - return status; - } - } - - return Status::OK(); -} #endif -Status SSLManager::_setupSystemCA(SSL_CTX* context) { -#if !defined(_WIN32) && !defined(__APPLE__) - // On non-Windows/non-Apple platforms, the OpenSSL libraries should have been configured - // with default locations for CA certificates. - if (SSL_CTX_set_default_verify_paths(context) != 1) { - return {ErrorCodes::InvalidSSLConfiguration, - str::stream() << "error loading system CA certificates " - << "(default certificate file: " - << X509_get_default_cert_file() - << ", " - << "default certificate path: " - << X509_get_default_cert_dir() - << ")"}; - } - return Status::OK(); -#else - - X509_STORE* verifyStore = SSL_CTX_get_cert_store(context); - if (!verifyStore) { - return {ErrorCodes::InvalidSSLConfiguration, - "no X509 store found for SSL context while loading system certificates"}; - } -#if defined(_WIN32) - auto status = importCertStoreToX509_STORE(L"root", CERT_SYSTEM_STORE_CURRENT_USER, verifyStore); - if (!status.isOK()) - return status; - return importCertStoreToX509_STORE(L"CA", CERT_SYSTEM_STORE_CURRENT_USER, verifyStore); -#elif defined(__APPLE__) - return importKeychainToX509_STORE(verifyStore); -#endif -#endif -} - -bool SSLManager::_setupCRL(SSL_CTX* context, const std::string& crlFile) { - X509_STORE* store = SSL_CTX_get_cert_store(context); - fassert(16583, store); - - X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK); - X509_LOOKUP* lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file()); - fassert(16584, lookup); - - int status = X509_load_crl_file(lookup, crlFile.c_str(), X509_FILETYPE_PEM); - if (status == 0) { - error() << "cannot read CRL file: " << crlFile << ' ' - << getSSLErrorMessage(ERR_get_error()); - return false; - } - log() << "ssl imported " << status << " revoked certificate" << ((status == 1) ? "" : "s") - << " from the revocation list."; - return true; -} - -/* -* The interface layer between network and BIO-pair. The BIO-pair buffers -* the data to/from the TLS layer. -*/ -void SSLManager::_flushNetworkBIO(SSLConnection* conn) { - char buffer[BUFFER_SIZE]; - int wantWrite; - - /* - * Write the complete contents of the buffer. Leaving the buffer - * unflushed could cause a deadlock. - */ - while ((wantWrite = BIO_ctrl_pending(conn->networkBIO)) > 0) { - if (wantWrite > BUFFER_SIZE) { - wantWrite = BUFFER_SIZE; - } - int fromBIO = BIO_read(conn->networkBIO, buffer, wantWrite); - - int writePos = 0; - do { - int numWrite = fromBIO - writePos; - numWrite = send(conn->socket->rawFD(), buffer + writePos, numWrite, portSendFlags); - if (numWrite < 0) { - conn->socket->handleSendError(numWrite, ""); - } - writePos += numWrite; - } while (writePos < fromBIO); - } - - int wantRead; - while ((wantRead = BIO_ctrl_get_read_request(conn->networkBIO)) > 0) { - if (wantRead > BUFFER_SIZE) { - wantRead = BUFFER_SIZE; - } - - int numRead = recv(conn->socket->rawFD(), buffer, wantRead, portRecvFlags); - if (numRead <= 0) { - conn->socket->handleRecvError(numRead, wantRead); - continue; - } - - int toBIO = BIO_write(conn->networkBIO, buffer, numRead); - if (toBIO != numRead) { - LOG(3) << "Failed to write network data to the SSL BIO layer"; - throwSocketError(SocketErrorKind::RECV_ERROR, conn->socket->remoteString()); - } - } -} - -bool SSLManager::_doneWithSSLOp(SSLConnection* conn, int status) { - int sslErr = SSL_get_error(conn, status); - switch (sslErr) { - case SSL_ERROR_NONE: - _flushNetworkBIO(conn); // success, flush network BIO before leaving - return true; - case SSL_ERROR_WANT_WRITE: - case SSL_ERROR_WANT_READ: - _flushNetworkBIO(conn); // not ready, flush network BIO and try again - return false; - default: - return true; - } -} - -SSLConnection* SSLManager::connect(Socket* socket) { - std::unique_ptr<SSLConnection> sslConn = - stdx::make_unique<SSLConnection>(_clientContext.get(), socket, (const char*)NULL, 0); - - const auto undotted = removeFQDNRoot(socket->remoteAddr().hostOrIp()); - int ret = ::SSL_set_tlsext_host_name(sslConn->ssl, undotted.c_str()); - if (ret != 1) - _handleSSLError(SSL_get_error(sslConn.get(), ret), ret); - - do { - ret = ::SSL_connect(sslConn->ssl); - } while (!_doneWithSSLOp(sslConn.get(), ret)); - - if (ret != 1) - _handleSSLError(SSL_get_error(sslConn.get(), ret), ret); - - return sslConn.release(); -} - -SSLConnection* SSLManager::accept(Socket* socket, const char* initialBytes, int len) { - std::unique_ptr<SSLConnection> sslConn = - stdx::make_unique<SSLConnection>(_serverContext.get(), socket, initialBytes, len); - - int ret; - do { - ret = ::SSL_accept(sslConn->ssl); - } while (!_doneWithSSLOp(sslConn.get(), ret)); - - if (ret != 1) - _handleSSLError(SSL_get_error(sslConn.get(), ret), ret); - - return sslConn.release(); -} - -StatusWith<boost::optional<SSLPeerInfo>> SSLManager::parseAndValidatePeerCertificate( - SSL* conn, const std::string& remoteHost) { - if (!_sslConfiguration.hasCA && isSSLServer) - return {boost::none}; - - X509* peerCert = SSL_get_peer_certificate(conn); - - if (NULL == peerCert) { // no certificate presented by peer - if (_weakValidation) { - warning() << "no SSL certificate provided by peer"; - } else { - auto msg = "no SSL certificate provided by peer; connection rejected"; - error() << msg; - return Status(ErrorCodes::SSLHandshakeFailed, msg); - } - return {boost::none}; - } - ON_BLOCK_EXIT(X509_free, peerCert); - - long result = SSL_get_verify_result(conn); - - if (result != X509_V_OK) { - if (_allowInvalidCertificates) { - warning() << "SSL peer certificate validation failed: " - << X509_verify_cert_error_string(result); - } else { - str::stream msg; - msg << "SSL peer certificate validation failed: " - << X509_verify_cert_error_string(result); - error() << msg.ss.str(); - return Status(ErrorCodes::SSLHandshakeFailed, msg); - } - } - - // TODO: check optional cipher restriction, using cert. - std::string peerSubjectName = getCertificateSubjectName(peerCert); - LOG(2) << "Accepted TLS connection from peer: " << peerSubjectName; - - StatusWith<stdx::unordered_set<RoleName>> swPeerCertificateRoles = _parsePeerRoles(peerCert); - if (!swPeerCertificateRoles.isOK()) { - return swPeerCertificateRoles.getStatus(); - } - - // If this is an SSL client context (on a MongoDB server or client) - // perform hostname validation of the remote server - if (remoteHost.empty()) { - return boost::make_optional( - SSLPeerInfo(peerSubjectName, std::move(swPeerCertificateRoles.getValue()))); - } - - // Try to match using the Subject Alternate Name, if it exists. - // RFC-2818 requires the Subject Alternate Name to be used if present. - // Otherwise, the most specific Common Name field in the subject field - // must be used. - - bool sanMatch = false; - bool cnMatch = false; - StringBuilder certificateNames; - - STACK_OF(GENERAL_NAME)* sanNames = static_cast<STACK_OF(GENERAL_NAME)*>( - X509_get_ext_d2i(peerCert, NID_subject_alt_name, NULL, NULL)); - - if (sanNames != NULL) { - int sanNamesList = sk_GENERAL_NAME_num(sanNames); - certificateNames << "SAN(s): "; - for (int i = 0; i < sanNamesList; i++) { - const GENERAL_NAME* currentName = sk_GENERAL_NAME_value(sanNames, i); - if (currentName && currentName->type == GEN_DNS) { - char* dnsName = reinterpret_cast<char*>(ASN1_STRING_data(currentName->d.dNSName)); - if (hostNameMatchForX509Certificates(remoteHost, dnsName)) { - sanMatch = true; - break; - } - certificateNames << std::string(dnsName) << " "; - } - } - sk_GENERAL_NAME_pop_free(sanNames, GENERAL_NAME_free); - } else if (peerSubjectName.find("CN=") != std::string::npos) { - // If Subject Alternate Name (SAN) doesn't exist and Common Name (CN) does, - // check Common Name. - int cnBegin = peerSubjectName.find("CN=") + 3; - int cnEnd = peerSubjectName.find(",", cnBegin); - std::string commonName = peerSubjectName.substr(cnBegin, cnEnd - cnBegin); - - if (hostNameMatchForX509Certificates(remoteHost, commonName)) { - cnMatch = true; - } - certificateNames << "CN: " << commonName; - } else { - certificateNames << "No Common Name (CN) or Subject Alternate Names (SAN) found"; - } - - if (!sanMatch && !cnMatch) { - StringBuilder msgBuilder; - msgBuilder << "The server certificate does not match the host name. Hostname: " - << remoteHost << " does not match " << certificateNames.str(); - std::string msg = msgBuilder.str(); - if (_allowInvalidCertificates || _allowInvalidHostnames || isUnixDomainSocket(remoteHost)) { - warning() << msg; - } else { - error() << msg; - return Status(ErrorCodes::SSLHandshakeFailed, msg); - } - } - - return boost::make_optional(SSLPeerInfo(peerSubjectName, stdx::unordered_set<RoleName>())); -} - - -SSLPeerInfo SSLManager::parseAndValidatePeerCertificateDeprecated(const SSLConnection* conn, - const std::string& remoteHost) { - auto swPeerSubjectName = parseAndValidatePeerCertificate(conn->ssl, remoteHost); - // We can't use uassertStatusOK here because we need to throw a NetworkException. - if (!swPeerSubjectName.isOK()) { - throwSocketError(SocketErrorKind::CONNECT_ERROR, swPeerSubjectName.getStatus().reason()); - } - return swPeerSubjectName.getValue().get_value_or(SSLPeerInfo()); -} - -StatusWith<stdx::unordered_set<RoleName>> SSLManager::_parsePeerRoles(X509* peerCert) const { - // exts is owned by the peerCert - const STACK_OF(X509_EXTENSION)* exts = X509_get0_extensions(peerCert); - - int extCount = 0; - if (exts) { - extCount = sk_X509_EXTENSION_num(exts); - } - - ASN1_OBJECT* rolesObj = OBJ_nid2obj(_rolesNid); - - // Search all certificate extensions for our own - stdx::unordered_set<RoleName> roles; - for (int i = 0; i < extCount; i++) { - X509_EXTENSION* ex = sk_X509_EXTENSION_value(exts, i); - ASN1_OBJECT* obj = X509_EXTENSION_get_object(ex); - - if (!OBJ_cmp(obj, rolesObj)) { - // We've found an extension which has our roles OID - ASN1_OCTET_STRING* data = X509_EXTENSION_get_data(ex); - - /* - * MongoDBAuthorizationGrant ::= CHOICE { - * MongoDBRole, - * ...!UTF8String:"Unrecognized entity in MongoDBAuthorizationGrant" - * } - * MongoDBAuthorizationGrants ::= SET OF MongoDBAuthorizationGrant - */ - // Extract the set of roles from our extension, and load them into an OpenSSL stack. - STACK_OF(ASN1_TYPE)* mongoDBAuthorizationGrants = nullptr; - - // OpenSSL's parsing function will try and manipulate the pointer it's passed. If we - // passed it 'data->data' directly, it would modify structures owned by peerCert. - const unsigned char* dataBytes = data->data; - mongoDBAuthorizationGrants = - d2i_ASN1_SET_ANY(&mongoDBAuthorizationGrants, &dataBytes, data->length); - if (!mongoDBAuthorizationGrants) { - return Status(ErrorCodes::FailedToParse, - "Failed to parse x509 authorization grants"); - } - const auto grantGuard = MakeGuard([&mongoDBAuthorizationGrants]() { - sk_ASN1_TYPE_pop_free(mongoDBAuthorizationGrants, ASN1_TYPE_free); - }); - - /* - * MongoDBRole ::= SEQUENCE { - * role UTF8String, - * database UTF8String - * } - */ - // Loop through every role in the stack. - ASN1_TYPE* MongoDBRoleWrapped = nullptr; - while ((MongoDBRoleWrapped = sk_ASN1_TYPE_pop(mongoDBAuthorizationGrants))) { - const auto roleWrappedGuard = - MakeGuard([MongoDBRoleWrapped]() { ASN1_TYPE_free(MongoDBRoleWrapped); }); - - if (MongoDBRoleWrapped->type == V_ASN1_SEQUENCE) { - // Unwrap the ASN1Type into a STACK_OF(ASN1_TYPE) - unsigned char* roleBytes = ASN1_STRING_data(MongoDBRoleWrapped->value.sequence); - int roleBytesLength = ASN1_STRING_length(MongoDBRoleWrapped->value.sequence); - ASN1_SEQUENCE_ANY* MongoDBRole = nullptr; - MongoDBRole = d2i_ASN1_SEQUENCE_ANY( - &MongoDBRole, (const unsigned char**)&roleBytes, roleBytesLength); - if (!MongoDBRole) { - return Status(ErrorCodes::FailedToParse, - "Failed to parse role in x509 authorization grant"); - } - const auto roleGuard = MakeGuard( - [&MongoDBRole]() { sk_ASN1_TYPE_pop_free(MongoDBRole, ASN1_TYPE_free); }); - - if (sk_ASN1_TYPE_num(MongoDBRole) != 2) { - return Status(ErrorCodes::FailedToParse, - "Role entity in MongoDBAuthorizationGrant must have exactly " - "2 sequence elements"); - } - // Extract the subcomponents of the sequence, which are popped off the stack in - // reverse order. Here, parse the role's database. - ASN1_TYPE* roleComponent = sk_ASN1_TYPE_pop(MongoDBRole); - const auto roleDBGuard = - MakeGuard([roleComponent]() { ASN1_TYPE_free(roleComponent); }); - if (roleComponent->type != V_ASN1_UTF8STRING) { - return Status(ErrorCodes::FailedToParse, - "database in MongoDBRole must be a UTF8 string"); - } - std::string roleDB( - reinterpret_cast<char*>(ASN1_STRING_data(roleComponent->value.utf8string))); - - // Parse the role's name. - roleComponent = sk_ASN1_TYPE_pop(MongoDBRole); - const auto roleNameGuard = - MakeGuard([roleComponent]() { ASN1_TYPE_free(roleComponent); }); - if (roleComponent->type != V_ASN1_UTF8STRING) { - return Status(ErrorCodes::FailedToParse, - "role in MongoDBRole must be a UTF8 string"); - } - std::string roleName( - reinterpret_cast<char*>(ASN1_STRING_data(roleComponent->value.utf8string))); - - // Construct a RoleName from the subcomponents - roles.emplace(RoleName(roleName, roleDB)); - - } else { - return Status(ErrorCodes::FailedToParse, - "Unrecognized entity in MongoDBAuthorizationGrant"); - } - } - LOG(1) << "MONGODB-X509 authorization parsed the following roles from peer " - "certificate: " - << [&roles]() { - StringBuilder sb; - std::for_each(roles.begin(), roles.end(), [&sb](const RoleName& role) { - sb << role.toString(); - }); - return sb.str(); - }(); - } - } - - return roles; -} - -std::string SSLManagerInterface::getSSLErrorMessage(int code) { - // 120 from the SSL documentation for ERR_error_string - static const size_t msglen = 120; - - char msg[msglen]; - ERR_error_string_n(code, msg, msglen); - return msg; -} - -void SSLManager::_handleSSLError(int code, int ret) { - int err = ERR_get_error(); - - switch (code) { - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - // should not happen because we turned on AUTO_RETRY - // However, it turns out this CAN happen during a connect, if the other side - // accepts the socket connection but fails to do the SSL handshake in a timely - // manner. - error() << "SSL: " << code << ", possibly timed out during connect"; - break; - - case SSL_ERROR_ZERO_RETURN: - // TODO: Check if we can avoid throwing an exception for this condition - LOG(3) << "SSL network connection closed"; - break; - case SSL_ERROR_SYSCALL: - // If ERR_get_error returned 0, the error queue is empty - // check the return value of the actual SSL operation - if (err != 0) { - error() << "SSL: " << getSSLErrorMessage(err); - } else if (ret == 0) { - error() << "Unexpected EOF encountered during SSL communication"; - } else { - error() << "The SSL BIO reported an I/O error " << errnoWithDescription(); - } - break; - case SSL_ERROR_SSL: { - error() << "SSL: " << getSSLErrorMessage(err); - break; - } - - default: - error() << "unrecognized SSL error"; - break; - } - throwSocketError(SocketErrorKind::CONNECT_ERROR, ""); -} -} // namespace mongo - -// TODO SERVER-11601 Use NFC Unicode canonicalization -bool mongo::hostNameMatchForX509Certificates(std::string nameToMatch, std::string certHostName) { - nameToMatch = removeFQDNRoot(std::move(nameToMatch)); - certHostName = removeFQDNRoot(std::move(certHostName)); - - if (certHostName.size() < 2) { - return false; - } - - // match wildcard DNS names - if (certHostName[0] == '*' && certHostName[1] == '.') { - // allow name.example.com if the cert is *.example.com, '*' does not match '.' - const char* subName = strchr(nameToMatch.c_str(), '.'); - return subName && !strcasecmp(certHostName.c_str() + 1, subName); - } else { - return !strcasecmp(nameToMatch.c_str(), certHostName.c_str()); - } -} - -#else - -namespace mongo { -namespace { -MONGO_INITIALIZER(SSLManager)(InitializerContext*) { - // we need a no-op initializer so that we can depend on SSLManager as a prerequisite in - // non-SSL builds. - return Status::OK(); -} -} // namespace } // namespace mongo - -#endif // #ifdef MONGO_CONFIG_SSL diff --git a/src/mongo/util/net/ssl_manager_none.cpp b/src/mongo/util/net/ssl_manager_none.cpp new file mode 100644 index 00000000000..3464e4a0244 --- /dev/null +++ b/src/mongo/util/net/ssl_manager_none.cpp @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/util/net/ssl_manager.h" + +#include "mongo/base/init.h" + +namespace mongo { +namespace { +MONGO_INITIALIZER(SSLManager)(InitializerContext*) { + // we need a no-op initializer so that we can depend on SSLManager as a prerequisite in + // non-SSL builds. + return Status::OK(); +} +} // namespace +} // namespace mongo diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp new file mode 100644 index 00000000000..c9e0b44ea49 --- /dev/null +++ b/src/mongo/util/net/ssl_manager_openssl.cpp @@ -0,0 +1,1524 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork + +#include "mongo/platform/basic.h" + +#include "mongo/util/net/ssl_manager.h" + +#include <boost/algorithm/string.hpp> +#include <boost/date_time/posix_time/posix_time.hpp> +#include <fstream> +#include <iostream> +#include <sstream> +#include <stack> +#include <string> +#include <vector> + +#include "mongo/base/init.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/config.h" +#include "mongo/db/server_parameters.h" +#include "mongo/platform/atomic_word.h" +#include "mongo/stdx/memory.h" +#include "mongo/transport/session.h" +#include "mongo/util/concurrency/mutex.h" +#include "mongo/util/debug_util.h" +#include "mongo/util/exit.h" +#include "mongo/util/log.h" +#include "mongo/util/mongoutils/str.h" +#include "mongo/util/net/private/ssl_expiration.h" +#include "mongo/util/net/sock.h" +#include "mongo/util/net/socket_exception.h" +#include "mongo/util/net/ssl_options.h" +#include "mongo/util/net/ssl_types.h" +#include "mongo/util/scopeguard.h" +#include "mongo/util/text.h" + +#include <openssl/asn1.h> +#include <openssl/asn1t.h> +#include <openssl/evp.h> +#include <openssl/x509_vfy.h> +#include <openssl/x509v3.h> +#if defined(_WIN32) +#include <wincrypt.h> +#elif defined(__APPLE__) +#include <Security/Security.h> +#endif + +namespace mongo { + +namespace { + +std::string removeFQDNRoot(std::string name) { + if (name.back() == '.') { + name.pop_back(); + } + return name; +}; + + +// Because the hostname having a slash is used by `mongo::SockAddr` to determine if a hostname is a +// Unix Domain Socket endpoint, this function uses the same logic. (See +// `mongo::SockAddr::Sockaddr(StringData, int, sa_family_t)`). A user explicitly specifying a Unix +// Domain Socket in the present working directory, through a code path which supplies `sa_family_t` +// as `AF_UNIX` will cause this code to lie. This will, in turn, cause the +// `SSLManager::parseAndValidatePeerCertificate` code to believe a socket is a host, which will then +// cause a connection failure if and only if that domain socket also has a certificate for SSL and +// the connection is an SSL connection. +bool isUnixDomainSocket(const std::string& hostname) { + return end(hostname) != std::find(begin(hostname), end(hostname), '/'); +} + +// If the underlying SSL supports auto-configuration of ECDH parameters, this function will select +// it, otherwise this function will do nothing. +void setECDHModeAuto(SSL_CTX* const ctx) { +#ifdef MONGO_CONFIG_HAVE_SSL_SET_ECDH_AUTO + ::SSL_CTX_set_ecdh_auto(ctx, true); +#endif + std::ignore = ctx; +} + +struct DHFreer { + void operator()(DH* const dh) noexcept { + if (dh) { + ::DH_free(dh); + } + } +}; +using UniqueDHParams = std::unique_ptr<DH, DHFreer>; + +struct BIOFree { + void operator()(BIO* const p) noexcept { + // Assumes that BIO_free succeeds. + if (p) { + ::BIO_free(p); + } + } +}; +using UniqueBIO = std::unique_ptr<BIO, BIOFree>; + +UniqueBIO makeUniqueMemBio(std::vector<std::uint8_t>& v) { + UniqueBIO rv(::BIO_new_mem_buf(v.data(), v.size())); + if (!rv) { + class ssl_bad_alloc : public std::bad_alloc { + private: + std::string message; + + public: + explicit ssl_bad_alloc(std::string m) : message(std::move(m)) {} + + const char* what() const noexcept override { + return message.c_str(); + } + }; + throw ssl_bad_alloc(str::stream() + << "Error allocating SSL BIO: " + << SSLManagerInterface::getSSLErrorMessage(ERR_get_error())); + } + return rv; +} + +// Old copies of OpenSSL will not have constants to disable protocols they don't support. +// Define them to values we can OR together safely to generically disable these protocols across +// all versions of OpenSSL. +#ifndef SSL_OP_NO_TLSv1_1 +#define SSL_OP_NO_TLSv1_1 0 +#endif +#ifndef SSL_OP_NO_TLSv1_2 +#define SSL_OP_NO_TLSv1_2 0 +#endif + +// clang-format off +#ifndef MONGO_CONFIG_HAVE_ASN1_ANY_DEFINITIONS +// Copies of OpenSSL before 1.0.0 do not have ASN1_SEQUENCE_ANY, ASN1_SET_ANY, or the helper +// functions which let us deserialize these objects. We must polyfill the definitions to interact +// with ASN1 objects so stored. +typedef STACK_OF(ASN1_TYPE) ASN1_SEQUENCE_ANY; + +ASN1_ITEM_TEMPLATE(ASN1_SEQUENCE_ANY) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SEQUENCE_OF, 0, ASN1_SEQUENCE_ANY, ASN1_ANY) +ASN1_ITEM_TEMPLATE_END(ASN1_SEQUENCE_ANY) + +ASN1_ITEM_TEMPLATE(ASN1_SET_ANY) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SET_OF, 0, ASN1_SET_ANY, ASN1_ANY) +ASN1_ITEM_TEMPLATE_END(ASN1_SET_ANY) + +IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname(ASN1_SEQUENCE_ANY, ASN1_SEQUENCE_ANY, + ASN1_SEQUENCE_ANY) +IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname(ASN1_SEQUENCE_ANY, ASN1_SET_ANY, ASN1_SET_ANY) +; // clang format needs to see a semicolon or it will start formatting unrelated code +#endif // MONGO_CONFIG_NEEDS_ASN1_ANY_DEFINITIONS +// clang-format on + +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) +// Copies of OpenSSL after 1.1.0 define new functions for interaction with +// X509 structure. We must polyfill used definitions to interact with older +// OpenSSL versions. +const STACK_OF(X509_EXTENSION) * X509_get0_extensions(const X509* peerCert) { + return peerCert->cert_info->extensions; +} +#endif + +/** + * Multithreaded Support for SSL. + * + * In order to allow OpenSSL to work in a multithreaded environment, you + * may need to provide some callbacks for it to use for locking. The following code + * sets up a vector of mutexes and provides a thread unique ID number. + * The so-called SSLThreadInfo class encapsulates most of the logic required for + * OpenSSL multithreaded support. + * + * OpenSSL before version 1.1.0 requires applications provide a callback which emits a thread + * identifier. This ID is used to store thread specific ERR information. When a thread is + * terminated, it must call ERR_remove_state or ERR_remove_thread_state. These functions may + * themselves invoke the application provided callback. These IDs are stored in a hashtable with + * a questionable hash function. They must be uniformly distributed to prevent collisions. + */ +class SSLThreadInfo { +public: + static unsigned long getID() { + struct CallErrRemoveState { + explicit CallErrRemoveState(ThreadIDManager& manager, unsigned long id) + : _manager(manager), id(id) {} + + ~CallErrRemoveState() { + ERR_remove_state(0); + _manager.releaseID(id); + }; + + ThreadIDManager& _manager; + unsigned long id; + }; + + // NOTE: This logic is fully intentional. Because ERR_remove_state (called within + // the destructor of the kRemoveStateFromThread object) re-enters this function, + // we must have a two phase protection, otherwise we would access a thread local + // during its destruction. + static thread_local boost::optional<CallErrRemoveState> threadLocalState; + if (!threadLocalState) { + threadLocalState.emplace(_idManager, _idManager.reserveID()); + } + + return threadLocalState->id; + } + + static void lockingCallback(int mode, int type, const char* file, int line) { + if (mode & CRYPTO_LOCK) { + _mutex[type]->lock(); + } else { + _mutex[type]->unlock(); + } + } + + static void init() { + CRYPTO_set_id_callback(&SSLThreadInfo::getID); + CRYPTO_set_locking_callback(&SSLThreadInfo::lockingCallback); + + while ((int)_mutex.size() < CRYPTO_num_locks()) { + _mutex.emplace_back(stdx::make_unique<stdx::recursive_mutex>()); + } + } + +private: + SSLThreadInfo() = delete; + + // Note: see SERVER-8734 for why we are using a recursive mutex here. + // Once the deadlock fix in OpenSSL is incorporated into most distros of + // Linux, this can be changed back to a nonrecursive mutex. + static std::vector<std::unique_ptr<stdx::recursive_mutex>> _mutex; + + class ThreadIDManager { + public: + unsigned long reserveID() { + stdx::unique_lock<stdx::mutex> lock(_idMutex); + if (!_idLast.empty()) { + unsigned long ret = _idLast.top(); + _idLast.pop(); + return ret; + } + return ++_idNext; + } + + void releaseID(unsigned long id) { + stdx::unique_lock<stdx::mutex> lock(_idMutex); + _idLast.push(id); + } + + private: + // Machinery for producing IDs that are unique for the life of a thread. + stdx::mutex _idMutex; // Protects _idNext and _idLast. + unsigned long _idNext = 0; // Stores the next thread ID to use, if none already allocated. + std::stack<unsigned long, std::vector<unsigned long>> + _idLast; // Stores old thread IDs, for reuse. + }; + static ThreadIDManager _idManager; +}; +std::vector<std::unique_ptr<stdx::recursive_mutex>> SSLThreadInfo::_mutex; +SSLThreadInfo::ThreadIDManager SSLThreadInfo::_idManager; + +namespace { +// We only want to free SSL_CTX objects if they have been populated. OpenSSL seems to perform this +// check before freeing them, but because it does not document this, we should protect ourselves. +void free_ssl_context(SSL_CTX* ctx) { + if (ctx != nullptr) { + SSL_CTX_free(ctx); + } +} +} // namespace + +//////////////////////////////////////////////////////////////// + +SimpleMutex sslManagerMtx; +SSLManagerInterface* theSSLManager = NULL; +using UniqueSSLContext = std::unique_ptr<SSL_CTX, decltype(&free_ssl_context)>; +static const int BUFFER_SIZE = 8 * 1024; +static const int DATE_LEN = 128; + +class SSLManager : public SSLManagerInterface { +public: + explicit SSLManager(const SSLParams& params, bool isServer); + + /** + * Initializes an OpenSSL context according to the provided settings. Only settings which are + * acceptable on non-blocking connections are set. + */ + Status initSSLContext(SSL_CTX* context, + const SSLParams& params, + ConnectionDirection direction) final; + + virtual SSLConnection* connect(Socket* socket); + + virtual SSLConnection* accept(Socket* socket, const char* initialBytes, int len); + + virtual SSLPeerInfo parseAndValidatePeerCertificateDeprecated(const SSLConnection* conn, + const std::string& remoteHost); + + StatusWith<boost::optional<SSLPeerInfo>> parseAndValidatePeerCertificate( + SSL* conn, const std::string& remoteHost) final; + + virtual const SSLConfiguration& getSSLConfiguration() const { + return _sslConfiguration; + } + + virtual int SSL_read(SSLConnection* conn, void* buf, int num); + + virtual int SSL_write(SSLConnection* conn, const void* buf, int num); + + virtual unsigned long ERR_get_error(); + + virtual char* ERR_error_string(unsigned long e, char* buf); + + virtual int SSL_get_error(const SSLConnection* conn, int ret); + + virtual int SSL_shutdown(SSLConnection* conn); + + virtual void SSL_free(SSLConnection* conn); + +private: + const int _rolesNid = OBJ_create(mongodbRolesOID.identifier.c_str(), + mongodbRolesOID.shortDescription.c_str(), + mongodbRolesOID.longDescription.c_str()); + UniqueSSLContext _serverContext; // SSL context for incoming connections + UniqueSSLContext _clientContext; // SSL context for outgoing connections + bool _weakValidation; + bool _allowInvalidCertificates; + bool _allowInvalidHostnames; + SSLConfiguration _sslConfiguration; + + /** + * creates an SSL object to be used for this file descriptor. + * caller must SSL_free it. + */ + SSL* _secure(SSL_CTX* context, int fd); + + /** + * Given an error code from an SSL-type IO function, logs an + * appropriate message and throws a NetworkException. + */ + MONGO_COMPILER_NORETURN void _handleSSLError(int code, int ret); + + /* + * Init the SSL context using parameters provided in params. This SSL context will + * be configured for blocking send/receive. + */ + bool _initSynchronousSSLContext(UniqueSSLContext* context, + const SSLParams& params, + ConnectionDirection direction); + + /* + * Converts time from OpenSSL return value to unsigned long long + * representing the milliseconds since the epoch. + */ + unsigned long long _convertASN1ToMillis(ASN1_TIME* t); + + /* + * Parse and store x509 subject name from the PEM keyfile. + * For server instances check that PEM certificate is not expired + * and extract server certificate notAfter date. + * @param keyFile referencing the PEM file to be read. + * @param subjectName as a pointer to the subject name variable being set. + * @param serverNotAfter a Date_t object pointer that is valued if the + * date is to be checked (as for a server certificate) and null otherwise. + * @return bool showing if the function was successful. + */ + bool _parseAndValidateCertificate(const std::string& keyFile, + const std::string& keyPassword, + std::string* subjectName, + Date_t* serverNotAfter); + + + StatusWith<stdx::unordered_set<RoleName>> _parsePeerRoles(X509* peerCert) const; + + /** @return true if was successful, otherwise false */ + bool _setupPEM(SSL_CTX* context, const std::string& keyFile, const std::string& password); + + /* + * Set up an SSL context for certificate validation by loading a CA + */ + Status _setupCA(SSL_CTX* context, const std::string& caFile); + + /* + * Set up an SSL context for certificate validation by loading the system's CA store + */ + Status _setupSystemCA(SSL_CTX* context); + + /* + * Import a certificate revocation list into an SSL context + * for use with validating certificates + */ + bool _setupCRL(SSL_CTX* context, const std::string& crlFile); + + /* + * sub function for checking the result of an SSL operation + */ + bool _doneWithSSLOp(SSLConnection* conn, int status); + + /* + * Send and receive network data + */ + void _flushNetworkBIO(SSLConnection* conn); + + /** + * Callbacks for SSL functions. + */ + static int password_cb(char* buf, int num, int rwflag, void* userdata); + static int verify_cb(int ok, X509_STORE_CTX* ctx); +}; + +void setupFIPS() { +// Turn on FIPS mode if requested, OPENSSL_FIPS must be defined by the OpenSSL headers +#if defined(MONGO_CONFIG_HAVE_FIPS_MODE_SET) + int status = FIPS_mode_set(1); + if (!status) { + severe() << "can't activate FIPS mode: " + << SSLManagerInterface::getSSLErrorMessage(ERR_get_error()); + fassertFailedNoTrace(16703); + } + log() << "FIPS 140-2 mode activated"; +#else + severe() << "this version of mongodb was not compiled with FIPS support"; + fassertFailedNoTrace(17089); +#endif +} + +} // namespace + +// Global variable indicating if this is a server or a client instance +bool isSSLServer = false; + + +MONGO_INITIALIZER(SetupOpenSSL)(InitializerContext*) { + SSL_library_init(); + SSL_load_error_strings(); + ERR_load_crypto_strings(); + + if (sslGlobalParams.sslFIPSMode) { + setupFIPS(); + } + + // Add all digests and ciphers to OpenSSL's internal table + // so that encryption/decryption is backwards compatible + OpenSSL_add_all_algorithms(); + + // Setup OpenSSL multithreading callbacks and mutexes + SSLThreadInfo::init(); + + return Status::OK(); +} + +MONGO_INITIALIZER_WITH_PREREQUISITES(SSLManager, ("SetupOpenSSL"))(InitializerContext*) { + stdx::lock_guard<SimpleMutex> lck(sslManagerMtx); + if (!isSSLServer || (sslGlobalParams.sslMode.load() != SSLParams::SSLMode_disabled)) { + theSSLManager = new SSLManager(sslGlobalParams, isSSLServer); + } + return Status::OK(); +} + +std::unique_ptr<SSLManagerInterface> SSLManagerInterface::create(const SSLParams& params, + bool isServer) { + return stdx::make_unique<SSLManager>(params, isServer); +} + +SSLManagerInterface* getSSLManager() { + stdx::lock_guard<SimpleMutex> lck(sslManagerMtx); + if (theSSLManager) + return theSSLManager; + return NULL; +} + +std::string getCertificateSubjectName(X509* cert) { + std::string result; + + BIO* out = BIO_new(BIO_s_mem()); + uassert(16884, "unable to allocate BIO memory", NULL != out); + ON_BLOCK_EXIT(BIO_free, out); + + if (X509_NAME_print_ex(out, X509_get_subject_name(cert), 0, XN_FLAG_RFC2253) >= 0) { + if (BIO_number_written(out) > 0) { + result.resize(BIO_number_written(out)); + BIO_read(out, &result[0], result.size()); + } + } else { + log() << "failed to convert subject name to RFC2253 format"; + } + + return result; +} + +SSLConnection::SSLConnection(SSL_CTX* context, Socket* sock, const char* initialBytes, int len) + : socket(sock) { + ssl = SSL_new(context); + + std::string sslErr = + NULL != getSSLManager() ? getSSLManager()->getSSLErrorMessage(ERR_get_error()) : ""; + massert(15861, "Error creating new SSL object " + sslErr, ssl); + + BIO_new_bio_pair(&internalBIO, BUFFER_SIZE, &networkBIO, BUFFER_SIZE); + SSL_set_bio(ssl, internalBIO, internalBIO); + + if (len > 0) { + int toBIO = BIO_write(networkBIO, initialBytes, len); + if (toBIO != len) { + LOG(3) << "Failed to write initial network data to the SSL BIO layer"; + throwSocketError(SocketErrorKind::RECV_ERROR, socket->remoteString()); + } + } +} + +SSLConnection::~SSLConnection() { + if (ssl) { // The internalBIO is automatically freed as part of SSL_free + SSL_free(ssl); + } + if (networkBIO) { + BIO_free(networkBIO); + } +} + +SSLManager::SSLManager(const SSLParams& params, bool isServer) + : _serverContext(nullptr, free_ssl_context), + _clientContext(nullptr, free_ssl_context), + _weakValidation(params.sslWeakCertificateValidation), + _allowInvalidCertificates(params.sslAllowInvalidCertificates), + _allowInvalidHostnames(params.sslAllowInvalidHostnames) { + if (!_initSynchronousSSLContext(&_clientContext, params, ConnectionDirection::kOutgoing)) { + uasserted(16768, "ssl initialization problem"); + } + + // pick the certificate for use in outgoing connections, + std::string clientPEM, clientPassword; + if (!isServer || params.sslClusterFile.empty()) { + // We are either a client, or a server without a cluster key, + // so use the PEM key file, if specified + clientPEM = params.sslPEMKeyFile; + clientPassword = params.sslPEMKeyPassword; + } else { + // We are a server with a cluster key, so use the cluster key file + clientPEM = params.sslClusterFile; + clientPassword = params.sslClusterPassword; + } + + if (!clientPEM.empty()) { + if (!_parseAndValidateCertificate( + clientPEM, clientPassword, &_sslConfiguration.clientSubjectName, NULL)) { + uasserted(16941, "ssl initialization problem"); + } + } + // SSL server specific initialization + if (isServer) { + if (!_initSynchronousSSLContext(&_serverContext, params, ConnectionDirection::kIncoming)) { + uasserted(16562, "ssl initialization problem"); + } + + if (!_parseAndValidateCertificate(params.sslPEMKeyFile, + params.sslPEMKeyPassword, + &_sslConfiguration.serverSubjectName, + &_sslConfiguration.serverCertificateExpirationDate)) { + uasserted(16942, "ssl initialization problem"); + } + + static CertificateExpirationMonitor task = + CertificateExpirationMonitor(_sslConfiguration.serverCertificateExpirationDate); + } +} + +int SSLManager::password_cb(char* buf, int num, int rwflag, void* userdata) { + // Unless OpenSSL misbehaves, num should always be positive + fassert(17314, num > 0); + invariant(userdata); + auto pw = static_cast<const std::string*>(userdata); + + const size_t copied = pw->copy(buf, num - 1); + buf[copied] = '\0'; + return copied; +} + +int SSLManager::verify_cb(int ok, X509_STORE_CTX* ctx) { + return 1; // always succeed; we will catch the error in our get_verify_result() call +} + +int SSLManager::SSL_read(SSLConnection* conn, void* buf, int num) { + int status; + do { + status = ::SSL_read(conn->ssl, buf, num); + } while (!_doneWithSSLOp(conn, status)); + + if (status <= 0) + _handleSSLError(SSL_get_error(conn, status), status); + return status; +} + +int SSLManager::SSL_write(SSLConnection* conn, const void* buf, int num) { + int status; + do { + status = ::SSL_write(conn->ssl, buf, num); + } while (!_doneWithSSLOp(conn, status)); + + if (status <= 0) + _handleSSLError(SSL_get_error(conn, status), status); + return status; +} + +unsigned long SSLManager::ERR_get_error() { + return ::ERR_get_error(); +} + +char* SSLManager::ERR_error_string(unsigned long e, char* buf) { + return ::ERR_error_string(e, buf); +} + +int SSLManager::SSL_get_error(const SSLConnection* conn, int ret) { + return ::SSL_get_error(conn->ssl, ret); +} + +int SSLManager::SSL_shutdown(SSLConnection* conn) { + int status; + do { + status = ::SSL_shutdown(conn->ssl); + } while (!_doneWithSSLOp(conn, status)); + + if (status < 0) + _handleSSLError(SSL_get_error(conn, status), status); + return status; +} + +void SSLManager::SSL_free(SSLConnection* conn) { + return ::SSL_free(conn->ssl); +} + +Status SSLManager::initSSLContext(SSL_CTX* context, + const SSLParams& params, + ConnectionDirection direction) { + // SSL_OP_ALL - Activate all bug workaround options, to support buggy client SSL's. + // SSL_OP_NO_SSLv2 - Disable SSL v2 support + // SSL_OP_NO_SSLv3 - Disable SSL v3 support + long supportedProtocols = SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3; + + // Set the supported TLS protocols. Allow --sslDisabledProtocols to disable selected + // ciphers. + for (const SSLParams::Protocols& protocol : params.sslDisabledProtocols) { + if (protocol == SSLParams::Protocols::TLS1_0) { + supportedProtocols |= SSL_OP_NO_TLSv1; + } else if (protocol == SSLParams::Protocols::TLS1_1) { + supportedProtocols |= SSL_OP_NO_TLSv1_1; + } else if (protocol == SSLParams::Protocols::TLS1_2) { + supportedProtocols |= SSL_OP_NO_TLSv1_2; + } + } + ::SSL_CTX_set_options(context, supportedProtocols); + + // HIGH - Enable strong ciphers + // !EXPORT - Disable export ciphers (40/56 bit) + // !aNULL - Disable anonymous auth ciphers + // @STRENGTH - Sort ciphers based on strength + std::string cipherConfig = "HIGH:!EXPORT:!aNULL@STRENGTH"; + + // Allow the cipher configuration string to be overriden by --sslCipherConfig + if (!params.sslCipherConfig.empty()) { + cipherConfig = params.sslCipherConfig; + } + + if (0 == ::SSL_CTX_set_cipher_list(context, cipherConfig.c_str())) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Can not set supported cipher suites: " + << getSSLErrorMessage(ERR_get_error())); + } + + // We use the address of the context as the session id context. + if (0 == ::SSL_CTX_set_session_id_context( + context, reinterpret_cast<unsigned char*>(&context), sizeof(context))) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Can not store ssl session id context: " + << getSSLErrorMessage(ERR_get_error())); + } + + if (direction == ConnectionDirection::kOutgoing && !params.sslClusterFile.empty()) { + ::EVP_set_pw_prompt("Enter cluster certificate passphrase"); + if (!_setupPEM(context, params.sslClusterFile, params.sslClusterPassword)) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Can not set up ssl clusterFile."); + } + } else if (!params.sslPEMKeyFile.empty()) { + // Use the pemfile for everything else + ::EVP_set_pw_prompt("Enter PEM passphrase"); + if (!_setupPEM(context, params.sslPEMKeyFile, params.sslPEMKeyPassword)) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Can not set up PEM key file."); + } + } + + const auto status = + params.sslCAFile.empty() ? _setupSystemCA(context) : _setupCA(context, params.sslCAFile); + if (!status.isOK()) + return status; + + if (!params.sslCRLFile.empty()) { + if (!_setupCRL(context, params.sslCRLFile)) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Can not set up CRL file."); + } + } + + if (!params.sslPEMTempDHParam.empty()) { + try { + std::ifstream dhparamPemFile(params.sslPEMTempDHParam, std::ios_base::binary); + if (dhparamPemFile.fail() || dhparamPemFile.bad()) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Cannot open PEM DHParams file."); + } + + std::vector<std::uint8_t> paramData{std::istreambuf_iterator<char>(dhparamPemFile), + std::istreambuf_iterator<char>()}; + auto bio = makeUniqueMemBio(paramData); + + UniqueDHParams dhparams(::PEM_read_bio_DHparams(bio.get(), nullptr, nullptr, nullptr)); + if (!dhparams) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Error reading DHParams file." + << getSSLErrorMessage(ERR_get_error())); + } + + if (::SSL_CTX_set_tmp_dh(context, dhparams.get()) != 1) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Failure to set PFS DH parameters: " + << getSSLErrorMessage(ERR_get_error())); + } + } catch (const std::exception& ex) { + return Status(ErrorCodes::InvalidSSLConfiguration, ex.what()); + } + } + + // We always set ECDH mode anyhow, if available. + setECDHModeAuto(context); + + return Status::OK(); +} + +bool SSLManager::_initSynchronousSSLContext(UniqueSSLContext* contextPtr, + const SSLParams& params, + ConnectionDirection direction) { + *contextPtr = UniqueSSLContext(SSL_CTX_new(SSLv23_method()), free_ssl_context); + + uassertStatusOK(initSSLContext(contextPtr->get(), params, direction)); + + // If renegotiation is needed, don't return from recv() or send() until it's successful. + // Note: this is for blocking sockets only. + SSL_CTX_set_mode(contextPtr->get(), SSL_MODE_AUTO_RETRY); + + return true; +} + +unsigned long long SSLManager::_convertASN1ToMillis(ASN1_TIME* asn1time) { + BIO* outBIO = BIO_new(BIO_s_mem()); + int timeError = ASN1_TIME_print(outBIO, asn1time); + ON_BLOCK_EXIT(BIO_free, outBIO); + + if (timeError <= 0) { + error() << "ASN1_TIME_print failed or wrote no data."; + return 0; + } + + char dateChar[DATE_LEN]; + timeError = BIO_gets(outBIO, dateChar, DATE_LEN); + if (timeError <= 0) { + error() << "BIO_gets call failed to transfer contents to buf"; + return 0; + } + + // Ensure that day format is two digits for parsing. + // Jun 8 17:00:03 2014 becomes Jun 08 17:00:03 2014. + if (dateChar[4] == ' ') { + dateChar[4] = '0'; + } + + std::istringstream inStringStream((std::string(dateChar, 20))); + boost::posix_time::time_input_facet* inputFacet = + new boost::posix_time::time_input_facet("%b %d %H:%M:%S %Y"); + + inStringStream.imbue(std::locale(std::cout.getloc(), inputFacet)); + boost::posix_time::ptime posixTime; + inStringStream >> posixTime; + + const boost::gregorian::date epoch = boost::gregorian::date(1970, boost::gregorian::Jan, 1); + + return (posixTime - boost::posix_time::ptime(epoch)).total_milliseconds(); +} + +bool SSLManager::_parseAndValidateCertificate(const std::string& keyFile, + const std::string& keyPassword, + std::string* subjectName, + Date_t* serverCertificateExpirationDate) { + BIO* inBIO = BIO_new(BIO_s_file()); + if (inBIO == NULL) { + error() << "failed to allocate BIO object: " << getSSLErrorMessage(ERR_get_error()); + return false; + } + + ON_BLOCK_EXIT(BIO_free, inBIO); + if (BIO_read_filename(inBIO, keyFile.c_str()) <= 0) { + error() << "cannot read key file when setting subject name: " << keyFile << ' ' + << getSSLErrorMessage(ERR_get_error()); + return false; + } + + // Callback will not manipulate the password, so const_cast is safe. + X509* x509 = PEM_read_bio_X509(inBIO, + NULL, + &SSLManager::password_cb, + const_cast<void*>(static_cast<const void*>(&keyPassword))); + if (x509 == NULL) { + error() << "cannot retrieve certificate from keyfile: " << keyFile << ' ' + << getSSLErrorMessage(ERR_get_error()); + return false; + } + ON_BLOCK_EXIT(X509_free, x509); + + *subjectName = getCertificateSubjectName(x509); + if (serverCertificateExpirationDate != NULL) { + unsigned long long notBeforeMillis = _convertASN1ToMillis(X509_get_notBefore(x509)); + if (notBeforeMillis == 0) { + error() << "date conversion failed"; + return false; + } + + unsigned long long notAfterMillis = _convertASN1ToMillis(X509_get_notAfter(x509)); + if (notAfterMillis == 0) { + error() << "date conversion failed"; + return false; + } + + if ((notBeforeMillis > curTimeMillis64()) || (curTimeMillis64() > notAfterMillis)) { + severe() << "The provided SSL certificate is expired or not yet valid."; + fassertFailedNoTrace(28652); + } + + *serverCertificateExpirationDate = Date_t::fromMillisSinceEpoch(notAfterMillis); + } + + return true; +} + +bool SSLManager::_setupPEM(SSL_CTX* context, + const std::string& keyFile, + const std::string& password) { + if (SSL_CTX_use_certificate_chain_file(context, keyFile.c_str()) != 1) { + error() << "cannot read certificate file: " << keyFile << ' ' + << getSSLErrorMessage(ERR_get_error()); + return false; + } + + BIO* inBio = BIO_new(BIO_s_file()); + if (!inBio) { + error() << "failed to allocate BIO object: " << getSSLErrorMessage(ERR_get_error()); + return false; + } + const auto bioGuard = MakeGuard([&inBio]() { BIO_free(inBio); }); + + if (BIO_read_filename(inBio, keyFile.c_str()) <= 0) { + error() << "cannot read PEM key file: " << keyFile << ' ' + << getSSLErrorMessage(ERR_get_error()); + return false; + } + + // If password is empty, use default OpenSSL callback, which uses the terminal + // to securely request the password interactively from the user. + decltype(&SSLManager::password_cb) password_cb = nullptr; + void* userdata = nullptr; + if (!password.empty()) { + password_cb = &SSLManager::password_cb; + // SSLManager::password_cb will not manipulate the password, so const_cast is safe. + userdata = const_cast<void*>(static_cast<const void*>(&password)); + } + EVP_PKEY* privateKey = PEM_read_bio_PrivateKey(inBio, nullptr, password_cb, userdata); + if (!privateKey) { + error() << "cannot read PEM key file: " << keyFile << ' ' + << getSSLErrorMessage(ERR_get_error()); + return false; + } + const auto privateKeyGuard = MakeGuard([&privateKey]() { EVP_PKEY_free(privateKey); }); + + if (SSL_CTX_use_PrivateKey(context, privateKey) != 1) { + error() << "cannot use PEM key file: " << keyFile << ' ' + << getSSLErrorMessage(ERR_get_error()); + return false; + } + + // Verify that the certificate and the key go together. + if (SSL_CTX_check_private_key(context) != 1) { + error() << "SSL certificate validation: " << getSSLErrorMessage(ERR_get_error()); + return false; + } + + return true; +} + +Status SSLManager::_setupCA(SSL_CTX* context, const std::string& caFile) { + // Set the list of CAs sent to clients + STACK_OF(X509_NAME)* certNames = SSL_load_client_CA_file(caFile.c_str()); + if (certNames == NULL) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "cannot read certificate authority file: " << caFile << " " + << getSSLErrorMessage(ERR_get_error())); + } + SSL_CTX_set_client_CA_list(context, certNames); + + // Load trusted CA + if (SSL_CTX_load_verify_locations(context, caFile.c_str(), NULL) != 1) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "cannot read certificate authority file: " << caFile << " " + << getSSLErrorMessage(ERR_get_error())); + } + + // Set SSL to require peer (client) certificate verification + // if a certificate is presented + SSL_CTX_set_verify(context, SSL_VERIFY_PEER, &SSLManager::verify_cb); + _sslConfiguration.hasCA = true; + return Status::OK(); +} + +inline Status checkX509_STORE_error() { + const auto errCode = ERR_peek_last_error(); + if (ERR_GET_LIB(errCode) != ERR_LIB_X509 || + ERR_GET_REASON(errCode) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { + return {ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Error adding certificate to X509 store: " + << ERR_reason_error_string(errCode)}; + } + return Status::OK(); +} + +#if defined(_WIN32) +// This imports the certificates in a given Windows certificate store into an X509_STORE for +// openssl to use during certificate validation. +Status importCertStoreToX509_STORE(const wchar_t* storeName, + DWORD storeLocation, + X509_STORE* verifyStore) { + HCERTSTORE systemStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, + 0, + NULL, + storeLocation | CERT_STORE_READONLY_FLAG, + const_cast<LPWSTR>(storeName)); + if (systemStore == NULL) { + return {ErrorCodes::InvalidSSLConfiguration, + str::stream() << "error opening system CA store: " << errnoWithDescription()}; + } + auto systemStoreGuard = MakeGuard([systemStore]() { CertCloseStore(systemStore, 0); }); + + PCCERT_CONTEXT certCtx = NULL; + while ((certCtx = CertEnumCertificatesInStore(systemStore, certCtx)) != NULL) { + auto certBytes = static_cast<const unsigned char*>(certCtx->pbCertEncoded); + X509* x509Obj = d2i_X509(NULL, &certBytes, certCtx->cbCertEncoded); + if (x509Obj == NULL) { + return {ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Error parsing X509 object from Windows certificate store" + << SSLManagerInterface::getSSLErrorMessage(ERR_get_error())}; + } + const auto x509ObjGuard = MakeGuard([&x509Obj]() { X509_free(x509Obj); }); + + if (X509_STORE_add_cert(verifyStore, x509Obj) != 1) { + auto status = checkX509_STORE_error(); + if (!status.isOK()) + return status; + } + } + int lastError = GetLastError(); + if (lastError != CRYPT_E_NOT_FOUND) { + return {ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Error enumerating certificates: " + << errnoWithDescription(lastError)}; + } + + return Status::OK(); +} +#elif defined(__APPLE__) + +template <typename T> +class CFTypeRefHolder { +public: + explicit CFTypeRefHolder(T ptr) : ref(static_cast<CFTypeRef>(ptr)) {} + ~CFTypeRefHolder() { + CFRelease(ref); + } + operator T() { + return static_cast<T>(ref); + } + +private: + CFTypeRef ref = nullptr; +}; +template <typename T> +CFTypeRefHolder<T> makeCFTypeRefHolder(T ptr) { + return CFTypeRefHolder<T>(ptr); +} + +std::string OSStatusToString(OSStatus status) { + auto errMsg = makeCFTypeRefHolder(SecCopyErrorMessageString(status, NULL)); + return std::string{CFStringGetCStringPtr(errMsg, kCFStringEncodingUTF8)}; +} + +Status importKeychainToX509_STORE(X509_STORE* verifyStore) { + CFArrayRef result; + OSStatus status; + + // This copies all the certificates trusted by the system (regardless of what keychain they're + // attached to) into a CFArray. + if ((status = SecTrustCopyAnchorCertificates(&result)) != 0) { + return {ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Error enumerating certificates: " << OSStatusToString(status)}; + } + const auto resultGuard = makeCFTypeRefHolder(result); + + for (CFIndex i = 0; i < CFArrayGetCount(result); i++) { + SecCertificateRef cert = + static_cast<SecCertificateRef>(const_cast<void*>(CFArrayGetValueAtIndex(result, i))); + + auto rawData = makeCFTypeRefHolder(SecCertificateCopyData(cert)); + if (!rawData) { + return {ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Error enumerating certificates: " + << OSStatusToString(status)}; + } + const uint8_t* rawDataPtr = CFDataGetBytePtr(rawData); + + // Parse an openssl X509 object from each returned certificate + X509* x509Cert = d2i_X509(nullptr, &rawDataPtr, CFDataGetLength(rawData)); + if (!x509Cert) { + return {ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Error parsing X509 certificate from system keychain: " + << ERR_reason_error_string(ERR_peek_last_error())}; + } + const auto x509CertGuard = MakeGuard([&x509Cert]() { X509_free(x509Cert); }); + + // Add the parsed X509 object to the X509_STORE verification store + if (X509_STORE_add_cert(verifyStore, x509Cert) != 1) { + auto status = checkX509_STORE_error(); + if (!status.isOK()) + return status; + } + } + + return Status::OK(); +} +#endif + +Status SSLManager::_setupSystemCA(SSL_CTX* context) { +#if !defined(_WIN32) && !defined(__APPLE__) + // On non-Windows/non-Apple platforms, the OpenSSL libraries should have been configured + // with default locations for CA certificates. + if (SSL_CTX_set_default_verify_paths(context) != 1) { + return {ErrorCodes::InvalidSSLConfiguration, + str::stream() << "error loading system CA certificates " + << "(default certificate file: " + << X509_get_default_cert_file() + << ", " + << "default certificate path: " + << X509_get_default_cert_dir() + << ")"}; + } + return Status::OK(); +#else + + X509_STORE* verifyStore = SSL_CTX_get_cert_store(context); + if (!verifyStore) { + return {ErrorCodes::InvalidSSLConfiguration, + "no X509 store found for SSL context while loading system certificates"}; + } +#if defined(_WIN32) + auto status = importCertStoreToX509_STORE(L"root", CERT_SYSTEM_STORE_CURRENT_USER, verifyStore); + if (!status.isOK()) + return status; + return importCertStoreToX509_STORE(L"CA", CERT_SYSTEM_STORE_CURRENT_USER, verifyStore); +#elif defined(__APPLE__) + return importKeychainToX509_STORE(verifyStore); +#endif +#endif +} + +bool SSLManager::_setupCRL(SSL_CTX* context, const std::string& crlFile) { + X509_STORE* store = SSL_CTX_get_cert_store(context); + fassert(16583, store); + + X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK); + X509_LOOKUP* lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file()); + fassert(16584, lookup); + + int status = X509_load_crl_file(lookup, crlFile.c_str(), X509_FILETYPE_PEM); + if (status == 0) { + error() << "cannot read CRL file: " << crlFile << ' ' + << getSSLErrorMessage(ERR_get_error()); + return false; + } + log() << "ssl imported " << status << " revoked certificate" << ((status == 1) ? "" : "s") + << " from the revocation list."; + return true; +} + +/* +* The interface layer between network and BIO-pair. The BIO-pair buffers +* the data to/from the TLS layer. +*/ +void SSLManager::_flushNetworkBIO(SSLConnection* conn) { + char buffer[BUFFER_SIZE]; + int wantWrite; + + /* + * Write the complete contents of the buffer. Leaving the buffer + * unflushed could cause a deadlock. + */ + while ((wantWrite = BIO_ctrl_pending(conn->networkBIO)) > 0) { + if (wantWrite > BUFFER_SIZE) { + wantWrite = BUFFER_SIZE; + } + int fromBIO = BIO_read(conn->networkBIO, buffer, wantWrite); + + int writePos = 0; + do { + int numWrite = fromBIO - writePos; + numWrite = send(conn->socket->rawFD(), buffer + writePos, numWrite, portSendFlags); + if (numWrite < 0) { + conn->socket->handleSendError(numWrite, ""); + } + writePos += numWrite; + } while (writePos < fromBIO); + } + + int wantRead; + while ((wantRead = BIO_ctrl_get_read_request(conn->networkBIO)) > 0) { + if (wantRead > BUFFER_SIZE) { + wantRead = BUFFER_SIZE; + } + + int numRead = recv(conn->socket->rawFD(), buffer, wantRead, portRecvFlags); + if (numRead <= 0) { + conn->socket->handleRecvError(numRead, wantRead); + continue; + } + + int toBIO = BIO_write(conn->networkBIO, buffer, numRead); + if (toBIO != numRead) { + LOG(3) << "Failed to write network data to the SSL BIO layer"; + throwSocketError(SocketErrorKind::RECV_ERROR, conn->socket->remoteString()); + } + } +} + +bool SSLManager::_doneWithSSLOp(SSLConnection* conn, int status) { + int sslErr = SSL_get_error(conn, status); + switch (sslErr) { + case SSL_ERROR_NONE: + _flushNetworkBIO(conn); // success, flush network BIO before leaving + return true; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + _flushNetworkBIO(conn); // not ready, flush network BIO and try again + return false; + default: + return true; + } +} + +SSLConnection* SSLManager::connect(Socket* socket) { + std::unique_ptr<SSLConnection> sslConn = + stdx::make_unique<SSLConnection>(_clientContext.get(), socket, (const char*)NULL, 0); + + const auto undotted = removeFQDNRoot(socket->remoteAddr().hostOrIp()); + int ret = ::SSL_set_tlsext_host_name(sslConn->ssl, undotted.c_str()); + if (ret != 1) + _handleSSLError(SSL_get_error(sslConn.get(), ret), ret); + + do { + ret = ::SSL_connect(sslConn->ssl); + } while (!_doneWithSSLOp(sslConn.get(), ret)); + + if (ret != 1) + _handleSSLError(SSL_get_error(sslConn.get(), ret), ret); + + return sslConn.release(); +} + +SSLConnection* SSLManager::accept(Socket* socket, const char* initialBytes, int len) { + std::unique_ptr<SSLConnection> sslConn = + stdx::make_unique<SSLConnection>(_serverContext.get(), socket, initialBytes, len); + + int ret; + do { + ret = ::SSL_accept(sslConn->ssl); + } while (!_doneWithSSLOp(sslConn.get(), ret)); + + if (ret != 1) + _handleSSLError(SSL_get_error(sslConn.get(), ret), ret); + + return sslConn.release(); +} + +StatusWith<boost::optional<SSLPeerInfo>> SSLManager::parseAndValidatePeerCertificate( + SSL* conn, const std::string& remoteHost) { + if (!_sslConfiguration.hasCA && isSSLServer) + return {boost::none}; + + X509* peerCert = SSL_get_peer_certificate(conn); + + if (NULL == peerCert) { // no certificate presented by peer + if (_weakValidation) { + warning() << "no SSL certificate provided by peer"; + } else { + auto msg = "no SSL certificate provided by peer; connection rejected"; + error() << msg; + return Status(ErrorCodes::SSLHandshakeFailed, msg); + } + return {boost::none}; + } + ON_BLOCK_EXIT(X509_free, peerCert); + + long result = SSL_get_verify_result(conn); + + if (result != X509_V_OK) { + if (_allowInvalidCertificates) { + warning() << "SSL peer certificate validation failed: " + << X509_verify_cert_error_string(result); + } else { + str::stream msg; + msg << "SSL peer certificate validation failed: " + << X509_verify_cert_error_string(result); + error() << msg.ss.str(); + return Status(ErrorCodes::SSLHandshakeFailed, msg); + } + } + + // TODO: check optional cipher restriction, using cert. + std::string peerSubjectName = getCertificateSubjectName(peerCert); + LOG(2) << "Accepted TLS connection from peer: " << peerSubjectName; + + StatusWith<stdx::unordered_set<RoleName>> swPeerCertificateRoles = _parsePeerRoles(peerCert); + if (!swPeerCertificateRoles.isOK()) { + return swPeerCertificateRoles.getStatus(); + } + + // If this is an SSL client context (on a MongoDB server or client) + // perform hostname validation of the remote server + if (remoteHost.empty()) { + return boost::make_optional( + SSLPeerInfo(peerSubjectName, std::move(swPeerCertificateRoles.getValue()))); + } + + // Try to match using the Subject Alternate Name, if it exists. + // RFC-2818 requires the Subject Alternate Name to be used if present. + // Otherwise, the most specific Common Name field in the subject field + // must be used. + + bool sanMatch = false; + bool cnMatch = false; + StringBuilder certificateNames; + + STACK_OF(GENERAL_NAME)* sanNames = static_cast<STACK_OF(GENERAL_NAME)*>( + X509_get_ext_d2i(peerCert, NID_subject_alt_name, NULL, NULL)); + + if (sanNames != NULL) { + int sanNamesList = sk_GENERAL_NAME_num(sanNames); + certificateNames << "SAN(s): "; + for (int i = 0; i < sanNamesList; i++) { + const GENERAL_NAME* currentName = sk_GENERAL_NAME_value(sanNames, i); + if (currentName && currentName->type == GEN_DNS) { + char* dnsName = reinterpret_cast<char*>(ASN1_STRING_data(currentName->d.dNSName)); + if (hostNameMatchForX509Certificates(remoteHost, dnsName)) { + sanMatch = true; + break; + } + certificateNames << std::string(dnsName) << " "; + } + } + sk_GENERAL_NAME_pop_free(sanNames, GENERAL_NAME_free); + } else if (peerSubjectName.find("CN=") != std::string::npos) { + // If Subject Alternate Name (SAN) doesn't exist and Common Name (CN) does, + // check Common Name. + int cnBegin = peerSubjectName.find("CN=") + 3; + int cnEnd = peerSubjectName.find(",", cnBegin); + std::string commonName = peerSubjectName.substr(cnBegin, cnEnd - cnBegin); + + if (hostNameMatchForX509Certificates(remoteHost, commonName)) { + cnMatch = true; + } + certificateNames << "CN: " << commonName; + } else { + certificateNames << "No Common Name (CN) or Subject Alternate Names (SAN) found"; + } + + if (!sanMatch && !cnMatch) { + StringBuilder msgBuilder; + msgBuilder << "The server certificate does not match the host name. Hostname: " + << remoteHost << " does not match " << certificateNames.str(); + std::string msg = msgBuilder.str(); + if (_allowInvalidCertificates || _allowInvalidHostnames || isUnixDomainSocket(remoteHost)) { + warning() << msg; + } else { + error() << msg; + return Status(ErrorCodes::SSLHandshakeFailed, msg); + } + } + + return boost::make_optional(SSLPeerInfo(peerSubjectName, stdx::unordered_set<RoleName>())); +} + + +SSLPeerInfo SSLManager::parseAndValidatePeerCertificateDeprecated(const SSLConnection* conn, + const std::string& remoteHost) { + auto swPeerSubjectName = parseAndValidatePeerCertificate(conn->ssl, remoteHost); + // We can't use uassertStatusOK here because we need to throw a NetworkException. + if (!swPeerSubjectName.isOK()) { + throwSocketError(SocketErrorKind::CONNECT_ERROR, swPeerSubjectName.getStatus().reason()); + } + return swPeerSubjectName.getValue().get_value_or(SSLPeerInfo()); +} + +StatusWith<stdx::unordered_set<RoleName>> SSLManager::_parsePeerRoles(X509* peerCert) const { + // exts is owned by the peerCert + const STACK_OF(X509_EXTENSION)* exts = X509_get0_extensions(peerCert); + + int extCount = 0; + if (exts) { + extCount = sk_X509_EXTENSION_num(exts); + } + + ASN1_OBJECT* rolesObj = OBJ_nid2obj(_rolesNid); + + // Search all certificate extensions for our own + stdx::unordered_set<RoleName> roles; + for (int i = 0; i < extCount; i++) { + X509_EXTENSION* ex = sk_X509_EXTENSION_value(exts, i); + ASN1_OBJECT* obj = X509_EXTENSION_get_object(ex); + + if (!OBJ_cmp(obj, rolesObj)) { + // We've found an extension which has our roles OID + ASN1_OCTET_STRING* data = X509_EXTENSION_get_data(ex); + + /* + * MongoDBAuthorizationGrant ::= CHOICE { + * MongoDBRole, + * ...!UTF8String:"Unrecognized entity in MongoDBAuthorizationGrant" + * } + * MongoDBAuthorizationGrants ::= SET OF MongoDBAuthorizationGrant + */ + // Extract the set of roles from our extension, and load them into an OpenSSL stack. + STACK_OF(ASN1_TYPE)* mongoDBAuthorizationGrants = nullptr; + + // OpenSSL's parsing function will try and manipulate the pointer it's passed. If we + // passed it 'data->data' directly, it would modify structures owned by peerCert. + const unsigned char* dataBytes = data->data; + mongoDBAuthorizationGrants = + d2i_ASN1_SET_ANY(&mongoDBAuthorizationGrants, &dataBytes, data->length); + if (!mongoDBAuthorizationGrants) { + return Status(ErrorCodes::FailedToParse, + "Failed to parse x509 authorization grants"); + } + const auto grantGuard = MakeGuard([&mongoDBAuthorizationGrants]() { + sk_ASN1_TYPE_pop_free(mongoDBAuthorizationGrants, ASN1_TYPE_free); + }); + + /* + * MongoDBRole ::= SEQUENCE { + * role UTF8String, + * database UTF8String + * } + */ + // Loop through every role in the stack. + ASN1_TYPE* MongoDBRoleWrapped = nullptr; + while ((MongoDBRoleWrapped = sk_ASN1_TYPE_pop(mongoDBAuthorizationGrants))) { + const auto roleWrappedGuard = + MakeGuard([MongoDBRoleWrapped]() { ASN1_TYPE_free(MongoDBRoleWrapped); }); + + if (MongoDBRoleWrapped->type == V_ASN1_SEQUENCE) { + // Unwrap the ASN1Type into a STACK_OF(ASN1_TYPE) + unsigned char* roleBytes = ASN1_STRING_data(MongoDBRoleWrapped->value.sequence); + int roleBytesLength = ASN1_STRING_length(MongoDBRoleWrapped->value.sequence); + ASN1_SEQUENCE_ANY* MongoDBRole = nullptr; + MongoDBRole = d2i_ASN1_SEQUENCE_ANY( + &MongoDBRole, (const unsigned char**)&roleBytes, roleBytesLength); + if (!MongoDBRole) { + return Status(ErrorCodes::FailedToParse, + "Failed to parse role in x509 authorization grant"); + } + const auto roleGuard = MakeGuard( + [&MongoDBRole]() { sk_ASN1_TYPE_pop_free(MongoDBRole, ASN1_TYPE_free); }); + + if (sk_ASN1_TYPE_num(MongoDBRole) != 2) { + return Status(ErrorCodes::FailedToParse, + "Role entity in MongoDBAuthorizationGrant must have exactly " + "2 sequence elements"); + } + // Extract the subcomponents of the sequence, which are popped off the stack in + // reverse order. Here, parse the role's database. + ASN1_TYPE* roleComponent = sk_ASN1_TYPE_pop(MongoDBRole); + const auto roleDBGuard = + MakeGuard([roleComponent]() { ASN1_TYPE_free(roleComponent); }); + if (roleComponent->type != V_ASN1_UTF8STRING) { + return Status(ErrorCodes::FailedToParse, + "database in MongoDBRole must be a UTF8 string"); + } + std::string roleDB( + reinterpret_cast<char*>(ASN1_STRING_data(roleComponent->value.utf8string))); + + // Parse the role's name. + roleComponent = sk_ASN1_TYPE_pop(MongoDBRole); + const auto roleNameGuard = + MakeGuard([roleComponent]() { ASN1_TYPE_free(roleComponent); }); + if (roleComponent->type != V_ASN1_UTF8STRING) { + return Status(ErrorCodes::FailedToParse, + "role in MongoDBRole must be a UTF8 string"); + } + std::string roleName( + reinterpret_cast<char*>(ASN1_STRING_data(roleComponent->value.utf8string))); + + // Construct a RoleName from the subcomponents + roles.emplace(RoleName(roleName, roleDB)); + + } else { + return Status(ErrorCodes::FailedToParse, + "Unrecognized entity in MongoDBAuthorizationGrant"); + } + } + LOG(1) << "MONGODB-X509 authorization parsed the following roles from peer " + "certificate: " + << [&roles]() { + StringBuilder sb; + std::for_each(roles.begin(), roles.end(), [&sb](const RoleName& role) { + sb << role.toString(); + }); + return sb.str(); + }(); + } + } + + return roles; +} + +std::string SSLManagerInterface::getSSLErrorMessage(int code) { + // 120 from the SSL documentation for ERR_error_string + static const size_t msglen = 120; + + char msg[msglen]; + ERR_error_string_n(code, msg, msglen); + return msg; +} + +void SSLManager::_handleSSLError(int code, int ret) { + int err = ERR_get_error(); + + switch (code) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + // should not happen because we turned on AUTO_RETRY + // However, it turns out this CAN happen during a connect, if the other side + // accepts the socket connection but fails to do the SSL handshake in a timely + // manner. + error() << "SSL: " << code << ", possibly timed out during connect"; + break; + + case SSL_ERROR_ZERO_RETURN: + // TODO: Check if we can avoid throwing an exception for this condition + LOG(3) << "SSL network connection closed"; + break; + case SSL_ERROR_SYSCALL: + // If ERR_get_error returned 0, the error queue is empty + // check the return value of the actual SSL operation + if (err != 0) { + error() << "SSL: " << getSSLErrorMessage(err); + } else if (ret == 0) { + error() << "Unexpected EOF encountered during SSL communication"; + } else { + error() << "The SSL BIO reported an I/O error " << errnoWithDescription(); + } + break; + case SSL_ERROR_SSL: { + error() << "SSL: " << getSSLErrorMessage(err); + break; + } + + default: + error() << "unrecognized SSL error"; + break; + } + throwSocketError(SocketErrorKind::CONNECT_ERROR, ""); +} +} // namespace mongo + +// TODO SERVER-11601 Use NFC Unicode canonicalization +bool mongo::hostNameMatchForX509Certificates(std::string nameToMatch, std::string certHostName) { + nameToMatch = removeFQDNRoot(std::move(nameToMatch)); + certHostName = removeFQDNRoot(std::move(certHostName)); + + if (certHostName.size() < 2) { + return false; + } + + // match wildcard DNS names + if (certHostName[0] == '*' && certHostName[1] == '.') { + // allow name.example.com if the cert is *.example.com, '*' does not match '.' + const char* subName = strchr(nameToMatch.c_str(), '.'); + return subName && !strcasecmp(certHostName.c_str() + 1, subName); + } else { + return !strcasecmp(nameToMatch.c_str(), certHostName.c_str()); + } +} |