summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSpencer Jackson <spencer.jackson@mongodb.com>2018-02-15 15:30:46 -0500
committerSpencer Jackson <spencer.jackson@mongodb.com>2018-05-01 15:12:16 -0400
commit51af489a86f1862de87b51f26a9e818ec3b5df04 (patch)
treee894c8a4273268ace784e701b395e6bb01cdbd1e
parent11c54929c6106e7b347c879a6570f217c04bb338 (diff)
downloadmongo-51af489a86f1862de87b51f26a9e818ec3b5df04.tar.gz
SERVER-33329: Make server and shell emit TLS protocol_version alerts
-rw-r--r--jstests/ssl/ssl_alert_reporting.js59
-rw-r--r--src/mongo/transport/asio_utils.h147
-rw-r--r--src/mongo/transport/session_asio.h11
-rw-r--r--src/mongo/util/net/ssl_manager_openssl.cpp18
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