diff options
author | Maximilian Hils <git@maximilianhils.com> | 2017-09-04 17:32:49 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-04 17:32:49 +0200 |
commit | 68fab8bd92c87f5c91f94c8837477418d5b5ea3e (patch) | |
tree | f240abdb8032eb47c7471522cdfca428d1b1ca9c | |
parent | 96854cff528ebc9ef2576b3d55f712f28626ff84 (diff) | |
parent | de006ea8adc08b9a8a6aa94eda2b30468727c307 (diff) | |
download | mitmproxy-68fab8bd92c87f5c91f94c8837477418d5b5ea3e.tar.gz mitmproxy-68fab8bd92c87f5c91f94c8837477418d5b5ea3e.tar.bz2 mitmproxy-68fab8bd92c87f5c91f94c8837477418d5b5ea3e.zip |
Merge pull request #2560 from mhils/mitmproxy-net-tls
Split TLS parts from net.tcp into net.tls
-rw-r--r-- | mitmproxy/net/tcp.py | 323 | ||||
-rw-r--r-- | mitmproxy/net/tls.py | 340 | ||||
-rw-r--r-- | mitmproxy/options.py | 6 | ||||
-rw-r--r-- | mitmproxy/proxy/config.py | 6 | ||||
-rw-r--r-- | mitmproxy/proxy/protocol/tls.py | 2 | ||||
-rw-r--r-- | pathod/pathoc.py | 6 | ||||
-rw-r--r-- | pathod/pathoc_cmdline.py | 6 | ||||
-rw-r--r-- | pathod/pathod.py | 6 | ||||
-rw-r--r-- | pathod/pathod_cmdline.py | 6 | ||||
-rw-r--r-- | setup.cfg | 1 | ||||
-rw-r--r-- | test/mitmproxy/net/test_tcp.py | 84 | ||||
-rw-r--r-- | test/mitmproxy/net/test_tls.py | 55 | ||||
-rw-r--r-- | test/mitmproxy/proxy/test_server.py | 2 |
13 files changed, 447 insertions, 396 deletions
diff --git a/mitmproxy/net/tcp.py b/mitmproxy/net/tcp.py index e109236e..47c80e80 100644 --- a/mitmproxy/net/tcp.py +++ b/mitmproxy/net/tcp.py @@ -5,15 +5,11 @@ import sys import threading import time import traceback -import binascii -from ssl import match_hostname -from ssl import CertificateError from typing import Optional # noqa -from mitmproxy.utils import strutils +from mitmproxy.net import tls -import certifi from OpenSSL import SSL from mitmproxy import certs @@ -28,90 +24,6 @@ IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) EINTR = 4 -# To enable all SSL methods use: SSLv23 -# then add options to disable certain methods -# https://bugs.launchpad.net/pyopenssl/+bug/1020632/comments/3 -SSL_BASIC_OPTIONS = ( - SSL.OP_CIPHER_SERVER_PREFERENCE -) -if hasattr(SSL, "OP_NO_COMPRESSION"): - SSL_BASIC_OPTIONS |= SSL.OP_NO_COMPRESSION - -SSL_DEFAULT_METHOD = SSL.SSLv23_METHOD -SSL_DEFAULT_OPTIONS = ( - SSL.OP_NO_SSLv2 | - SSL.OP_NO_SSLv3 | - SSL_BASIC_OPTIONS -) -if hasattr(SSL, "OP_NO_COMPRESSION"): - SSL_DEFAULT_OPTIONS |= SSL.OP_NO_COMPRESSION - -""" -Map a reasonable SSL version specification into the format OpenSSL expects. -Don't ask... -https://bugs.launchpad.net/pyopenssl/+bug/1020632/comments/3 -""" -sslversion_choices = { - "all": (SSL.SSLv23_METHOD, SSL_BASIC_OPTIONS), - # SSLv23_METHOD + NO_SSLv2 + NO_SSLv3 == TLS 1.0+ - # TLSv1_METHOD would be TLS 1.0 only - "secure": (SSL.SSLv23_METHOD, (SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 | SSL_BASIC_OPTIONS)), - "SSLv2": (SSL.SSLv2_METHOD, SSL_BASIC_OPTIONS), - "SSLv3": (SSL.SSLv3_METHOD, SSL_BASIC_OPTIONS), - "TLSv1": (SSL.TLSv1_METHOD, SSL_BASIC_OPTIONS), - "TLSv1_1": (SSL.TLSv1_1_METHOD, SSL_BASIC_OPTIONS), - "TLSv1_2": (SSL.TLSv1_2_METHOD, SSL_BASIC_OPTIONS), -} - -ssl_method_names = { - SSL.SSLv2_METHOD: "SSLv2", - SSL.SSLv3_METHOD: "SSLv3", - SSL.SSLv23_METHOD: "SSLv23", - SSL.TLSv1_METHOD: "TLSv1", - SSL.TLSv1_1_METHOD: "TLSv1.1", - SSL.TLSv1_2_METHOD: "TLSv1.2", -} - - -class SSLKeyLogger: - - def __init__(self, filename): - self.filename = filename - self.f = None - self.lock = threading.Lock() - - # required for functools.wraps, which pyOpenSSL uses. - __name__ = "SSLKeyLogger" - - def __call__(self, connection, where, ret): - if where == SSL.SSL_CB_HANDSHAKE_DONE and ret == 1: - with self.lock: - if not self.f: - d = os.path.dirname(self.filename) - if not os.path.isdir(d): - os.makedirs(d) - self.f = open(self.filename, "ab") - self.f.write(b"\r\n") - client_random = binascii.hexlify(connection.client_random()) - masterkey = binascii.hexlify(connection.master_key()) - self.f.write(b"CLIENT_RANDOM %s %s\r\n" % (client_random, masterkey)) - self.f.flush() - - def close(self): - with self.lock: - if self.f: - self.f.close() - - @staticmethod - def create_logfun(filename): - if filename: - return SSLKeyLogger(filename) - return False - - -log_ssl_key = SSLKeyLogger.create_logfun( - os.getenv("MITMPROXY_SSLKEYLOGFILE") or os.getenv("SSLKEYLOGFILE")) - class _FileLike: BLOCKSIZE = 1024 * 32 @@ -422,107 +334,6 @@ class _Connection: except SSL.Error: pass - def _create_ssl_context(self, - method=SSL_DEFAULT_METHOD, - options=SSL_DEFAULT_OPTIONS, - verify_options=SSL.VERIFY_NONE, - ca_path=None, - ca_pemfile=None, - cipher_list=None, - alpn_protos=None, - alpn_select=None, - alpn_select_callback=None, - sni=None, - ): - """ - Creates an SSL Context. - - :param method: One of SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD, TLSv1_1_METHOD, or TLSv1_2_METHOD - :param options: A bit field consisting of OpenSSL.SSL.OP_* values - :param verify_options: A bit field consisting of OpenSSL.SSL.VERIFY_* values - :param ca_path: Path to a directory of trusted CA certificates prepared using the c_rehash tool - :param ca_pemfile: Path to a PEM formatted trusted CA certificate - :param cipher_list: A textual OpenSSL cipher list, see https://www.openssl.org/docs/apps/ciphers.html - :rtype : SSL.Context - """ - try: - context = SSL.Context(method) - except ValueError as e: - method_name = ssl_method_names.get(method, "unknown") - raise exceptions.TlsException( - "SSL method \"%s\" is most likely not supported " - "or disabled (for security reasons) in your libssl. " - "Please refer to https://github.com/mitmproxy/mitmproxy/issues/1101 " - "for more details." % method_name - ) - - # Options (NO_SSLv2/3) - if options is not None: - context.set_options(options) - - # Verify Options (NONE/PEER and trusted CAs) - if verify_options is not None: - def verify_cert(conn, x509, errno, err_depth, is_cert_verified): - if not is_cert_verified: - self.ssl_verification_error = exceptions.InvalidCertificateException( - "Certificate Verification Error for {}: {} (errno: {}, depth: {})".format( - sni, - strutils.always_str(SSL._ffi.string(SSL._lib.X509_verify_cert_error_string(errno)), "utf8"), - errno, - err_depth - ) - ) - return is_cert_verified - - context.set_verify(verify_options, verify_cert) - if ca_path is None and ca_pemfile is None: - ca_pemfile = certifi.where() - try: - context.load_verify_locations(ca_pemfile, ca_path) - except SSL.Error: - raise exceptions.TlsException( - "Cannot load trusted certificates ({}, {}).".format( - ca_pemfile, ca_path - ) - ) - - # Workaround for - # https://github.com/pyca/pyopenssl/issues/190 - # https://github.com/mitmproxy/mitmproxy/issues/472 - # Options already set before are not cleared. - context.set_mode(SSL._lib.SSL_MODE_AUTO_RETRY) - - # Cipher List - if cipher_list: - try: - context.set_cipher_list(cipher_list.encode()) - except SSL.Error as v: - raise exceptions.TlsException("SSL cipher specification error: %s" % str(v)) - - # SSLKEYLOGFILE - if log_ssl_key: - context.set_info_callback(log_ssl_key) - - if alpn_protos is not None: - # advertise application layer protocols - context.set_alpn_protos(alpn_protos) - elif alpn_select is not None and alpn_select_callback is None: - # select application layer protocol - def alpn_select_callback(conn_, options): - if alpn_select in options: - return bytes(alpn_select) - else: # pragma: no cover - return options[0] - context.set_alpn_select_callback(alpn_select_callback) - elif alpn_select_callback is not None and alpn_select is None: - if not callable(alpn_select_callback): - raise exceptions.TlsException("ALPN error: alpn_select_callback must be a function.") - context.set_alpn_select_callback(alpn_select_callback) - elif alpn_select_callback is not None and alpn_select is not None: - raise exceptions.TlsException("ALPN error: only define alpn_select (string) OR alpn_select_callback (function).") - - return context - class ConnectionCloser: def __init__(self, conn): @@ -552,10 +363,13 @@ class TCPClient(_Connection): self.source_address = source_address self.cert = None self.server_certs = [] - self.ssl_verification_error = None # type: Optional[exceptions.InvalidCertificateException] self.sni = None self.spoof_source_address = spoof_source_address + @property + def ssl_verification_error(self) -> Optional[exceptions.InvalidCertificateException]: + return getattr(self.connection, "cert_error", None) + def close(self): # Make sure to close the real socket, not the SSL proxy. # OpenSSL is really good at screwing up, i.e. when trying to recv from a failed connection, @@ -567,33 +381,8 @@ class TCPClient(_Connection): else: close_socket(self.connection) - def create_ssl_context(self, cert=None, alpn_protos=None, **sslctx_kwargs): - context = self._create_ssl_context( - alpn_protos=alpn_protos, - **sslctx_kwargs) - # Client Certs - if cert: - try: - context.use_privatekey_file(cert) - context.use_certificate_file(cert) - except SSL.Error as v: - raise exceptions.TlsException("SSL client certificate error: %s" % str(v)) - return context - def convert_to_ssl(self, sni=None, alpn_protos=None, **sslctx_kwargs): - """ - cert: Path to a file containing both client cert and private key. - - options: A bit field consisting of OpenSSL.SSL.OP_* values - verify_options: A bit field consisting of OpenSSL.SSL.VERIFY_* values - ca_path: Path to a directory of trusted CA certificates prepared using the c_rehash tool - ca_pemfile: Path to a PEM formatted trusted CA certificate - """ - verification_mode = sslctx_kwargs.get('verify_options', None) - if verification_mode == SSL.VERIFY_PEER and not sni: - raise exceptions.TlsException("Cannot validate certificate hostname without SNI") - - context = self.create_ssl_context( + context = tls.create_client_context( alpn_protos=alpn_protos, sni=sni, **sslctx_kwargs @@ -617,33 +406,6 @@ class TCPClient(_Connection): for i in self.connection.get_peer_cert_chain(): self.server_certs.append(certs.SSLCert(i)) - # Validate TLS Hostname - try: - crt = dict( - subjectAltName=[("DNS", x.decode("ascii", "strict")) for x in self.cert.altnames] - ) - if self.cert.cn: - crt["subject"] = [[["commonName", self.cert.cn.decode("ascii", "strict")]]] - if sni: - # SNI hostnames allow support of IDN by using ASCII-Compatible Encoding - # Conversion algorithm is in RFC 3490 which is implemented by idna codec - # https://docs.python.org/3/library/codecs.html#text-encodings - # https://tools.ietf.org/html/rfc6066#section-3 - # https://tools.ietf.org/html/rfc4985#section-3 - hostname = sni.encode("idna").decode("ascii") - else: - hostname = "no-hostname" - match_hostname(crt, hostname) - except (ValueError, CertificateError) as e: - self.ssl_verification_error = exceptions.InvalidCertificateException( - "Certificate Verification Error for {}: {}".format( - sni or repr(self.address), - str(e) - ) - ) - if verification_mode == SSL.VERIFY_PEER: - raise self.ssl_verification_error - self.ssl_established = True self.rfile.set_descriptor(self.connection) self.wfile.set_descriptor(self.connection) @@ -729,77 +491,15 @@ class BaseHandler(_Connection): self.server = server self.clientcert = None - def create_ssl_context(self, - cert, key, - handle_sni=None, - request_client_cert=None, - chain_file=None, - dhparams=None, - extra_chain_certs=None, - **sslctx_kwargs): - """ - cert: A certs.SSLCert object or the path to a certificate - chain file. - - handle_sni: SNI handler, should take a connection object. Server - name can be retrieved like this: - - connection.get_servername() - - And you can specify the connection keys as follows: - - new_context = Context(TLSv1_METHOD) - new_context.use_privatekey(key) - new_context.use_certificate(cert) - connection.set_context(new_context) - - The request_client_cert argument requires some explanation. We're - supposed to be able to do this with no negative effects - if the - client has no cert to present, we're notified and proceed as usual. - Unfortunately, Android seems to have a bug (tested on 4.2.2) - when - an Android client is asked to present a certificate it does not - have, it hangs up, which is frankly bogus. Some time down the track - we may be able to make the proper behaviour the default again, but - until then we're conservative. - """ - - context = self._create_ssl_context(ca_pemfile=chain_file, **sslctx_kwargs) - - context.use_privatekey(key) - if isinstance(cert, certs.SSLCert): - context.use_certificate(cert.x509) - else: - context.use_certificate_chain_file(cert) - - if extra_chain_certs: - for i in extra_chain_certs: - context.add_extra_chain_cert(i.x509) - - if handle_sni: - # SNI callback happens during do_handshake() - context.set_tlsext_servername_callback(handle_sni) - - if request_client_cert: - def save_cert(conn_, cert, errno_, depth_, preverify_ok_): - self.clientcert = certs.SSLCert(cert) - # Return true to prevent cert verification error - return True - context.set_verify(SSL.VERIFY_PEER, save_cert) - - if dhparams: - SSL._lib.SSL_CTX_set_tmp_dh(context._context, dhparams) - - return context - def convert_to_ssl(self, cert, key, **sslctx_kwargs): """ Convert connection to SSL. - For a list of parameters, see BaseHandler._create_ssl_context(...) + For a list of parameters, see tls.create_server_context(...) """ - context = self.create_ssl_context( - cert, - key, + context = tls.create_server_context( + cert=cert, + key=key, **sslctx_kwargs) self.connection = SSL.Connection(context, self.connection) self.connection.set_accept_state() @@ -808,6 +508,9 @@ class BaseHandler(_Connection): except SSL.Error as v: raise exceptions.TlsException("SSL handshake error: %s" % repr(v)) self.ssl_established = True + cert = self.connection.get_peer_certificate() + if cert: + self.clientcert = certs.SSLCert(cert) self.rfile.set_descriptor(self.connection) self.wfile.set_descriptor(self.connection) diff --git a/mitmproxy/net/tls.py b/mitmproxy/net/tls.py new file mode 100644 index 00000000..74911f1e --- /dev/null +++ b/mitmproxy/net/tls.py @@ -0,0 +1,340 @@ +# To enable all SSL methods use: SSLv23 +# then add options to disable certain methods +# https://bugs.launchpad.net/pyopenssl/+bug/1020632/comments/3 +import binascii +import os +import threading +import typing +from ssl import match_hostname, CertificateError + +import certifi +from OpenSSL import SSL + +from mitmproxy import exceptions, certs + +BASIC_OPTIONS = ( + SSL.OP_CIPHER_SERVER_PREFERENCE +) +if hasattr(SSL, "OP_NO_COMPRESSION"): + BASIC_OPTIONS |= SSL.OP_NO_COMPRESSION + +DEFAULT_METHOD = SSL.SSLv23_METHOD +DEFAULT_OPTIONS = ( + SSL.OP_NO_SSLv2 | + SSL.OP_NO_SSLv3 | + BASIC_OPTIONS +) + +""" +Map a reasonable SSL version specification into the format OpenSSL expects. +Don't ask... +https://bugs.launchpad.net/pyopenssl/+bug/1020632/comments/3 +""" +VERSION_CHOICES = { + "all": (SSL.SSLv23_METHOD, BASIC_OPTIONS), + # SSLv23_METHOD + NO_SSLv2 + NO_SSLv3 == TLS 1.0+ + # TLSv1_METHOD would be TLS 1.0 only + "secure": (DEFAULT_METHOD, DEFAULT_OPTIONS), + "SSLv2": (SSL.SSLv2_METHOD, BASIC_OPTIONS), + "SSLv3": (SSL.SSLv3_METHOD, BASIC_OPTIONS), + "TLSv1": (SSL.TLSv1_METHOD, BASIC_OPTIONS), + "TLSv1_1": (SSL.TLSv1_1_METHOD, BASIC_OPTIONS), + "TLSv1_2": (SSL.TLSv1_2_METHOD, BASIC_OPTIONS), +} + +METHOD_NAMES = { + SSL.SSLv2_METHOD: "SSLv2", + SSL.SSLv3_METHOD: "SSLv3", + SSL.SSLv23_METHOD: "SSLv23", + SSL.TLSv1_METHOD: "TLSv1", + SSL.TLSv1_1_METHOD: "TLSv1.1", + SSL.TLSv1_2_METHOD: "TLSv1.2", +} + + +class MasterSecretLogger: + def __init__(self, filename): + self.filename = filename + self.f = None + self.lock = threading.Lock() + + # required for functools.wraps, which pyOpenSSL uses. + __name__ = "MasterSecretLogger" + + def __call__(self, connection, where, ret): + if where == SSL.SSL_CB_HANDSHAKE_DONE and ret == 1: + with self.lock: + if not self.f: + d = os.path.dirname(self.filename) + if not os.path.isdir(d): + os.makedirs(d) + self.f = open(self.filename, "ab") + self.f.write(b"\r\n") + client_random = binascii.hexlify(connection.client_random()) + masterkey = binascii.hexlify(connection.master_key()) + self.f.write(b"CLIENT_RANDOM %s %s\r\n" % (client_random, masterkey)) + self.f.flush() + + def close(self): + with self.lock: + if self.f: + self.f.close() + + @staticmethod + def create_logfun(filename): + if filename: + return MasterSecretLogger(filename) + return None + + +log_master_secret = MasterSecretLogger.create_logfun( + os.getenv("MITMPROXY_SSLKEYLOGFILE") or os.getenv("SSLKEYLOGFILE") +) + + +def _create_ssl_context( + method: int = DEFAULT_METHOD, + options: int = DEFAULT_OPTIONS, + ca_path: str = None, + ca_pemfile: str = None, + cipher_list: str = None, + alpn_protos: typing.Iterable[bytes] = None, + alpn_select=None, + alpn_select_callback: typing.Callable[[typing.Any, typing.Any], bytes] = None, + verify: int = SSL.VERIFY_PEER, + verify_callback: typing.Optional[ + typing.Callable[[SSL.Connection, SSL.X509, int, int, bool], bool] + ] = None, +) -> SSL.Context: + """ + Creates an SSL Context. + + :param method: One of SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD, TLSv1_1_METHOD, or TLSv1_2_METHOD + :param options: A bit field consisting of OpenSSL.SSL.OP_* values + :param verify: A bit field consisting of OpenSSL.SSL.VERIFY_* values + :param ca_path: Path to a directory of trusted CA certificates prepared using the c_rehash tool + :param ca_pemfile: Path to a PEM formatted trusted CA certificate + :param cipher_list: A textual OpenSSL cipher list, see https://www.openssl.org/docs/apps/ciphers.html + :rtype : SSL.Context + """ + try: + context = SSL.Context(method) + except ValueError: + method_name = METHOD_NAMES.get(method, "unknown") + raise exceptions.TlsException( + "SSL method \"%s\" is most likely not supported " + "or disabled (for security reasons) in your libssl. " + "Please refer to https://github.com/mitmproxy/mitmproxy/issues/1101 " + "for more details." % method_name + ) + + # Options (NO_SSLv2/3) + if options is not None: + context.set_options(options) + + # Verify Options (NONE/PEER and trusted CAs) + if verify is not None: + context.set_verify(verify, verify_callback) + if ca_path is None and ca_pemfile is None: + ca_pemfile = certifi.where() + try: + context.load_verify_locations(ca_pemfile, ca_path) + except SSL.Error: + raise exceptions.TlsException( + "Cannot load trusted certificates ({}, {}).".format( + ca_pemfile, ca_path + ) + ) + + # Workaround for + # https://github.com/pyca/pyopenssl/issues/190 + # https://github.com/mitmproxy/mitmproxy/issues/472 + # Options already set before are not cleared. + context.set_mode(SSL._lib.SSL_MODE_AUTO_RETRY) + + # Cipher List + if cipher_list: + try: + context.set_cipher_list(cipher_list.encode()) + except SSL.Error as v: + raise exceptions.TlsException("SSL cipher specification error: %s" % str(v)) + + # SSLKEYLOGFILE + if log_master_secret: + context.set_info_callback(log_master_secret) + + if alpn_protos is not None: + # advertise application layer protocols + context.set_alpn_protos(alpn_protos) + elif alpn_select is not None and alpn_select_callback is None: + # select application layer protocol + def alpn_select_callback(conn_, options): + if alpn_select in options: + return bytes(alpn_select) + else: # pragma: no cover + return options[0] + + context.set_alpn_select_callback(alpn_select_callback) + elif alpn_select_callback is not None and alpn_select is None: + if not callable(alpn_select_callback): + raise exceptions.TlsException("ALPN error: alpn_select_callback must be a function.") + context.set_alpn_select_callback(alpn_select_callback) + elif alpn_select_callback is not None and alpn_select is not None: + raise exceptions.TlsException( + "ALPN error: only define alpn_select (string) OR alpn_select_callback (function).") + + return context + + +def create_client_context( + cert: str = None, + sni: str = None, + address: str=None, + verify: int = SSL.VERIFY_NONE, + **sslctx_kwargs +) -> SSL.Context: + """ + Args: + cert: Path to a file containing both client cert and private key. + sni: Server Name Indication. Required for VERIFY_PEER + address: server address, used for expressive error messages only + verify: A bit field consisting of OpenSSL.SSL.VERIFY_* values + """ + + if sni is None and verify != SSL.VERIFY_NONE: + raise exceptions.TlsException("Cannot validate certificate hostname without SNI") + + def verify_callback( + conn: SSL.Connection, + x509: SSL.X509, + errno: int, + depth: int, + is_cert_verified: bool + ) -> bool: + if is_cert_verified and depth == 0: + # Verify hostname of leaf certificate. + cert = certs.SSLCert(x509) + try: + crt = dict( + subjectAltName=[("DNS", x.decode("ascii", "strict")) for x in cert.altnames] + ) # type: typing.Dict[str, typing.Any] + if cert.cn: + crt["subject"] = [[["commonName", cert.cn.decode("ascii", "strict")]]] + if sni: + # SNI hostnames allow support of IDN by using ASCII-Compatible Encoding + # Conversion algorithm is in RFC 3490 which is implemented by idna codec + # https://docs.python.org/3/library/codecs.html#text-encodings + # https://tools.ietf.org/html/rfc6066#section-3 + # https://tools.ietf.org/html/rfc4985#section-3 + hostname = sni.encode("idna").decode("ascii") + else: + hostname = "no-hostname" + match_hostname(crt, hostname) + except (ValueError, CertificateError) as e: + conn.cert_error = exceptions.InvalidCertificateException( + "Certificate verification error for {}: {}".format( + sni or repr(address), + str(e) + ) + ) + is_cert_verified = False + elif is_cert_verified: + pass + else: + conn.cert_error = exceptions.InvalidCertificateException( + "Certificate verification error for {}: {} (errno: {}, depth: {})".format( + sni, + SSL._ffi.string(SSL._lib.X509_verify_cert_error_string(errno)).decode(), + errno, + depth + ) + ) + + # SSL_VERIFY_NONE: The handshake will be continued regardless of the verification result. + return is_cert_verified + + context = _create_ssl_context( + verify=verify, + verify_callback=verify_callback, + **sslctx_kwargs, + ) + + # Client Certs + if cert: + try: + context.use_privatekey_file(cert) + context.use_certificate_file(cert) + except SSL.Error as v: + raise exceptions.TlsException("SSL client certificate error: %s" % str(v)) + return context + + +def create_server_context( + cert: typing.Union[certs.SSLCert, str], + key: SSL.PKey, + handle_sni: typing.Optional[typing.Callable[[SSL.Connection], None]] = None, + request_client_cert: bool = False, + chain_file=None, + dhparams=None, + extra_chain_certs: typing.Iterable[certs.SSLCert] = None, + **sslctx_kwargs +) -> SSL.Context: + """ + cert: A certs.SSLCert object or the path to a certificate + chain file. + + handle_sni: SNI handler, should take a connection object. Server + name can be retrieved like this: + + connection.get_servername() + + The request_client_cert argument requires some explanation. We're + supposed to be able to do this with no negative effects - if the + client has no cert to present, we're notified and proceed as usual. + Unfortunately, Android seems to have a bug (tested on 4.2.2) - when + an Android client is asked to present a certificate it does not + have, it hangs up, which is frankly bogus. Some time down the track + we may be able to make the proper behaviour the default again, but + until then we're conservative. + """ + + def accept_all( + conn_: SSL.Connection, + x509: SSL.X509, + errno: int, + err_depth: int, + is_cert_verified: bool, + ) -> bool: + # Return true to prevent cert verification error + return True + + if request_client_cert: + verify = SSL.VERIFY_PEER + else: + verify = SSL.VERIFY_NONE + + context = _create_ssl_context( + ca_pemfile=chain_file, + verify=verify, + verify_callback=accept_all, + **sslctx_kwargs, + ) + + context.use_privatekey(key) + if isinstance(cert, certs.SSLCert): + context.use_certificate(cert.x509) + else: + context.use_certificate_chain_file(cert) + + if extra_chain_certs: + for i in extra_chain_certs: + context.add_extra_chain_cert(i.x509) + + if handle_sni: + # SNI callback happens during do_handshake() + context.set_tlsext_servername_callback(handle_sni) + + if dhparams: + SSL._lib.SSL_CTX_set_tmp_dh(context._context, dhparams) + + return context diff --git a/mitmproxy/options.py b/mitmproxy/options.py index b008e588..ff7edf39 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -2,7 +2,7 @@ from typing import Optional, Sequence from mitmproxy import optmanager from mitmproxy import contentviews -from mitmproxy.net import tcp +from mitmproxy.net import tls log_verbosity = [ "error", @@ -408,7 +408,7 @@ class Options(optmanager.OptManager): Set supported SSL/TLS versions for client connections. SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+. """, - choices=list(tcp.sslversion_choices.keys()), + choices=list(tls.VERSION_CHOICES.keys()), ) self.add_option( "ssl_version_server", str, "secure", @@ -416,7 +416,7 @@ class Options(optmanager.OptManager): Set supported SSL/TLS versions for server connections. SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+. """, - choices=list(tcp.sslversion_choices.keys()), + choices=list(tls.VERSION_CHOICES.keys()), ) self.add_option( "ssl_insecure", bool, False, diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index 9458cd42..c15640d7 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -7,7 +7,7 @@ from OpenSSL import SSL, crypto from mitmproxy import exceptions from mitmproxy import options as moptions from mitmproxy import certs -from mitmproxy.net import tcp +from mitmproxy.net import tls from mitmproxy.net import server_spec CONF_BASENAME = "mitmproxy" @@ -65,9 +65,9 @@ class ProxyConfig: self.check_tcp = HostMatcher(options.tcp_hosts) self.openssl_method_client, self.openssl_options_client = \ - tcp.sslversion_choices[options.ssl_version_client] + tls.VERSION_CHOICES[options.ssl_version_client] self.openssl_method_server, self.openssl_options_server = \ - tcp.sslversion_choices[options.ssl_version_server] + tls.VERSION_CHOICES[options.ssl_version_server] certstore_path = os.path.expanduser(options.cadir) if not os.path.exists(os.path.dirname(certstore_path)): diff --git a/mitmproxy/proxy/protocol/tls.py b/mitmproxy/proxy/protocol/tls.py index 10eea4ae..21bf1417 100644 --- a/mitmproxy/proxy/protocol/tls.py +++ b/mitmproxy/proxy/protocol/tls.py @@ -548,7 +548,7 @@ class TlsLayer(base.Layer): self.server_sni, method=self.config.openssl_method_server, options=self.config.openssl_options_server, - verify_options=self.config.openssl_verification_mode_server, + verify=self.config.openssl_verification_mode_server, ca_path=self.config.options.ssl_verify_upstream_trusted_cadir, ca_pemfile=self.config.options.ssl_verify_upstream_trusted_ca, cipher_list=ciphers_server, diff --git a/pathod/pathoc.py b/pathod/pathoc.py index 63a15b55..e1052750 100644 --- a/pathod/pathoc.py +++ b/pathod/pathoc.py @@ -13,7 +13,7 @@ import logging from mitmproxy import certs from mitmproxy import exceptions -from mitmproxy.net import tcp +from mitmproxy.net import tcp, tls from mitmproxy.net import websockets from mitmproxy.net import socks from mitmproxy.net import http as net_http @@ -158,8 +158,8 @@ class Pathoc(tcp.TCPClient): # SSL ssl=None, sni=None, - ssl_version=tcp.SSL_DEFAULT_METHOD, - ssl_options=tcp.SSL_DEFAULT_OPTIONS, + ssl_version=tls.DEFAULT_METHOD, + ssl_options=tls.DEFAULT_OPTIONS, clientcert=None, ciphers=None, diff --git a/pathod/pathoc_cmdline.py b/pathod/pathoc_cmdline.py index 0854f6ad..e85d98a8 100644 --- a/pathod/pathoc_cmdline.py +++ b/pathod/pathoc_cmdline.py @@ -3,7 +3,7 @@ import argparse import os import os.path -from mitmproxy.net import tcp +from mitmproxy.net import tls from mitmproxy import version from mitmproxy.net.http import user_agents from . import pathoc, language @@ -111,7 +111,7 @@ def args_pathoc(argv, stdout=sys.stdout, stderr=sys.stderr): ) group.add_argument( "--ssl-version", dest="ssl_version", type=str, default="secure", - choices=tcp.sslversion_choices.keys(), + choices=tls.VERSION_CHOICES.keys(), help="Set supported SSL/TLS versions. " "SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+." ) @@ -162,7 +162,7 @@ def args_pathoc(argv, stdout=sys.stdout, stderr=sys.stderr): args = parser.parse_args(argv[1:]) - args.ssl_version, args.ssl_options = tcp.sslversion_choices[args.ssl_version] + args.ssl_version, args.ssl_options = tls.VERSION_CHOICES[args.ssl_version] args.port = None if ":" in args.host: diff --git a/pathod/pathod.py b/pathod/pathod.py index 7c773c3b..f8e64f9e 100644 --- a/pathod/pathod.py +++ b/pathod/pathod.py @@ -3,7 +3,7 @@ import logging import os import sys import threading -from mitmproxy.net import tcp +from mitmproxy.net import tcp, tls from mitmproxy import certs as mcerts from mitmproxy.net import websockets from mitmproxy import version @@ -37,8 +37,8 @@ class SSLOptions: sans=(), not_after_connect=None, request_client_cert=False, - ssl_version=tcp.SSL_DEFAULT_METHOD, - ssl_options=tcp.SSL_DEFAULT_OPTIONS, + ssl_version=tls.DEFAULT_METHOD, + ssl_options=tls.DEFAULT_OPTIONS, ciphers=None, certs=None, alpn_select=b'h2', diff --git a/pathod/pathod_cmdline.py b/pathod/pathod_cmdline.py index c646aaee..dfce7a52 100644 --- a/pathod/pathod_cmdline.py +++ b/pathod/pathod_cmdline.py @@ -4,7 +4,7 @@ import os import os.path import re -from mitmproxy.net import tcp +from mitmproxy.net import tls from mitmproxy.utils import human from mitmproxy import version from . import pathod @@ -143,7 +143,7 @@ def args_pathod(argv, stdout_=sys.stdout, stderr_=sys.stderr): ) group.add_argument( "--ssl-version", dest="ssl_version", type=str, default="secure", - choices=tcp.sslversion_choices.keys(), + choices=tls.VERSION_CHOICES.keys(), help="Set supported SSL/TLS versions. " "SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+." ) @@ -182,7 +182,7 @@ def args_pathod(argv, stdout_=sys.stdout, stderr_=sys.stderr): args = parser.parse_args(argv[1:]) - args.ssl_version, args.ssl_options = tcp.sslversion_choices[args.ssl_version] + args.ssl_version, args.ssl_options = tls.VERSION_CHOICES[args.ssl_version] certs = [] for i in args.ssl_certs: @@ -50,6 +50,7 @@ exclude = mitmproxy/net/http/multipart.py mitmproxy/net/http/url.py mitmproxy/net/tcp.py + mitmproxy/net/tls.py mitmproxy/options.py mitmproxy/proxy/config.py mitmproxy/proxy/modes/http_proxy.py diff --git a/test/mitmproxy/net/test_tcp.py b/test/mitmproxy/net/test_tcp.py index 3345840e..9d521533 100644 --- a/test/mitmproxy/net/test_tcp.py +++ b/test/mitmproxy/net/test_tcp.py @@ -206,7 +206,7 @@ class TestInvalidTrustFile(tservers.ServerTestBase): with pytest.raises(exceptions.TlsException): c.convert_to_ssl( sni="example.mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/generate.py") ) @@ -236,7 +236,7 @@ class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase): def test_mode_none_should_pass(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - c.convert_to_ssl(verify_options=SSL.VERIFY_NONE) + c.convert_to_ssl(verify=SSL.VERIFY_NONE) # Verification errors should be saved even if connection isn't aborted assert c.ssl_verification_error @@ -252,7 +252,7 @@ class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase): with pytest.raises(exceptions.InvalidCertificateException): c.convert_to_ssl( sni="example.mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/trusted-root.crt") ) @@ -276,17 +276,27 @@ class TestSSLUpstreamCertVerificationWBadHostname(tservers.ServerTestBase): with c.connect(): with pytest.raises(exceptions.TlsException): c.convert_to_ssl( - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/trusted-root.crt") ) + def test_mode_none_should_pass_without_sni(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl( + verify=SSL.VERIFY_NONE, + ca_path=tutils.test_data.path("mitmproxy/net/data/verificationcerts/") + ) + + assert "'no-hostname' doesn't match" in str(c.ssl_verification_error) + def test_should_fail(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): with pytest.raises(exceptions.InvalidCertificateException): c.convert_to_ssl( sni="mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/trusted-root.crt") ) assert c.ssl_verification_error @@ -305,7 +315,7 @@ class TestSSLUpstreamCertVerificationWValidCertChain(tservers.ServerTestBase): with c.connect(): c.convert_to_ssl( sni="example.mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("mitmproxy/net/data/verificationcerts/trusted-root.crt") ) @@ -321,7 +331,7 @@ class TestSSLUpstreamCertVerificationWValidCertChain(tservers.ServerTestBase): with c.connect(): c.convert_to_ssl( sni="example.mitmproxy.org", - verify_options=SSL.VERIFY_PEER, + verify=SSL.VERIFY_PEER, ca_path=tutils.test_data.path("mitmproxy/net/data/verificationcerts/") ) @@ -774,10 +784,7 @@ class TestPeek(tservers.ServerTestBase): c.close() with pytest.raises(exceptions.NetlibException): - if c.rfile.peek(1) == b"": - # Workaround for Python 2 on Unix: - # Peeking a closed connection does not raise an exception here. - raise exceptions.NetlibException() + c.rfile.peek(1) class TestPeekSSL(TestPeek): @@ -787,58 +794,3 @@ class TestPeekSSL(TestPeek): with c.connect() as conn: c.convert_to_ssl() return conn.pop() - - -class TestSSLKeyLogger(tservers.ServerTestBase): - handler = EchoHandler - ssl = dict( - cipher_list="AES256-SHA" - ) - - def test_log(self, tmpdir): - testval = b"echo!\n" - _logfun = tcp.log_ssl_key - - logfile = str(tmpdir.join("foo", "bar", "logfile")) - tcp.log_ssl_key = tcp.SSLKeyLogger(logfile) - - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - c.wfile.write(testval) - c.wfile.flush() - assert c.rfile.readline() == testval - c.finish() - - tcp.log_ssl_key.close() - with open(logfile, "rb") as f: - assert f.read().count(b"CLIENT_RANDOM") == 2 - - tcp.log_ssl_key = _logfun - - def test_create_logfun(self): - assert isinstance( - tcp.SSLKeyLogger.create_logfun("test"), - tcp.SSLKeyLogger) - assert not tcp.SSLKeyLogger.create_logfun(False) - - -class TestSSLInvalid(tservers.ServerTestBase): - handler = EchoHandler - ssl = True - - def test_invalid_ssl_method_should_fail(self): - fake_ssl_method = 100500 - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - with pytest.raises(exceptions.TlsException): - c.convert_to_ssl(method=fake_ssl_method) - - def test_alpn_error(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - with pytest.raises(exceptions.TlsException, match="must be a function"): - c.create_ssl_context(alpn_select_callback="foo") - - with pytest.raises(exceptions.TlsException, match="ALPN error"): - c.create_ssl_context(alpn_select="foo", alpn_select_callback="bar") diff --git a/test/mitmproxy/net/test_tls.py b/test/mitmproxy/net/test_tls.py new file mode 100644 index 00000000..d0583d34 --- /dev/null +++ b/test/mitmproxy/net/test_tls.py @@ -0,0 +1,55 @@ +import pytest + +from mitmproxy import exceptions +from mitmproxy.net import tls +from mitmproxy.net.tcp import TCPClient +from test.mitmproxy.net.test_tcp import EchoHandler +from . import tservers + + +class TestMasterSecretLogger(tservers.ServerTestBase): + handler = EchoHandler + ssl = dict( + cipher_list="AES256-SHA" + ) + + def test_log(self, tmpdir): + testval = b"echo!\n" + _logfun = tls.log_master_secret + + logfile = str(tmpdir.join("foo", "bar", "logfile")) + tls.log_master_secret = tls.MasterSecretLogger(logfile) + + c = TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl() + c.wfile.write(testval) + c.wfile.flush() + assert c.rfile.readline() == testval + c.finish() + + tls.log_master_secret.close() + with open(logfile, "rb") as f: + assert f.read().count(b"CLIENT_RANDOM") == 2 + + tls.log_master_secret = _logfun + + def test_create_logfun(self): + assert isinstance( + tls.MasterSecretLogger.create_logfun("test"), + tls.MasterSecretLogger) + assert not tls.MasterSecretLogger.create_logfun(False) + + +class TestTLSInvalid: + def test_invalid_ssl_method_should_fail(self): + fake_ssl_method = 100500 + with pytest.raises(exceptions.TlsException): + tls.create_client_context(method=fake_ssl_method) + + def test_alpn_error(self): + with pytest.raises(exceptions.TlsException, match="must be a function"): + tls.create_client_context(alpn_select_callback="foo") + + with pytest.raises(exceptions.TlsException, match="ALPN error"): + tls.create_client_context(alpn_select="foo", alpn_select_callback="bar") diff --git a/test/mitmproxy/proxy/test_server.py b/test/mitmproxy/proxy/test_server.py index 562f822c..affdf221 100644 --- a/test/mitmproxy/proxy/test_server.py +++ b/test/mitmproxy/proxy/test_server.py @@ -468,7 +468,7 @@ class TestHTTPSUpstreamServerVerificationWBadCert(tservers.HTTPProxyTest): self.options.ssl_insecure = False r = self._request() assert r.status_code == 502 - assert b"Certificate Verification Error" in r.raw_content + assert b"Certificate verification error" in r.raw_content class TestHTTPSNoCommonName(tservers.HTTPProxyTest): |