diff options
| author | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-18 17:02:53 -0300 |
|---|---|---|
| committer | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-18 17:02:53 -0300 |
| commit | 959575a5ca598bf5f37fb5cebe7ed1d80d3d71f7 (patch) | |
| tree | acc8d60aedb12b70048e676e8a7349deb0010db8 /src/mongo/util/net | |
| parent | 76588293975fc059cf076779e4283e6ffaf8afff (diff) | |
New upstream version 6.0.20upstream
Diffstat (limited to 'src/mongo/util/net')
| -rw-r--r-- | src/mongo/util/net/SConscript | 7 | ||||
| -rw-r--r-- | src/mongo/util/net/sock_test.cpp | 161 | ||||
| -rw-r--r-- | src/mongo/util/net/sock_test_utils.cpp | 199 | ||||
| -rw-r--r-- | src/mongo/util/net/sock_test_utils.h | 41 | ||||
| -rw-r--r-- | src/mongo/util/net/sockaddr.cpp | 2 | ||||
| -rw-r--r-- | src/mongo/util/net/ssl/detail/impl/engine_apple.ipp | 4 | ||||
| -rw-r--r-- | src/mongo/util/net/ssl_manager_apple.cpp | 14 | ||||
| -rw-r--r-- | src/mongo/util/net/ssl_manager_openssl.cpp | 57 | ||||
| -rw-r--r-- | src/mongo/util/net/ssl_manager_test.cpp | 387 | ||||
| -rw-r--r-- | src/mongo/util/net/ssl_manager_windows.cpp | 17 |
10 files changed, 673 insertions, 216 deletions
diff --git a/src/mongo/util/net/SConscript b/src/mongo/util/net/SConscript index 272cd9740a1..f885a122325 100644 --- a/src/mongo/util/net/SConscript +++ b/src/mongo/util/net/SConscript @@ -252,9 +252,10 @@ if get_option('ssl') == 'on': env.CppUnitTest( target='util_net_ssl_test', source=[ - 'ssl_manager_test.cpp', - 'ssl_options_test.cpp', - 'sock_test.cpp', + "ssl_manager_test.cpp", + "ssl_options_test.cpp", + "sock_test.cpp", + "sock_test_utils.cpp", ], LIBDEPS=[ '$BUILD_DIR/mongo/client/connection_string', diff --git a/src/mongo/util/net/sock_test.cpp b/src/mongo/util/net/sock_test.cpp index ccb751ea2dd..84d91f5ba1b 100644 --- a/src/mongo/util/net/sock_test.cpp +++ b/src/mongo/util/net/sock_test.cpp @@ -31,176 +31,17 @@ #include "mongo/util/net/sock.h" -#ifndef _WIN32 -#include <netdb.h> -#include <sys/socket.h> -#include <sys/types.h> -#endif - #include "mongo/db/server_options.h" #include "mongo/stdx/thread.h" #include "mongo/unittest/unittest.h" #include "mongo/util/concurrency/notification.h" #include "mongo/util/fail_point.h" +#include "mongo/util/net/sock_test_utils.h" #include "mongo/util/net/socket_exception.h" namespace { using namespace mongo; -using std::shared_ptr; - -typedef std::shared_ptr<Socket> SocketPtr; -typedef std::pair<SocketPtr, SocketPtr> SocketPair; - -// On UNIX, make a connected pair of PF_LOCAL (aka PF_UNIX) sockets via the native 'socketpair' -// call. The 'type' parameter should be one of SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, etc. -// For Win32, we don't have a native socketpair function, so we hack up a connected PF_INET -// pair on a random port. -SocketPair socketPair(int type, int protocol = 0); - -#if defined(_WIN32) -namespace detail { -void awaitAccept(SOCKET* acceptSock, SOCKET listenSock, Notification<void>& notify) { - *acceptSock = INVALID_SOCKET; - const SOCKET result = ::accept(listenSock, nullptr, 0); - if (result != INVALID_SOCKET) { - *acceptSock = result; - } - notify.set(); -} - -void awaitConnect(SOCKET* connectSock, const struct addrinfo& where, Notification<void>& notify) { - *connectSock = INVALID_SOCKET; - SOCKET newSock = ::socket(where.ai_family, where.ai_socktype, where.ai_protocol); - if (newSock != INVALID_SOCKET) { - int result = ::connect(newSock, where.ai_addr, where.ai_addrlen); - if (result == 0) { - *connectSock = newSock; - } - } - notify.set(); -} -} // namespace detail - -SocketPair socketPair(const int type, const int protocol) { - const int domain = PF_INET; - - // Create a listen socket and a connect socket. - const SOCKET listenSock = ::socket(domain, type, protocol); - if (listenSock == INVALID_SOCKET) - return SocketPair(); - - // Bind the listen socket on port zero, it will pick one for us, and start it listening - // for connections. - struct addrinfo hints, *res; - ::memset(&hints, 0, sizeof(hints)); - hints.ai_family = PF_INET; - hints.ai_socktype = type; - hints.ai_flags = AI_PASSIVE; - - int result = ::getaddrinfo(nullptr, "0", &hints, &res); - if (result != 0) { - closesocket(listenSock); - return SocketPair(); - } - - result = ::bind(listenSock, res->ai_addr, res->ai_addrlen); - if (result != 0) { - closesocket(listenSock); - ::freeaddrinfo(res); - return SocketPair(); - } - - // Read out the port to which we bound. - sockaddr_in bindAddr; - ::socklen_t len = sizeof(bindAddr); - ::memset(&bindAddr, 0, sizeof(bindAddr)); - result = ::getsockname(listenSock, reinterpret_cast<struct sockaddr*>(&bindAddr), &len); - if (result != 0) { - closesocket(listenSock); - ::freeaddrinfo(res); - return SocketPair(); - } - - result = ::listen(listenSock, 1); - if (result != 0) { - closesocket(listenSock); - ::freeaddrinfo(res); - return SocketPair(); - } - - struct addrinfo connectHints, *connectRes; - ::memset(&connectHints, 0, sizeof(connectHints)); - connectHints.ai_family = PF_INET; - connectHints.ai_socktype = type; - std::stringstream portStream; - portStream << ntohs(bindAddr.sin_port); - result = ::getaddrinfo(nullptr, portStream.str().c_str(), &connectHints, &connectRes); - if (result != 0) { - closesocket(listenSock); - ::freeaddrinfo(res); - return SocketPair(); - } - - // I'd prefer to avoid trying to do this non-blocking on Windows. Just spin up some - // threads to do the connect and acccept. - - Notification<void> accepted; - SOCKET acceptSock = INVALID_SOCKET; - stdx::thread acceptor([&] { detail::awaitAccept(&acceptSock, listenSock, accepted); }); - - Notification<void> connected; - SOCKET connectSock = INVALID_SOCKET; - stdx::thread connector([&] { detail::awaitConnect(&connectSock, *connectRes, connected); }); - - connected.get(); - connector.join(); - if (connectSock == INVALID_SOCKET) { - closesocket(listenSock); - ::freeaddrinfo(res); - ::freeaddrinfo(connectRes); - closesocket(acceptSock); - closesocket(connectSock); - return SocketPair(); - } - - accepted.get(); - acceptor.join(); - if (acceptSock == INVALID_SOCKET) { - closesocket(listenSock); - ::freeaddrinfo(res); - ::freeaddrinfo(connectRes); - closesocket(acceptSock); - closesocket(connectSock); - return SocketPair(); - } - - closesocket(listenSock); - ::freeaddrinfo(res); - ::freeaddrinfo(connectRes); - - SocketPtr first(new Socket(static_cast<int>(acceptSock), SockAddr())); - SocketPtr second(new Socket(static_cast<int>(connectSock), SockAddr())); - - return SocketPair(first, second); -} -#else -// We can just use ::socketpair and wrap up the result in a Socket. -SocketPair socketPair(const int type, const int protocol) { - // PF_LOCAL is the POSIX name for Unix domain sockets, while PF_UNIX - // is the name that BSD used. We use the BSD name because it is more - // widely supported (e.g. Solaris 10). - const int domain = PF_UNIX; - - int socks[2]; - const int result = ::socketpair(domain, type, protocol, socks); - if (result == 0) { - return SocketPair(SocketPtr(new Socket(socks[0], SockAddr())), - SocketPtr(new Socket(socks[1], SockAddr()))); - } - return SocketPair(); -} -#endif // This should match the name of the fail point declared in sock.cpp. const char kSocketFailPointName[] = "throwSockExcep"; diff --git a/src/mongo/util/net/sock_test_utils.cpp b/src/mongo/util/net/sock_test_utils.cpp new file mode 100644 index 00000000000..9164b8077ca --- /dev/null +++ b/src/mongo/util/net/sock_test_utils.cpp @@ -0,0 +1,199 @@ +/** + * Copyright (C) 2024-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/sock_test_utils.h" + +#ifndef _WIN32 +#include <netdb.h> +#include <sys/socket.h> +#include <sys/types.h> +#endif + +#include "mongo/stdx/thread.h" +#include "mongo/util/concurrency/notification.h" +#include "mongo/util/net/socket_exception.h" + +namespace mongo { +namespace { + +#if defined(_WIN32) +namespace detail { +void awaitAccept(SOCKET* acceptSock, SOCKET listenSock, Notification<void>& notify) { + *acceptSock = INVALID_SOCKET; + const SOCKET result = ::accept(listenSock, nullptr, 0); + if (result != INVALID_SOCKET) { + *acceptSock = result; + } + notify.set(); +} + +void awaitConnect(SOCKET* connectSock, const struct addrinfo& where, Notification<void>& notify) { + *connectSock = INVALID_SOCKET; + SOCKET newSock = ::socket(where.ai_family, where.ai_socktype, where.ai_protocol); + if (newSock != INVALID_SOCKET) { + int result = ::connect(newSock, where.ai_addr, where.ai_addrlen); + if (result == 0) { + *connectSock = newSock; + } + } + notify.set(); +} +} // namespace detail + +SocketPair socketPairImpl(const int type, const int protocol) { + const int domain = PF_INET; + + // Create a listen socket and a connect socket. + const SOCKET listenSock = ::socket(domain, type, protocol); + if (listenSock == INVALID_SOCKET) + return SocketPair(); + + // Bind the listen socket on port zero, it will pick one for us, and start it listening + // for connections. + struct addrinfo hints, *res; + ::memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_INET; + hints.ai_socktype = type; + hints.ai_flags = AI_PASSIVE; + + int result = ::getaddrinfo(nullptr, "0", &hints, &res); + if (result != 0) { + closesocket(listenSock); + return SocketPair(); + } + + result = ::bind(listenSock, res->ai_addr, res->ai_addrlen); + if (result != 0) { + closesocket(listenSock); + ::freeaddrinfo(res); + return SocketPair(); + } + + // Read out the port to which we bound. + sockaddr_in bindAddr; + ::socklen_t len = sizeof(bindAddr); + ::memset(&bindAddr, 0, sizeof(bindAddr)); + result = ::getsockname(listenSock, reinterpret_cast<struct sockaddr*>(&bindAddr), &len); + if (result != 0) { + closesocket(listenSock); + ::freeaddrinfo(res); + return SocketPair(); + } + + result = ::listen(listenSock, 1); + if (result != 0) { + closesocket(listenSock); + ::freeaddrinfo(res); + return SocketPair(); + } + + struct addrinfo connectHints, *connectRes; + ::memset(&connectHints, 0, sizeof(connectHints)); + connectHints.ai_family = PF_INET; + connectHints.ai_socktype = type; + std::stringstream portStream; + portStream << ntohs(bindAddr.sin_port); + result = ::getaddrinfo(nullptr, portStream.str().c_str(), &connectHints, &connectRes); + if (result != 0) { + closesocket(listenSock); + ::freeaddrinfo(res); + return SocketPair(); + } + + // I'd prefer to avoid trying to do this non-blocking on Windows. Just spin up some + // threads to do the connect and acccept. + + Notification<void> accepted; + SOCKET acceptSock = INVALID_SOCKET; + stdx::thread acceptor([&] { detail::awaitAccept(&acceptSock, listenSock, accepted); }); + + Notification<void> connected; + SOCKET connectSock = INVALID_SOCKET; + stdx::thread connector([&] { detail::awaitConnect(&connectSock, *connectRes, connected); }); + + connected.get(); + connector.join(); + if (connectSock == INVALID_SOCKET) { + closesocket(listenSock); + ::freeaddrinfo(res); + ::freeaddrinfo(connectRes); + closesocket(acceptSock); + closesocket(connectSock); + return SocketPair(); + } + + accepted.get(); + acceptor.join(); + if (acceptSock == INVALID_SOCKET) { + closesocket(listenSock); + ::freeaddrinfo(res); + ::freeaddrinfo(connectRes); + closesocket(acceptSock); + closesocket(connectSock); + return SocketPair(); + } + + closesocket(listenSock); + ::freeaddrinfo(res); + ::freeaddrinfo(connectRes); + + SocketPtr first = std::make_shared<Socket>(static_cast<int>(acceptSock), SockAddr()); + SocketPtr second = std::make_shared<Socket>(static_cast<int>(connectSock), SockAddr()); + return SocketPair(first, second); +} +#else +// We can just use ::socketpair and wrap up the result in a Socket. +SocketPair socketPairImpl(const int type, const int protocol) { + // PF_LOCAL is the POSIX name for Unix domain sockets, while PF_UNIX + // is the name that BSD used. We use the BSD name because it is more + // widely supported (e.g. Solaris 10). + const int domain = PF_UNIX; + + int socks[2]; + const int result = ::socketpair(domain, type, protocol, socks); + if (result == 0) { + return SocketPair(std::make_shared<Socket>(socks[0], SockAddr()), + std::make_shared<Socket>(socks[1], SockAddr())); + } + return SocketPair(); +} +#endif +} // namespace + +// On UNIX, make a connected pair of PF_LOCAL (aka PF_UNIX) sockets via the native 'socketpair' +// call. The 'type' parameter should be one of SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, etc. +// For Win32, we don't have a native socketpair function, so we hack up a connected PF_INET +// pair on a random port. +SocketPair socketPair(int type, int protocol) { + return socketPairImpl(type, protocol); +} + +} // namespace mongo diff --git a/src/mongo/util/net/sock_test_utils.h b/src/mongo/util/net/sock_test_utils.h new file mode 100644 index 00000000000..f168edf1292 --- /dev/null +++ b/src/mongo/util/net/sock_test_utils.h @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2024-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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. + */ + +#pragma once + +#include "mongo/util/net/sock.h" + +namespace mongo { +using SocketPtr = std::shared_ptr<Socket>; +using SocketPair = std::pair<SocketPtr, SocketPtr>; + +// Create a connected pair of sockets for testing purposes. +SocketPair socketPair(int type, int protocol = 0); + +} // namespace mongo diff --git a/src/mongo/util/net/sockaddr.cpp b/src/mongo/util/net/sockaddr.cpp index 1a2b058cbf3..5bf2998df6d 100644 --- a/src/mongo/util/net/sockaddr.cpp +++ b/src/mongo/util/net/sockaddr.cpp @@ -149,7 +149,7 @@ void SockAddr::initUnixDomainSocket(StringData path, int port) { uassert( 13079, "path to unix socket too long", path.size() < sizeof(as<sockaddr_un>().sun_path)); as<sockaddr_un>().sun_family = AF_UNIX; - path.copyTo(as<sockaddr_un>().sun_path, /* includeEndingNull =*/true); + str::copyAsCString(as<sockaddr_un>().sun_path, path); addressSize = sizeof(sockaddr_un); _isValid = true; } diff --git a/src/mongo/util/net/ssl/detail/impl/engine_apple.ipp b/src/mongo/util/net/ssl/detail/impl/engine_apple.ipp index 015a44fcee8..b863cb0e75a 100644 --- a/src/mongo/util/net/ssl/detail/impl/engine_apple.ipp +++ b/src/mongo/util/net/ssl/detail/impl/engine_apple.ipp @@ -158,10 +158,6 @@ bool engine::_initSSL(stream_base::handshake_type type, asio::error_code& ec) { } if (status == ::errSecSuccess) { - status = ::SSLSetPeerID(_ssl.get(), _ssl.get(), sizeof(native_handle_type)); - } - - if (status == ::errSecSuccess) { status = ::SSLSetIOFuncs(_ssl.get(), read_func, write_func); } diff --git a/src/mongo/util/net/ssl_manager_apple.cpp b/src/mongo/util/net/ssl_manager_apple.cpp index c13a4bb8eb8..dcde5542ca9 100644 --- a/src/mongo/util/net/ssl_manager_apple.cpp +++ b/src/mongo/util/net/ssl_manager_apple.cpp @@ -1156,7 +1156,6 @@ public: } uassertOSStatusOK(::SSLSetConnection(_ssl.get(), static_cast<void*>(this))); - uassertOSStatusOK(::SSLSetPeerID(_ssl.get(), _ssl.get(), sizeof(_ssl))); uassertOSStatusOK(::SSLSetIOFuncs(_ssl.get(), read_func, write_func)); uassertOSStatusOK(::SSLSetProtocolVersionMin(_ssl.get(), ctx->protoMin)); uassertOSStatusOK(::SSLSetProtocolVersionMax(_ssl.get(), ctx->protoMax)); @@ -1654,11 +1653,14 @@ Future<SSLPeerInfo> SSLManagerApple::parseAndValidatePeerCertificate( return swPeerSubjectName.getStatus(); } const auto peerSubjectName = std::move(swPeerSubjectName.getValue()); - LOGV2_DEBUG(23207, - 2, - "Accepted TLS connection from peer: {peerSubjectName}", - "Accepted TLS connection from peer", - "peerSubjectName"_attr = peerSubjectName); + // The cipher will be presented as a number. + ::SSLCipherSuite cipher; + uassertOSStatusOK(::SSLGetNegotiatedCipher(ssl, &cipher)); + + LOGV2_INFO(6723803, + "Accepted TLS connection from peer", + "peerSubjectName"_attr = peerSubjectName, + "cipher"_attr = cipher); // Server side. if (remoteHost.empty()) { diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp index 43c646bc16c..b27676156d6 100644 --- a/src/mongo/util/net/ssl_manager_openssl.cpp +++ b/src/mongo/util/net/ssl_manager_openssl.cpp @@ -319,23 +319,6 @@ X509* X509_OBJECT_get0_X509(const X509_OBJECT* a) { return a->data.x509; } -UniqueStackOfX509 SSLgetVerifiedChain(SSL* s) { - auto* store = SSL_CTX_get_cert_store(SSL_get_SSL_CTX(s)); - auto* peerChain = SSL_get_peer_cert_chain(s); - UniqueX509 peer(SSL_get_peer_certificate(s)); - - UniqueX509StoreCtx ctx(X509_STORE_CTX_new()); - if (!X509_STORE_CTX_init(ctx.get(), store, peer.get(), peerChain)) { - return nullptr; - } - - if (X509_verify_cert(ctx.get()) <= 0) { - return nullptr; - } - - return UniqueStackOfX509(X509_STORE_CTX_get1_chain(ctx.get())); -} - const OCSP_CERTID* OCSP_SINGLERESP_get0_id(const OCSP_SINGLERESP* single) { return single->certId; } @@ -368,16 +351,24 @@ static ASN1OID tlsFeatureOID("1.3.6.1.5.5.7.1.24", "tlsfeature", "TLS Feature"); static int const NID_tlsfeature = OBJ_create(tlsFeatureOID.identifier.c_str(), tlsFeatureOID.shortDescription.c_str(), tlsFeatureOID.longDescription.c_str()); +#endif -#else UniqueStackOfX509 SSLgetVerifiedChain(SSL* s) { - auto chain = SSL_get0_verified_chain(s); + auto* store = SSL_CTX_get_cert_store(SSL_get_SSL_CTX(s)); + auto* peerChain = SSL_get_peer_cert_chain(s); + UniqueX509 peer(SSL_get_peer_certificate(s)); - return UniqueStackOfX509(X509_chain_up_ref(chain)); -} + UniqueX509StoreCtx ctx(X509_STORE_CTX_new()); + if (!X509_STORE_CTX_init(ctx.get(), store, peer.get(), peerChain)) { + return nullptr; + } -#endif + if (X509_verify_cert(ctx.get()) <= 0) { + return nullptr; + } + return UniqueStackOfX509(X509_STORE_CTX_get1_chain(ctx.get())); +} SSLX509Name convertX509ToSSLX509Name(X509_NAME* x509Name) { std::vector<std::vector<SSLX509Name::Entry>> entries; @@ -2996,7 +2987,7 @@ bool SSLManagerOpenSSL::_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_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); X509_LOOKUP* lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file()); fassert(16584, lookup); @@ -3010,15 +3001,7 @@ bool SSLManagerOpenSSL::_setupCRL(SSL_CTX* context, const std::string& crlFile) return false; } - if (status == 1) { - LOGV2(4652601, "ssl imported 1 revoked certificate from the revocation list."); - } else { - LOGV2(4652602, - "ssl imported {numberCerts} revoked certificates from the revocation list", - "SSL imported revoked certificates from the revocation list", - "numberCerts"_attr = status); - } - + LOGV2(4652602, "SSL imported certificate revocation list(s)", "numberCRLs"_attr = status); return true; } @@ -3280,11 +3263,11 @@ Future<SSLPeerInfo> SSLManagerOpenSSL::parseAndValidatePeerCertificate( // TODO: check optional cipher restriction, using cert. auto peerSubject = getCertificateSubjectX509Name(peerCert.get()); - LOGV2_DEBUG(23229, - 2, - "Accepted TLS connection from peer: {peerSubject}", - "Accepted TLS connection from peer", - "peerSubject"_attr = peerSubject); + const auto cipher = SSL_get_current_cipher(conn); + LOGV2_INFO(6723801, + "Accepted TLS connection from peer", + "peerSubject"_attr = peerSubject, + "cipher"_attr = SSL_CIPHER_get_name(cipher)); StatusWith<stdx::unordered_set<RoleName>> swPeerCertificateRoles = _parsePeerRoles(peerCert.get()); diff --git a/src/mongo/util/net/ssl_manager_test.cpp b/src/mongo/util/net/ssl_manager_test.cpp index 1dd6585f7c1..80e83af242a 100644 --- a/src/mongo/util/net/ssl_manager_test.cpp +++ b/src/mongo/util/net/ssl_manager_test.cpp @@ -29,15 +29,20 @@ #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest +#include <asio.hpp> +#include <boost/filesystem.hpp> #include <fstream> #include "mongo/config.h" #include "mongo/platform/basic.h" +#include "mongo/bson/json.h" #include "mongo/transport/service_entry_point.h" #include "mongo/transport/transport_layer_asio.h" #include "mongo/transport/transport_layer_manager.h" +#include "mongo/util/net/sock_test_utils.h" #include "mongo/util/net/ssl/context.hpp" +#include "mongo/util/net/ssl/stream.hpp" #include "mongo/util/net/ssl_manager.h" #include "mongo/util/net/ssl_options.h" @@ -49,10 +54,36 @@ #include "mongo/util/net/ssl/context_openssl.hpp" #endif +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest + +namespace fs = boost::filesystem; namespace mongo { namespace { +#define TEST_CERTS_DIR "jstests/libs/" +// certs & CRLs rooted in ca.pem +constexpr const char* caFile = TEST_CERTS_DIR "ca.pem"; +constexpr const char* serverKeyFile = TEST_CERTS_DIR "server.pem"; +constexpr const char* clientKeyFile = TEST_CERTS_DIR "client.pem"; +constexpr const char* revokedClientKeyFile = TEST_CERTS_DIR "client_revoked.pem"; + +constexpr const char* intermediateACaFile = TEST_CERTS_DIR "intermediate-ca.pem"; +constexpr const char* intermediateALeafKeyFile = TEST_CERTS_DIR "server-intermediate-leaf.pem"; +constexpr const char* intermediateBCaFile = TEST_CERTS_DIR "intermediate-ca-B.pem"; +constexpr const char* intermediateBLeafKeyFile = TEST_CERTS_DIR "intermediate-ca-B-leaf.pem"; +constexpr const char* emptyCRL = TEST_CERTS_DIR "crl.pem"; +constexpr const char* expiredCRL = TEST_CERTS_DIR "crl_expired.pem"; +constexpr const char* clientRevokedCRL = TEST_CERTS_DIR "crl_client_revoked.pem"; +constexpr const char* intermediateBRevokedCRL = TEST_CERTS_DIR "crl_intermediate_ca_B_revoked.pem"; +constexpr const char* intermediateBCRL = TEST_CERTS_DIR "crl_from_intermediate_ca_B.pem"; + +// certs & CRLs rooted in trusted-ca.pem +constexpr const char* trustedCaFile = TEST_CERTS_DIR "trusted-ca.pem"; +constexpr const char* trustedServerKeyFile = TEST_CERTS_DIR "trusted-server.pem"; +constexpr const char* trustedClientKeyFile = TEST_CERTS_DIR "trusted-client.pem"; +constexpr const char* trustedEmptyCRL = TEST_CERTS_DIR "crl_from_trusted_ca.pem"; + // Test implementation needed by ASIO transport. class ServiceEntryPointUtil : public ServiceEntryPoint { public: @@ -103,7 +134,7 @@ public: } private: - mutable Mutex _mutex = MONGO_MAKE_LATCH("::_mutex"); + mutable Mutex _mutex; stdx::condition_variable _cv; std::vector<transport::SessionHandle> _sessions; transport::TransportLayer* _transport = nullptr; @@ -115,6 +146,76 @@ std::string loadFile(const std::string& name) { return str; } +// Reads the input stream until EOF or a valid PEM block is encountered. +// Skips private key PEM blocks if includePrivateKeys is true. +// Returns the parsed PEM block as a string (with newlines), or an empty +// string if none is found or a read error occurs. +std::string readOnePEMBlock(std::ifstream& inputStrm, bool includePrivateKeys) { + std::string line; + for (;;) { + std::stringstream output; + bool foundBegin = false; + bool foundEnd = false; + bool discard = false; + + while (!foundBegin && std::getline(inputStrm, line)) { + StringData lineSD(line); + foundBegin = (lineSD.startsWith("-----BEGIN ") && lineSD.endsWith("-----")); + } + if (!foundBegin) { + return ""; + } + + discard = (!includePrivateKeys && line.find("PRIVATE KEY") != std::string::npos); + output << line << std::endl; + + while (!foundEnd && std::getline(inputStrm, line)) { + StringData lineSD(line); + output << line << std::endl; + foundEnd = (lineSD.startsWith("-----END ") && lineSD.endsWith("-----")); + } + if (!foundEnd) { + return ""; + } + if (!discard) { + return output.str(); + } + } +} + +struct PEMFileSpec { + std::string path; + bool includePrivateKeys{false}; + void serialize(BSONObjBuilder* bob) const { + bob->append("path", path); + bob->append("includePrivateKeys", includePrivateKeys); + } +}; +// Given a list of PEM files, this concatenates the PEM blocks in those files +// (optionally filtering out private keys) and writes the result into a temporary +// file. Returns the path to the temp file. +std::string combinePEMFiles(const std::vector<PEMFileSpec>& pemSpecs) { + // make a temp file for the output + auto path = fs::temp_directory_path() / fs::unique_path("tmpfile_%%%%_%%%%_%%%%_%%%%.pem"); + std::ofstream outStream(path.string()); + invariant(outStream.is_open()); + + LOGV2( + 9476600, "Combining PEM files", "output"_attr = path.string(), "pemFiles"_attr = pemSpecs); + + // read & parse the PEM files; append PEM blocks to output + for (auto& pemSpec : pemSpecs) { + std::ifstream input(pemSpec.path); + std::string pemBlock; + do { + pemBlock = readOnePEMBlock(input, pemSpec.includePrivateKeys); + outStream << pemBlock; + } while (!pemBlock.empty()); + } + outStream.close(); + return path.string(); +} + TEST(SSLManager, matchHostname) { enum Expected : bool { match = true, mismatch = false }; const struct { @@ -734,6 +835,8 @@ TEST(SSLManager, TransientSSLParamsStressTestWithManager) { #endif // MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL +#ifdef MONGO_CONFIG_SSL + static bool isSanWarningWritten(const std::vector<std::string>& logLines) { for (const auto& line : logLines) { if (std::string::npos != @@ -780,6 +883,288 @@ TEST(SSLManager, InitContextNoSanWarning) { ASSERT_FALSE(isSanWarningWritten(getCapturedTextFormatLogMessages())); } +class SSLTestFixture { +public: + SSLTestFixture(const SSLParams& ingressParams, + const SSLParams& egressParams, + bool ingressIsServer = true, + bool egressIsServer = true, + const boost::optional<TransientSSLParams>& transientSSLParams = boost::none) { + auto serviceContext = ServiceContext::make(); + setGlobalServiceContext(std::move(serviceContext)); + + // SSLManagerWindows uses this global boolean to decide whether to + // use unique key container names when setting up the crypto context. + // This must be true in order for the handshake to work. + isSSLServer = true; + + serverSSLManager = SSLManagerInterface::create(ingressParams, ingressIsServer); + clientSSLManager = + SSLManagerInterface::create(egressParams, transientSSLParams, egressIsServer); + + serverSSLContext = std::make_shared<asio::ssl::context>(asio::ssl::context::sslv23); + clientSSLContext = std::make_shared<asio::ssl::context>(asio::ssl::context::sslv23); + uassertStatusOK( + serverSSLManager->initSSLContext(serverSSLContext->native_handle(), + ingressParams, + SSLManagerInterface::ConnectionDirection::kIncoming)); + uassertStatusOK( + clientSSLManager->initSSLContext(clientSSLContext->native_handle(), + egressParams, + SSLManagerInterface::ConnectionDirection::kOutgoing)); + } + + void doHandshake() { + auto socks = socketPair(SOCK_STREAM); + + serverConn = std::make_shared<ConnectionContext>(socks.first->rawFD(), *serverSSLContext); + clientConn = std::make_shared<ConnectionContext>(socks.second->rawFD(), *clientSSLContext); + Status serverStatus = Status::OK(); + Status clientStatus = Status::OK(); + + auto serverThread = stdx::thread([this, &serverStatus]() { + try { + serverConn->sslSocket->handshake(asio::ssl::stream_base::server); + } catch (const DBException& ex) { + serverStatus = ex.toStatus().withContext("Server handshake failed"); + } + }); + + try { + clientConn->sslSocket->handshake(asio::ssl::stream_base::client); + } catch (const DBException& ex) { + clientStatus = ex.toStatus().withContext("Client handshake failed"); + } + serverThread.join(); + + // rethrow any handshake errors with context + uassertStatusOK(serverStatus); + uassertStatusOK(clientStatus); + } + + struct IngressEgressValidationResult { + StatusWith<SSLPeerInfo> ingress; + StatusWith<SSLPeerInfo> egress; + }; + IngressEgressValidationResult runIngressEgressValidation(); + + class ConnectionContext { + public: + ConnectionContext(int fd, asio::ssl::context& ctx) : io_context() { + asio::ip::tcp::socket socket(io_context, asio::ip::tcp::v4(), fd); + sslSocket = + std::make_unique<asio::ssl::stream<decltype(socket)>>(std::move(socket), ctx, ""); + } + asio::io_context io_context; + std::unique_ptr<asio::ssl::stream<asio::ip::tcp::socket>> sslSocket; + }; + + std::shared_ptr<SSLManagerInterface> clientSSLManager; + std::shared_ptr<SSLManagerInterface> serverSSLManager; + + std::shared_ptr<asio::ssl::context> clientSSLContext; + std::shared_ptr<asio::ssl::context> serverSSLContext; + + std::shared_ptr<ConnectionContext> clientConn; + std::shared_ptr<ConnectionContext> serverConn; +}; + +SSLTestFixture::IngressEgressValidationResult SSLTestFixture::runIngressEgressValidation() { + static const HostAndPort hostForLogging("hostforlogging"); + + // Caller must doHandshake beforehand + invariant(serverConn); + invariant(clientConn); + + IngressEgressValidationResult result{SSLPeerInfo{}, SSLPeerInfo{}}; + + // do ingress (server) first + try { + result.ingress = + serverSSLManager + ->parseAndValidatePeerCertificate(serverConn->sslSocket->native_handle(), + boost::none, + "", + hostForLogging, + nullptr) + .get(); + } catch (const DBException& ex) { + result.ingress = ex.toStatus(); + } + + // do egress (client) next + try { + result.egress = + clientSSLManager + ->parseAndValidatePeerCertificate(clientConn->sslSocket->native_handle(), + boost::none, + "localhost", + hostForLogging, + nullptr) + .get(); + } catch (const DBException& ex) { + result.egress = ex.toStatus(); + } + + return result; +} + +struct CertValidationTestCase { + std::string cafile; + std::string clusterCaFile; + bool pass; + bool allowInvalidCerts{false}; + + void serialize(BSONObjBuilder* bob) const { + bob->append("CAFile", cafile); + bob->append("clusterCAFile", clusterCaFile); + bob->append("expectPass", pass); + bob->append("allowInvalidCerts", allowInvalidCerts); + } +}; + +void checkValidationResults(SSLTestFixture::IngressEgressValidationResult& result, + bool expectIngressPass, + bool expectEgressPass, + ErrorCodes::Error expectIngressCode = ErrorCodes::SSLHandshakeFailed, + ErrorCodes::Error expectEgressCode = ErrorCodes::SSLHandshakeFailed) { + ASSERT_EQ(result.ingress.isOK(), expectIngressPass) + << "Ingress validation status: " << result.ingress.getStatus(); + ASSERT_EQ(result.egress.isOK(), expectEgressPass) + << "Egress validation status: " << result.egress.getStatus(); + if (!result.ingress.isOK()) { + ASSERT_EQ(result.ingress.getStatus().code(), expectIngressCode) + << "Ingress validation status: " << result.ingress.getStatus(); + } + if (!result.egress.isOK()) { + ASSERT_EQ(result.egress.getStatus().code(), expectEgressCode) + << "Egress validation status: " << result.egress.getStatus(); + } +} + +// Tests that validation fails if configured CRL for the issuer of the peer certificate being +// validated has expired. +// Caveats: +// - Apple: CRL unsupported; test disabled +// - Windows: validation fails, but with misleading error message +#if MONGO_CONFIG_SSL_PROVIDER != MONGO_CONFIG_SSL_PROVIDER_APPLE +TEST(SSLManager, expiredCRLTest) { + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslCAFile = caFile; + clientParams.sslPEMKeyFile = clientKeyFile; + clientParams.sslCRLFile = expiredCRL; + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = serverKeyFile; + serverParams.sslCRLFile = expiredCRL; + + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, false /*expectIngressPass*/, false /*expectEgressPass*/); + +#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS + constexpr const char* cause = "revocation server was offline"; +#else + constexpr const char* cause = "expired"; +#endif + ASSERT_NE(result.ingress.getStatus().reason().find(cause), std::string::npos); + ASSERT_NE(result.egress.getStatus().reason().find(cause), std::string::npos); +} + +// Tests basic CRL revocation works on ingress if the client is configured with a revoked key. +// Caveats: +// - Apple: CRL unsupported; test disabled +TEST(SSLManager, basicCRLRevocationTests) { + struct TestCase { + std::string serverCRLFile; + bool serverPass; + void serialize(BSONObjBuilder* bob) const { + bob->append("serverCRLFile", serverCRLFile); + bob->append("serverPass", serverPass); + } + }; + + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslCAFile = trustedCaFile; + clientParams.sslPEMKeyFile = revokedClientKeyFile; + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = trustedServerKeyFile; + + { + serverParams.sslCRLFile = emptyCRL; + LOGV2(9476702, "Running test case", "CRLFile"_attr = emptyCRL, "pass"_attr = true); + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, true, true /*expectEgressPass*/); + } + { + serverParams.sslCRLFile = clientRevokedCRL; + LOGV2(9476703, "Running test case", "CRLFile"_attr = clientRevokedCRL, "pass"_attr = false); + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, false, true /*expectEgressPass*/); + ASSERT_NE(result.ingress.getStatus().reason().find("revoked"), std::string::npos); + } +} + +// Tests whether validation passes if an intermediate CA issuer cert is revoked, but +// the end-entity cert is not. +// Caveats: +// - Apple: CRL unsupported; test disabled +// - Windows: multiple CRLs (root CRL + intermediate CRL) is not allowed +// TODO: backport SERVER-95583 +TEST(SSLManager, revocationWithCRLsIntermediateTests) { + // intermediate-ca-B.pem + intermediate-ca-B-leaf.pem bundle + const std::string intermediateBLeafWithIssuerCertKeyFile = combinePEMFiles( + {{intermediateBLeafKeyFile, true /*includePrivKey*/}, {intermediateBCaFile}}); + // crl_from_intermediate_ca_B.pem + crl_intermediate_ca_B_revoked.pem + const std::string crlsFromRootAndIntermediateB = + combinePEMFiles({{intermediateBRevokedCRL}, {intermediateBCRL}}); + + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslCAFile = caFile; + clientParams.sslPEMKeyFile = clientKeyFile; + clientParams.sslCRLFile = crlsFromRootAndIntermediateB; + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = intermediateBLeafWithIssuerCertKeyFile; + +#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS + ASSERT_THROWS_CODE_AND_WHAT( + SSLManagerInterface::create(clientParams, true), + DBException, + ErrorCodes::InvalidSSLConfiguration, + "CertAddCRLContextToStore Failed The object or property already exists."); +#else + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, true, false); + ASSERT_NE(result.egress.getStatus().reason().find("revoked"), std::string::npos); +#endif +} + +#endif // MONGO_CONFIG_SSL_PROVIDER != MONGO_CONFIG_SSL_PROVIDER_APPLE +#endif // MONGO_CONFIG_SSL } // namespace } // namespace mongo diff --git a/src/mongo/util/net/ssl_manager_windows.cpp b/src/mongo/util/net/ssl_manager_windows.cpp index b88ae85e5b4..7169e1eef1f 100644 --- a/src/mongo/util/net/ssl_manager_windows.cpp +++ b/src/mongo/util/net/ssl_manager_windows.cpp @@ -2062,10 +2062,19 @@ Future<SSLPeerInfo> SSLManagerWindows::parseAndValidatePeerCertificate( return Future<SSLPeerInfo>::makeReady(SSLPeerInfo(sni)); } - LOGV2_DEBUG(23270, - 2, - "Accepted TLS connection from peer: {peerSubjectName}", - "peerSubjectName"_attr = peerSubjectName); + SecPkgContext_CipherInfo cipherInfo; + SECURITY_STATUS ssCipher = QueryContextAttributes(ssl, SECPKG_ATTR_CIPHER_INFO, &cipherInfo); + if (ssCipher != SEC_E_OK) { + return Status(ErrorCodes::SSLHandshakeFailed, + str::stream() + << "QueryContextAttributes for connection info failed with" << ssCipher); + } + const auto cipher = std::wstring(cipherInfo.szCipherSuite); + + LOGV2_INFO(6723802, + "Accepted TLS connection from peer", + "peerSubjectName"_attr = peerSubjectName, + "cipher"_attr = toUtf8String(cipher)); // If this is a server and client and server certificate are the same, log a warning. if (remoteHost.empty() && _sslConfiguration.serverSubjectName() == peerSubjectName) { |
