diff options
Diffstat (limited to 'libmproxy/protocol2/tls.py')
-rw-r--r-- | libmproxy/protocol2/tls.py | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/libmproxy/protocol2/tls.py b/libmproxy/protocol2/tls.py new file mode 100644 index 00000000..2362b2b2 --- /dev/null +++ b/libmproxy/protocol2/tls.py @@ -0,0 +1,203 @@ +from __future__ import (absolute_import, print_function, division) +import traceback +from netlib import tcp + +from ..exceptions import ProtocolException +from .layer import Layer, yield_from_callback +from .messages import Connect, Reconnect, ChangeServer +from .auto import AutoLayer + + +class TlsLayer(Layer): + def __init__(self, ctx, client_tls, server_tls): + super(TlsLayer, self).__init__(ctx) + self._client_tls = client_tls + self._server_tls = server_tls + self._connected = False + self.client_sni = None + self._sni_from_server_change = None + + def __call__(self): + """ + The strategy for establishing SSL is as follows: + First, we determine whether we need the server cert to establish ssl with the client. + If so, we first connect to the server and then to the client. + If not, we only connect to the client and do the server_ssl lazily on a Connect message. + + An additional complexity is that establish ssl with the server may require a SNI value from the client. + In an ideal world, we'd do the following: + 1. Start the SSL handshake with the client + 2. Check if the client sends a SNI. + 3. Pause the client handshake, establish SSL with the server. + 4. Finish the client handshake with the certificate from the server. + There's just one issue: We cannot get a callback from OpenSSL if the client doesn't send a SNI. :( + Thus, we resort to the following workaround when establishing SSL with the server: + 1. Try to establish SSL with the server without SNI. If this fails, we ignore it. + 2. Establish SSL with client. + - If there's a SNI callback, reconnect to the server with SNI. + - If not and the server connect failed, raise the original exception. + Further notes: + - OpenSSL 1.0.2 introduces a callback that would help here: + https://www.openssl.org/docs/ssl/SSL_CTX_set_cert_cb.html + - The original mitmproxy issue is https://github.com/mitmproxy/mitmproxy/issues/427 + """ + client_tls_requires_server_cert = ( + self._client_tls and self._server_tls and not self.config.no_upstream_cert + ) + lazy_server_tls = ( + self._server_tls and not client_tls_requires_server_cert + ) + + if client_tls_requires_server_cert: + for m in self._establish_tls_with_client_and_server(): + yield m + elif self._client_tls: + for m in self._establish_tls_with_client(): + yield m + + self.next_layer() + layer = AutoLayer(self) + for message in layer(): + if message != Connect or not self._connected: + yield message + if message == Connect: + if lazy_server_tls: + self._establish_tls_with_server() + if message == ChangeServer and message.depth == 1: + self._server_tls = message.server_tls + self._sni_from_server_change = message.sni + if message == Reconnect or message == ChangeServer: + if self._server_tls: + self._establish_tls_with_server() + + @property + def sni_for_upstream_connection(self): + if self._sni_from_server_change is False: + return None + else: + return self._sni_from_server_change or self.client_sni + + def _establish_tls_with_client_and_server(self): + """ + This function deals with the problem that the server may require a SNI value from the client. + """ + + # First, try to connect to the server. + yield Connect() + self._connected = True + server_err = None + try: + self._establish_tls_with_server() + except ProtocolException as e: + server_err = e + + for message in self._establish_tls_with_client(): + if message == Reconnect: + yield message + self._establish_tls_with_server() + else: + raise RuntimeError("Unexpected Message: %s" % message) + + if server_err and not self.client_sni: + raise server_err + + def handle_sni(self, connection): + """ + This callback gets called during the TLS handshake with the client. + The client has just sent the Sever Name Indication (SNI). + """ + try: + old_upstream_sni = self.sni_for_upstream_connection + + sn = connection.get_servername() + if not sn: + return + self.client_sni = sn.decode("utf8").encode("idna") + + if old_upstream_sni != self.sni_for_upstream_connection: + # Perform reconnect + if self._server_tls: + self.yield_from_callback(Reconnect()) + + if self.client_sni: + # Now, change client context to reflect possibly changed certificate: + cert, key, chain_file = self.find_cert() + new_context = self.client_conn.create_ssl_context( + cert, key, + method=self.config.openssl_method_client, + options=self.config.openssl_options_client, + cipher_list=self.config.ciphers_client, + dhparams=self.config.certstore.dhparams, + chain_file=chain_file + ) + connection.set_context(new_context) + # An unhandled exception in this method will core dump PyOpenSSL, so + # make dang sure it doesn't happen. + except: # pragma: no cover + self.log("Error in handle_sni:\r\n" + traceback.format_exc(), "error") + + @yield_from_callback + def _establish_tls_with_client(self): + self.log("Establish TLS with client", "debug") + cert, key, chain_file = self.find_cert() + try: + self.client_conn.convert_to_ssl( + cert, key, + method=self.config.openssl_method_client, + options=self.config.openssl_options_client, + handle_sni=self.handle_sni, + cipher_list=self.config.ciphers_client, + dhparams=self.config.certstore.dhparams, + chain_file=chain_file + ) + except tcp.NetLibError as e: + raise ProtocolException(repr(e), e) + + def _establish_tls_with_server(self): + self.log("Establish TLS with server", "debug") + try: + self.server_conn.establish_ssl( + self.config.clientcerts, + self.sni_for_upstream_connection, + method=self.config.openssl_method_server, + options=self.config.openssl_options_server, + verify_options=self.config.openssl_verification_mode_server, + ca_path=self.config.openssl_trusted_cadir_server, + ca_pemfile=self.config.openssl_trusted_ca_server, + cipher_list=self.config.ciphers_server, + ) + tls_cert_err = self.server_conn.ssl_verification_error + if tls_cert_err is not None: + self.log( + "TLS verification failed for upstream server at depth %s with error: %s" % + (tls_cert_err['depth'], tls_cert_err['errno']), + "error") + self.log("Ignoring server verification error, continuing with connection", "error") + except tcp.NetLibInvalidCertificateError as e: + tls_cert_err = self.server_conn.ssl_verification_error + self.log( + "TLS verification failed for upstream server at depth %s with error: %s" % + (tls_cert_err['depth'], tls_cert_err['errno']), + "error") + self.log("Aborting connection attempt", "error") + raise ProtocolException(repr(e), e) + except tcp.NetLibError as e: + raise ProtocolException(repr(e), e) + + def find_cert(self): + host = self.server_conn.address.host + sans = set() + # Incorporate upstream certificate + if self.server_conn.tls_established and (not self.config.no_upstream_cert): + upstream_cert = self.server_conn.cert + sans.update(upstream_cert.altnames) + if upstream_cert.cn: + sans.add(host) + host = upstream_cert.cn.decode("utf8").encode("idna") + # Also add SNI values. + if self.client_sni: + sans.add(self.client_sni) + if self._sni_from_server_change: + sans.add(self._sni_from_server_change) + + return self.config.certstore.get_cert(host, list(sans)) |