From 56e653fdd204e1ad091e0736454aefc005b5ce3f Mon Sep 17 00:00:00 2001 From: Spencer Jackson Date: Thu, 15 Feb 2018 15:30:46 -0500 Subject: SERVER-33329: Make server and shell emit TLS protocol_version alerts (cherry picked from commit 51af489a86f1862de87b51f26a9e818ec3b5df04) --- src/mongo/transport/session_asio.h | 17 ++++ src/mongo/util/net/message_port.cpp | 12 +++ src/mongo/util/net/ssl_manager.cpp | 156 ++++++++++++++++++++++++++++++++++-- src/mongo/util/net/ssl_manager.h | 7 ++ 4 files changed, 184 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/mongo/transport/session_asio.h b/src/mongo/transport/session_asio.h index 6128d1515d6..59b8f8f57c8 100644 --- a/src/mongo/transport/session_asio.h +++ b/src/mongo/transport/session_asio.h @@ -283,6 +283,23 @@ private: false); } + auto tlsAlert = checkTLSRequest( + ConstDataRange(asio::buffer_cast(buffer), asio::buffer_size(buffer))); + if (tlsAlert) { + return opportunisticWrite( + sync, + _socket, + asio::buffer(tlsAlert->data(), tlsAlert->size()), + [ this, onComplete = std::move(onComplete) ](const std::error_code& ec, + size_t size) { + return onComplete( + {ErrorCodes::SSLHandshakeFailed, + "SSL handshake failed, as client requested disabled protocol"}, + false); + }); + } + + _sslSocket.emplace(std::move(_socket), *_tl->_sslContext); auto handshakeCompleteCb = [ this, onComplete = std::move(onComplete) ]( diff --git a/src/mongo/util/net/message_port.cpp b/src/mongo/util/net/message_port.cpp index 17fb8a30497..e5bc637af4d 100644 --- a/src/mongo/util/net/message_port.cpp +++ b/src/mongo/util/net/message_port.cpp @@ -126,6 +126,18 @@ bool MessagingPort::recv(Message& m) { uassert(17132, "SSL handshake received but server is started without SSL support", sslGlobalParams.sslMode.load() != SSLParams::SSLMode_disabled); + + auto tlsAlert = checkTLSRequest( + ConstDataRange(reinterpret_cast(&header), sizeof(header))); + + if (tlsAlert) { + _psock->send(reinterpret_cast(tlsAlert->data()), + tlsAlert->size(), + "tls protocol mismatch"); + log() << "SSL handshake failed, as client requested disabled protocol"; + return false; + } + setX509PeerInfo( _psock->doSSLHandshake(reinterpret_cast(&header), sizeof(header))); LOG(1) << "new ssl connection, SNI server name [" << _psock->getSNIServerName() diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp index c1c68e31926..c7a023a7753 100644 --- a/src/mongo/util/net/ssl_manager.cpp +++ b/src/mongo/util/net/ssl_manager.cpp @@ -422,7 +422,7 @@ private: * Given an error code from an SSL-type IO function, logs an * appropriate message and throws a SocketException. */ - MONGO_COMPILER_NORETURN void _handleSSLError(int code, int ret); + MONGO_COMPILER_NORETURN void _handleSSLError(SSLConnection* conn, int ret); /* * Init the SSL context using parameters provided in params. This SSL context will @@ -716,7 +716,7 @@ int SSLManager::SSL_read(SSLConnection* conn, void* buf, int num) { } while (!_doneWithSSLOp(conn, status)); if (status <= 0) - _handleSSLError(SSL_get_error(conn, status), status); + _handleSSLError(conn, status); return status; } @@ -727,7 +727,7 @@ int SSLManager::SSL_write(SSLConnection* conn, const void* buf, int num) { } while (!_doneWithSSLOp(conn, status)); if (status <= 0) - _handleSSLError(SSL_get_error(conn, status), status); + _handleSSLError(conn, status); return status; } @@ -750,7 +750,7 @@ int SSLManager::SSL_shutdown(SSLConnection* conn) { } while (!_doneWithSSLOp(conn, status)); if (status < 0) - _handleSSLError(SSL_get_error(conn, status), status); + _handleSSLError(conn, status); return status; } @@ -1293,14 +1293,14 @@ SSLConnection* SSLManager::connect(Socket* socket) { 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); + _handleSSLError(sslConn.get(), ret); do { ret = ::SSL_connect(sslConn->ssl); } while (!_doneWithSSLOp(sslConn.get(), ret)); if (ret != 1) - _handleSSLError(SSL_get_error(sslConn.get(), ret), ret); + _handleSSLError(sslConn.get(), ret); return sslConn.release(); } @@ -1315,7 +1315,7 @@ SSLConnection* SSLManager::accept(Socket* socket, const char* initialBytes, int } while (!_doneWithSSLOp(sslConn.get(), ret)); if (ret != 1) - _handleSSLError(SSL_get_error(sslConn.get(), ret), ret); + _handleSSLError(sslConn.get(), ret); return sslConn.release(); } @@ -1570,7 +1570,8 @@ std::string SSLManagerInterface::getSSLErrorMessage(int code) { return msg; } -void SSLManager::_handleSSLError(int code, int ret) { +void SSLManager::_handleSSLError(SSLConnection* conn, int ret) { + int code = SSL_get_error(conn, ret); int err = ERR_get_error(); switch (code) { @@ -1607,6 +1608,7 @@ void SSLManager::_handleSSLError(int code, int ret) { error() << "unrecognized SSL error"; break; } + _flushNetworkBIO(conn); throw SocketException(SocketException::CONNECT_ERROR, ""); } } // namespace mongo @@ -1630,6 +1632,144 @@ bool mongo::hostNameMatchForX509Certificates(std::string nameToMatch, std::strin } } +boost::optional> mongo::checkTLSRequest(ConstDataRange dataRange) { + // This method's caller should have read in at least one MSGHEADER::Value's worth of data. + // The fragment we are about to examine must be strictly smaller. + static const size_t sizeOfTLSFragmentToRead = 11; + invariant(dataRange.length() >= sizeOfTLSFragmentToRead); + + static_assert(sizeOfTLSFragmentToRead < sizeof(MSGHEADER::Value), + "checkTLSRequest's caller read a MSGHEADER::Value, which must be larger than " + "message containing the TLS version"); + + ConstDataRangeCursor cdr(dataRange); + + /** + * The fragment we are to examine is a record, containing a handshake, containing a + * ClientHello. We wish to examine the advertised protocol version in the ClientHello. + * The following roughly describes the contents of these structures. Note that we do not + * need, or wish to, examine the entire ClientHello, we're looking exclusively for the + * client_version. + * + * Below is a rough description of the payload we will be examining. We shall perform some + * basic checks to ensure the payload matches these expectations. If it does not, we should + * bail out, and not emit protocol version alerts. + * + * enum {alert(21), handshake(22)} ContentType; + * TLSPlaintext { + * ContentType type = handshake(22), + * ProtocolVersion version; // Irrelevant. Clients send the real version in ClientHello. + * uint16 length; + * fragment, see Handshake stuct for contents + * ... + * } + * + * enum {client_hello(1)} HandshakeType; + * Handshake { + * HandshakeType msg_type = client_hello(1); + * uint24_t length; + * ClientHello body; + * } + * + * ClientHello { + * ProtocolVersion client_version; // <- This is the value we want to extract. + * } + */ + + static const std::uint8_t ContentType_handshake = 22; + static const std::uint8_t HandshakeType_client_hello = 1; + + using ProtocolVersion = std::array; + static const ProtocolVersion tls10VersionBytes{3, 1}; + static const ProtocolVersion tls11VersionBytes{3, 2}; + + // Parse the record header. + // Extract the ContentType from the header, and ensure it is a handshake. + StatusWith record_ContentType = cdr.readAndAdvance(); + if (!record_ContentType.isOK() || record_ContentType.getValue() != ContentType_handshake) { + return boost::none; + } + // Skip the record's ProtocolVersion. Clients tend to send TLS 1.0 in + // the record, but then their real protocol version in the enclosed ClientHello. + StatusWith record_protocol_version = cdr.readAndAdvance(); + if (!record_protocol_version.isOK()) { + return boost::none; + } + // Parse the record length. It should be be larger than the remaining expected payload. + auto record_length = cdr.readAndAdvance>(); + if (!record_length.isOK() || record_length.getValue() < cdr.length()) { + return boost::none; + } + + // Parse the handshake header. + // Extract the HandshakeType, and ensure it is a ClientHello. + StatusWith handshake_type = cdr.readAndAdvance(); + if (!handshake_type.isOK() || handshake_type.getValue() != HandshakeType_client_hello) { + return boost::none; + } + // Extract the handshake length, and ensure it is larger than the remaining expected + // payload. This requires a little work because the packet represents it with a uint24_t. + StatusWith> handshake_length_bytes = + cdr.readAndAdvance>(); + if (!handshake_length_bytes.isOK()) { + return boost::none; + } + std::uint32_t handshake_length = 0; + for (std::uint8_t handshake_byte : handshake_length_bytes.getValue()) { + handshake_length <<= 8; + handshake_length |= handshake_byte; + } + if (handshake_length < cdr.length()) { + return boost::none; + } + StatusWith client_version = cdr.readAndAdvance(); + if (!client_version.isOK()) { + return boost::none; + } + + // Invariant: We read exactly as much data as expected. + invariant(cdr.data() - dataRange.data() == sizeOfTLSFragmentToRead); + + auto isProtocolDisabled = [](SSLParams::Protocols protocol) { + const auto& params = getSSLGlobalParams(); + return std::find(params.sslDisabledProtocols.begin(), + params.sslDisabledProtocols.end(), + protocol) != params.sslDisabledProtocols.end(); + }; + + auto makeTLSProtocolVersionAlert = + [](const std::array& versionBytes) -> std::array { + /** + * The structure for this alert packet is as follows: + * TLSPlaintext { + * ContentType type = alert(21); + * ProtocolVersion = versionBytes; + * uint16_t length = 2 + * fragment = AlertDescription { + * AlertLevel level = fatal(2); + * AlertDescription = protocol_version(70); + * } + * + */ + return std::array{ + 0x15, versionBytes[0], versionBytes[1], 0x00, 0x02, 0x02, 0x46}; + }; + + ProtocolVersion version = client_version.getValue(); + if (version == tls10VersionBytes && isProtocolDisabled(SSLParams::Protocols::TLS1_0)) { + return makeTLSProtocolVersionAlert(version); + } else if (client_version == tls11VersionBytes && + isProtocolDisabled(SSLParams::Protocols::TLS1_1)) { + return makeTLSProtocolVersionAlert(version); + } + // TLS1.2 cannot be distinguished from TLS1.3, just by looking at the ProtocolVersion bytes. + // TLS 1.3 compatible clients advertise a "supported_versions" extension, which we would + // have to extract here. + // Hopefully by the time this matters, OpenSSL will properly emit protocol_version alerts. + + return boost::none; +} + #else namespace mongo { diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h index 002a0dd6be7..943651a64c6 100644 --- a/src/mongo/util/net/ssl_manager.h +++ b/src/mongo/util/net/ssl_manager.h @@ -206,5 +206,12 @@ const SSLParams& getSSLGlobalParams(); * x.509 certificate. Matches a remote host name to an x.509 host name, including wildcards. */ bool hostNameMatchForX509Certificates(std::string nameToMatch, std::string certHostName); + +/** + * Peeks at a fragment of a client issued TLS handshake packet. Returns a TLS alert + * packet if the client has selected a protocol which has been disabled by the server. + */ +boost::optional> checkTLSRequest(ConstDataRange cdr); + } // namespace mongo #endif // #ifdef MONGO_CONFIG_SSL -- cgit v1.2.1