Perform RFC 2818 hostname verification in client SSL handshakes

If the verification mode is `system_ca`, clients will now do hostname
verification. Thus, only certificates from expected hostnames are
allowed when SSL is enabled. This can be overridden by forcible setting
the SSL mode to autodetect.

Clients will also send the hostname even when `system_ca` is not being
performed. This leaks possible metadata, but allows servers providing
multiple hostnames to respond with the correct certificate. One example
is cloudflare, which getmonero.org is currently using.
This commit is contained in:
Lee Clagett 2019-03-19 16:04:32 -04:00
parent 0416764cae
commit eca0fea45a
3 changed files with 26 additions and 5 deletions

View file

@ -174,7 +174,7 @@ namespace net_utils
// SSL Options // SSL Options
if (ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_enabled || ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_autodetect) if (ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_enabled || ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_autodetect)
{ {
if (!m_ssl_options.handshake(*m_ssl_socket, boost::asio::ssl::stream_base::client)) if (!m_ssl_options.handshake(*m_ssl_socket, boost::asio::ssl::stream_base::client, addr))
{ {
if (ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_autodetect) if (ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_autodetect)
{ {

View file

@ -108,9 +108,21 @@ namespace net_utils
then the handshake will not fail when peer verification fails. The then the handshake will not fail when peer verification fails. The
assumption is that a re-connect will be attempted, so a warning is assumption is that a re-connect will be attempted, so a warning is
logged instead of failure. logged instead of failure.
\note It is strongly encouraged that clients using `system_ca`
verification provide a non-empty `host` for rfc2818 verification.
\param socket Used in SSL handshake and verification
\param type Client or server
\param host This parameter is only used when
`type == client && !host.empty()`. The value is sent to the server for
situations where multiple hostnames are being handled by a server. If
`verification == system_ca` the client also does a rfc2818 check to
ensure that the server certificate is to the provided hostname.
\return True if the SSL handshake completes with peer verification \return True if the SSL handshake completes with peer verification
settings. */ settings. */
bool handshake(boost::asio::ssl::stream<boost::asio::ip::tcp::socket> &socket, boost::asio::ssl::stream_base::handshake_type type) const; bool handshake(boost::asio::ssl::stream<boost::asio::ip::tcp::socket> &socket, boost::asio::ssl::stream_base::handshake_type type, const std::string& host = {}) const;
}; };
// https://security.stackexchange.com/questions/34780/checking-client-hello-for-https-classification // https://security.stackexchange.com/questions/34780/checking-client-hello-for-https-classification

View file

@ -311,7 +311,7 @@ bool ssl_options_t::has_fingerprint(boost::asio::ssl::verify_context &ctx) const
return false; return false;
} }
bool ssl_options_t::handshake(boost::asio::ssl::stream<boost::asio::ip::tcp::socket> &socket, boost::asio::ssl::stream_base::handshake_type type) const bool ssl_options_t::handshake(boost::asio::ssl::stream<boost::asio::ip::tcp::socket> &socket, boost::asio::ssl::stream_base::handshake_type type, const std::string& host) const
{ {
socket.next_layer().set_option(boost::asio::ip::tcp::no_delay(true)); socket.next_layer().set_option(boost::asio::ip::tcp::no_delay(true));
@ -330,11 +330,20 @@ bool ssl_options_t::handshake(boost::asio::ssl::stream<boost::asio::ip::tcp::soc
else else
{ {
socket.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert); socket.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert);
socket.set_verify_callback([&](bool preverified, boost::asio::ssl::verify_context &ctx)
// in case server is doing "virtual" domains, set hostname
SSL* const ssl_ctx = socket.native_handle();
if (type == boost::asio::ssl::stream_base::client && !host.empty() && ssl_ctx)
SSL_set_tlsext_host_name(ssl_ctx, host.c_str());
socket.set_verify_callback([&](const bool preverified, boost::asio::ssl::verify_context &ctx)
{ {
// preverified means it passed system or user CA check. System CA is never loaded // preverified means it passed system or user CA check. System CA is never loaded
// when fingerprints are whitelisted. // when fingerprints are whitelisted.
if (!preverified && !has_fingerprint(ctx)) const bool verified = preverified &&
(verification != ssl_verification_t::system_ca || host.empty() || boost::asio::ssl::rfc2818_verification(host)(preverified, ctx));
if (!verified && !has_fingerprint(ctx))
{ {
// autodetect will reconnect without SSL - warn and keep connection encrypted // autodetect will reconnect without SSL - warn and keep connection encrypted
if (support != ssl_support_t::e_ssl_support_autodetect) if (support != ssl_support_t::e_ssl_support_autodetect)