aboutsummaryrefslogtreecommitdiffstats
path: root/netlib
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2015-11-01 18:15:30 +0100
committerMaximilian Hils <git@maximilianhils.com>2015-11-01 18:15:30 +0100
commit5af9df326aef1cf72be7fd5390df239fb6b906c7 (patch)
tree664d5346ef5f9de989b0e463f37c1605dcb0ffce /netlib
parentb4eb4eab92aa7fee0fb1c3aaaedad0d08d1c6c3b (diff)
downloadmitmproxy-5af9df326aef1cf72be7fd5390df239fb6b906c7.tar.gz
mitmproxy-5af9df326aef1cf72be7fd5390df239fb6b906c7.tar.bz2
mitmproxy-5af9df326aef1cf72be7fd5390df239fb6b906c7.zip
fix certificate verification
This commit fixes netlib's optional (turned off by default) certificate verification, which previously did not validate the cert's host name. As it turns out, verifying the connection's host name on an intercepting proxy is not really straightforward - if we receive a connection in transparent mode without SNI, we have no clue which hosts the client intends to connect to. There are two basic approaches to solve this problem: 1. Exactly mirror the host names presented by the server in the spoofed certificate presented to the client. 2. Require the client to send the TLS Server Name Indication extension. While this does not work with older clients, we can validate the hostname on the proxy. Approach 1 is problematic in mitmproxy's use case, as we may want to deliberately divert connections without the client's knowledge. As a consequence, we opt for approach 2. While mitmproxy does now require a SNI value to be sent by the client if certificate verification is turned on, we retain our ability to present certificates to the client which are accepted with a maximum likelihood.
Diffstat (limited to 'netlib')
-rw-r--r--netlib/certutils.py5
-rw-r--r--netlib/tcp.py37
2 files changed, 35 insertions, 7 deletions
diff --git a/netlib/certutils.py b/netlib/certutils.py
index b3ddcbe4..93366a99 100644
--- a/netlib/certutils.py
+++ b/netlib/certutils.py
@@ -437,6 +437,11 @@ class SSLCert(object):
@property
def altnames(self):
+ """
+ Returns:
+ All DNS altnames.
+ """
+ # tcp.TCPClient.convert_to_ssl assumes that this property only contains DNS altnames for hostname verification.
altnames = []
for i in range(self.x509.get_extension_count()):
ext = self.x509.get_extension(i)
diff --git a/netlib/tcp.py b/netlib/tcp.py
index b751d71f..33776fc4 100644
--- a/netlib/tcp.py
+++ b/netlib/tcp.py
@@ -11,6 +11,7 @@ import binascii
from six.moves import range
import certifi
+from backports import ssl_match_hostname
import six
import OpenSSL
from OpenSSL import SSL
@@ -597,9 +598,14 @@ class TCPClient(_Connection):
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 TlsException("Cannot validate certificate hostname without SNI")
+
context = self.create_ssl_context(
alpn_protos=alpn_protos,
- **sslctx_kwargs)
+ **sslctx_kwargs
+ )
self.connection = SSL.Connection(context, self.connection)
if sni:
self.sni = sni
@@ -612,15 +618,32 @@ class TCPClient(_Connection):
raise InvalidCertificateException("SSL handshake error: %s" % repr(v))
else:
raise TlsException("SSL handshake error: %s" % repr(v))
+ else:
+ # Fix for pre v1.0 OpenSSL, which doesn't throw an exception on
+ # certificate validation failure
+ if verification_mode == SSL.VERIFY_PEER and self.ssl_verification_error is not None:
+ raise InvalidCertificateException("SSL handshake error: certificate verify failed")
- # Fix for pre v1.0 OpenSSL, which doesn't throw an exception on
- # certificate validation failure
- verification_mode = sslctx_kwargs.get('verify_options', None)
- if self.ssl_verification_error is not None and verification_mode == SSL.VERIFY_PEER:
- raise InvalidCertificateException("SSL handshake error: certificate verify failed")
+ self.cert = certutils.SSLCert(self.connection.get_peer_certificate())
+
+ # 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:
+ hostname = sni.decode("ascii", "strict")
+ else:
+ hostname = "no-hostname"
+ ssl_match_hostname.match_hostname(crt, hostname)
+ except (ValueError, ssl_match_hostname.CertificateError) as e:
+ self.ssl_verification_error = dict(depth=0, errno="Invalid Hostname")
+ if verification_mode == SSL.VERIFY_PEER:
+ raise InvalidCertificateException("Presented certificate for {} is not valid: {}".format(sni, str(e)))
self.ssl_established = True
- self.cert = certutils.SSLCert(self.connection.get_peer_certificate())
self.rfile.set_descriptor(self.connection)
self.wfile.set_descriptor(self.connection)