diff options
author | Spencer Jackson <spencer.jackson@mongodb.com> | 2018-02-15 15:30:46 -0500 |
---|---|---|
committer | Spencer Jackson <spencer.jackson@mongodb.com> | 2018-05-01 15:12:16 -0400 |
commit | 51af489a86f1862de87b51f26a9e818ec3b5df04 (patch) | |
tree | e894c8a4273268ace784e701b395e6bb01cdbd1e | |
parent | 11c54929c6106e7b347c879a6570f217c04bb338 (diff) | |
download | mongo-51af489a86f1862de87b51f26a9e818ec3b5df04.tar.gz |
SERVER-33329: Make server and shell emit TLS protocol_version alerts
-rw-r--r-- | jstests/ssl/ssl_alert_reporting.js | 59 | ||||
-rw-r--r-- | src/mongo/transport/asio_utils.h | 147 | ||||
-rw-r--r-- | src/mongo/transport/session_asio.h | 11 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_openssl.cpp | 18 |
4 files changed, 227 insertions, 8 deletions
diff --git a/jstests/ssl/ssl_alert_reporting.js b/jstests/ssl/ssl_alert_reporting.js new file mode 100644 index 00000000000..da8b630bf0c --- /dev/null +++ b/jstests/ssl/ssl_alert_reporting.js @@ -0,0 +1,59 @@ +// Ensure that TLS version alerts are correctly propagated + +load('jstests/ssl/libs/ssl_helpers.js'); + +(function() { + 'use strict'; + + const clientOptions = [ + "--ssl", + "--sslPEMKeyFile", + "jstests/libs/client.pem", + "--sslCAFile", + "jstests/libs/ca.pem", + "--eval", + ";" + ]; + + function runTest(serverDisabledProtos, clientDisabledProtos) { + const implementation = determineSSLProvider(); + let expectedRegex; + if (implementation === "openssl") { + expectedRegex = + /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: SocketException: tlsv1 alert protocol version/; + } else if (implementation === "windows") { + expectedRegex = + /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: SocketException: The function requested is not supported/; + } else if (implementation === "apple") { + expectedRegex = + /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: SocketException: Secure.Transport: bad protocol version/; + } else { + throw Error("Unrecognized TLS implementation!"); + } + + var md = MongoRunner.runMongod({ + nopreallocj: "", + sslMode: "requireSSL", + sslCAFile: "jstests/libs/ca.pem", + sslPEMKeyFile: "jstests/libs/server.pem", + sslDisabledProtocols: serverDisabledProtos, + }); + + clearRawMongoProgramOutput(); + let shell = runMongoProgram("mongo", + "--port", + md.port, + ...clientOptions, + "--sslDisabledProtocols", + clientDisabledProtos); + let mongoOutput = rawMongoProgramOutput(); + assert(mongoOutput.match(expectedRegex), + "Mongo shell output was as follows:\n" + mongoOutput + "\n************"); + + MongoRunner.stopMongod(md); + } + + // Client recieves and reports a protocol version alert if it advertises a protocol older than + // the server's oldest supported protocol + runTest("TLS1_0", "TLS1_1,TLS1_2"); +}()); diff --git a/src/mongo/transport/asio_utils.h b/src/mongo/transport/asio_utils.h index be660d8b861..3d38694266f 100644 --- a/src/mongo/transport/asio_utils.h +++ b/src/mongo/transport/asio_utils.h @@ -30,6 +30,7 @@ #include "mongo/base/status.h" #include "mongo/base/system_error.h" +#include "mongo/config.h" #include "mongo/util/errno_util.h" #include "mongo/util/future.h" #include "mongo/util/net/hostandport.h" @@ -170,6 +171,152 @@ StatusWith<EventsMask> pollASIOSocket(Socket& socket, EventsMask mask, Milliseco } } +#ifdef MONGO_CONFIG_SSL +/** + * 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. + */ +template <typename Buffer> +boost::optional<std::array<std::uint8_t, 7>> checkTLSRequest(const Buffer& buffers) { + // 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(asio::buffer_size(buffers) >= sizeOfTLSFragmentToRead); + + static_assert(sizeOfTLSFragmentToRead < sizeof(MSGHEADER::Value), + "checkTLSRequest's caller read a MSGHEADER::Value, which must be larger than " + "message containing the TLS version"); + + /** + * 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<std::uint8_t, 2>; + static const ProtocolVersion tls10VersionBytes{3, 1}; + static const ProtocolVersion tls11VersionBytes{3, 2}; + + auto request = asio::buffer_cast<const char*>(buffers); + auto cdr = ConstDataRangeCursor(request, request + asio::buffer_size(buffers)); + + // Parse the record header. + // Extract the ContentType from the header, and ensure it is a handshake. + StatusWith<std::uint8_t> record_ContentType = cdr.readAndAdvance<std::uint8_t>(); + 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<ProtocolVersion> record_protocol_version = cdr.readAndAdvance<ProtocolVersion>(); + 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<BigEndian<std::uint16_t>>(); + 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<std::uint8_t> handshake_type = cdr.readAndAdvance<std::uint8_t>(); + 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<std::array<std::uint8_t, 3>> handshake_length_bytes = + cdr.readAndAdvance<std::array<std::uint8_t, 3>>(); + 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<ProtocolVersion> client_version = cdr.readAndAdvance<ProtocolVersion>(); + if (!client_version.isOK()) { + return boost::none; + } + + // Invariant: We read exactly as much data as expected. + invariant((cdr.data() - request) == 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<std::uint8_t, 2>& versionBytes) -> std::array<std::uint8_t, 7> { + /** + * 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<std::uint8_t, 7>{ + 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; +} +#endif + /** * Pass this to asio functions in place of a callback to have them return a Future<T>. This behaves * similarly to asio::use_future_t, however it returns a mongo::Future<T> rather than a diff --git a/src/mongo/transport/session_asio.h b/src/mongo/transport/session_asio.h index 459cab76676..ea10ec61af4 100644 --- a/src/mongo/transport/session_asio.h +++ b/src/mongo/transport/session_asio.h @@ -527,6 +527,17 @@ private: "SSL handshake received but server is started without SSL support")); } + auto tlsAlert = checkTLSRequest(buffer); + if (tlsAlert) { + return opportunisticWrite(getSocket(), + asio::buffer(tlsAlert->data(), tlsAlert->size())) + .then([] { + return Future<bool>::makeReady( + Status(ErrorCodes::SSLHandshakeFailed, + "SSL handshake failed, as client requested disabled protocol")); + }); + } + _sslSocket.emplace(std::move(_socket), *_tl->_ingressSSLContext, ""); auto doHandshake = [&] { if (_blockingMode == Sync) { diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp index daf847c796d..e13ce17067c 100644 --- a/src/mongo/util/net/ssl_manager_openssl.cpp +++ b/src/mongo/util/net/ssl_manager_openssl.cpp @@ -367,7 +367,7 @@ private: * 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); + MONGO_COMPILER_NORETURN void _handleSSLError(SSLConnectionOpenSSL* conn, int ret); /* * Init the SSL context using parameters provided in params. This SSL context will @@ -619,7 +619,7 @@ int SSLManagerOpenSSL::SSL_read(SSLConnectionInterface* connInterface, void* buf } while (!_doneWithSSLOp(conn, status)); if (status <= 0) - _handleSSLError(SSL_get_error(conn->ssl, status), status); + _handleSSLError(conn, status); return status; } @@ -631,7 +631,7 @@ int SSLManagerOpenSSL::SSL_write(SSLConnectionInterface* connInterface, const vo } while (!_doneWithSSLOp(conn, status)); if (status <= 0) - _handleSSLError(SSL_get_error(conn->ssl, status), status); + _handleSSLError(conn, status); return status; } @@ -643,7 +643,7 @@ int SSLManagerOpenSSL::SSL_shutdown(SSLConnectionInterface* connInterface) { } while (!_doneWithSSLOp(conn, status)); if (status < 0) - _handleSSLError(SSL_get_error(conn->ssl, status), status); + _handleSSLError(conn, status); return status; } @@ -1182,14 +1182,14 @@ SSLConnectionInterface* SSLManagerOpenSSL::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()->ssl, 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()->ssl, ret), ret); + _handleSSLError(sslConn.get(), ret); return sslConn.release(); } @@ -1206,7 +1206,7 @@ SSLConnectionInterface* SSLManagerOpenSSL::accept(Socket* socket, } while (!_doneWithSSLOp(sslConn.get(), ret)); if (ret != 1) - _handleSSLError(SSL_get_error(sslConn.get()->ssl, ret), ret); + _handleSSLError(sslConn.get(), ret); return sslConn.release(); } @@ -1371,7 +1371,8 @@ std::string SSLManagerInterface::getSSLErrorMessage(int code) { return msg; } -void SSLManagerOpenSSL::_handleSSLError(int code, int ret) { +void SSLManagerOpenSSL::_handleSSLError(SSLConnectionOpenSSL* conn, int ret) { + int code = SSL_get_error(conn->ssl, ret); int err = ERR_get_error(); switch (code) { @@ -1408,6 +1409,7 @@ void SSLManagerOpenSSL::_handleSSLError(int code, int ret) { error() << "unrecognized SSL error"; break; } + _flushNetworkBIO(conn); throwSocketError(SocketErrorKind::CONNECT_ERROR, ""); } } // namespace mongo |