diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index a7a719cff..c5306b4ab 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -52,6 +52,9 @@ class ProxyConfig: ssl_version_server=tcp.SSL_DEFAULT_METHOD, ssl_ports=TRANSPARENT_SSL_PORTS, spoofed_ssl_port=None, + ssl_verify_upstream_cert=False, + ssl_upstream_trusted_cadir=None, + ssl_upstream_trusted_ca=None ): self.host = host self.port = port @@ -100,6 +103,13 @@ class ProxyConfig: self.openssl_method_server = ssl_version_server else: self.openssl_method_server = tcp.SSL_VERSIONS[ssl_version_server] + + if ssl_verify_upstream_cert: + self.openssl_verification_mode_server = SSL.VERIFY_PEER + else: + self.openssl_verification_mode_server = SSL.VERIFY_NONE + self.openssl_trusted_cadir_server = ssl_upstream_trusted_cadir + self.openssl_trusted_ca_server = ssl_upstream_trusted_ca self.openssl_options_client = tcp.SSL_DEFAULT_OPTIONS self.openssl_options_server = tcp.SSL_DEFAULT_OPTIONS @@ -203,7 +213,10 @@ def process_proxy_options(parser, options): ssl_version_client=options.ssl_version_client, ssl_version_server=options.ssl_version_server, ssl_ports=ssl_ports, - spoofed_ssl_port=spoofed_ssl_port + spoofed_ssl_port=spoofed_ssl_port, + ssl_verify_upstream_cert=options.ssl_verify_upstream_cert, + ssl_upstream_trusted_cadir=options.ssl_upstream_trusted_cadir, + ssl_upstream_trusted_ca=options.ssl_upstream_trusted_ca ) @@ -242,6 +255,23 @@ def ssl_option_group(parser): action="store_true", dest="no_upstream_cert", help="Don't connect to upstream server to look up certificate details." ) + group.add_argument( + "--verify-upstream-cert", default=False, + action="store_true", dest="ssl_verify_upstream_cert", + help="Verify upstream server SSL/TLS certificates and fail if invalid " + "or not present." + ) + group.add_argument( + "--upstream-trusted-cadir", default=None, action="store", + dest="ssl_upstream_trusted_cadir", + help="Path to a directory of trusted CA certificates for upstream " + "server verification prepared using the c_rehash tool." + ) + group.add_argument( + "--upstream-trusted-ca", default=None, action="store", + dest="ssl_upstream_trusted_ca", + help="Path to a PEM formatted trusted CA certificate." + ) group.add_argument( "--ssl-port", action="append", diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index 051e84893..2711bd0e7 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -235,8 +235,18 @@ class ConnectionHandler: sni, 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, ) + ssl_cert_err = self.server_conn.ssl_verification_error + if ssl_cert_err is not None: + self.log( + "SSL verification failed for upstream server at depth %s with error: %s" % + (ssl_cert_err['depth'], ssl_cert_err['errno']), + "error") + self.log("Ignoring server verification error, continuing with connection", "error") except tcp.NetLibError as v: e = ProxyError(502, repr(v)) # Workaround for https://github.com/mitmproxy/mitmproxy/issues/427 @@ -246,6 +256,13 @@ class ConnectionHandler: if client and "handshake failure" in e.message: self.server_conn.may_require_sni = e else: + ssl_cert_err = self.server_conn.ssl_verification_error + if ssl_cert_err is not None: + self.log( + "SSL verification failed for upstream server at depth %s with error: %s" % + (ssl_cert_err['depth'], ssl_cert_err['errno']), + "error") + self.log("Aborting connection attempt", "error") raise e if client: if self.client_conn.ssl_established: diff --git a/test/data/trusted-cadir/8117bdb9.0 b/test/data/trusted-cadir/8117bdb9.0 new file mode 120000 index 000000000..91dcd7971 --- /dev/null +++ b/test/data/trusted-cadir/8117bdb9.0 @@ -0,0 +1 @@ +trusted-ca.pem \ No newline at end of file diff --git a/test/data/trusted-cadir/9d45e6a9.0 b/test/data/trusted-cadir/9d45e6a9.0 new file mode 120000 index 000000000..91dcd7971 --- /dev/null +++ b/test/data/trusted-cadir/9d45e6a9.0 @@ -0,0 +1 @@ +trusted-ca.pem \ No newline at end of file diff --git a/test/data/trusted-cadir/trusted-ca.pem b/test/data/trusted-cadir/trusted-ca.pem new file mode 100644 index 000000000..ae78b5465 --- /dev/null +++ b/test/data/trusted-cadir/trusted-ca.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICJzCCAZACCQCo1BdopddN/TANBgkqhkiG9w0BAQUFADBXMQswCQYDVQQGEwJB +VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRAwDgYDVQQDEwdUUlVTVEVEMCAXDTE1MDYxOTE4MDEzMVoYDzIx +MTUwNTI2MTgwMTMxWjBXMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0 +ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQDEwdU +UlVTVEVEMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC00Jf3KrBAmLQWl+Dz +8Qrig8ActB94kv0/Lu03P/2DwOR8kH2h3w4OC3b3CFKX31h7hm/H1PPHq7cIX6IR +fwrYCtBE77UbxklSlrwn06j6YSotz0/dwLEQEFDXWITJq7AyntaiafDHazbbXESN +m/+I/YEl2wKemEHE//qWbeM9kwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAF0NREP3 +X+fTebzJGttzrFkDhGVFKRNyLXblXRVanlGOYF+q8grgZY2ufC/55gqf+ub6FRT5 +gKPhL4V2rqL8UAvCE7jq8ujpVfTB8kRAKC675W2DBZk2EJX9mjlr89t7qXGsI5nF +onpfJ1UtiJshNoV7h/NFHeoag91kx628807n +-----END CERTIFICATE----- diff --git a/test/data/trusted-server.crt b/test/data/trusted-server.crt new file mode 100644 index 000000000..76f8559a3 --- /dev/null +++ b/test/data/trusted-server.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIC8jCCAlugAwIBAgICEAcwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQVUx +EzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMg +UHR5IEx0ZDEQMA4GA1UEAxMHVFJVU1RFRDAgFw0xNTA2MjAwMTE4MjdaGA8yMTE1 +MDUyNzAxMTgyN1owfjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx +ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UECxMLSU5U +RVJNIFVOSVQxITAfBgNVBAMTGE9SRyBXSVRIIElOVEVSTUVESUFURSBDQTCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtRPNKgh4WdYGmU2Ae6Tf2Mbd3oaRI/uY +Qm6aKeYk1i7g41C0vVowNcD/qdNpGUNnai/Kak9anHOYyppNo7zHgf3EO8zQ4NTQ +pkDKsdCqbUQcjGfhjWXKnOw+I5er4Rj+MwM1f5cbwb8bYHiSPmXaxzdL0/SNXGAA +ys/UswgwkU8CAwEAAaOBozCBoDAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBTPkPQW +DAPOIy8mipuEsZcP1694EDBxBgNVHSMEajBooVukWTBXMQswCQYDVQQGEwJBVTET +MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMRAwDgYDVQQDEwdUUlVTVEVEggkAqNQXaKXXTf0wDQYJKoZIhvcNAQEF +BQADgYEApaPbwonY8l+zSxlY2Fw4WNKfl5nwcTW4fuv/0tZLzvsS6P4hTXxbYJNa +k3hQ1qlrr8DiWJewF85hYvEI2F/7eqS5dhhPTEUFPpsjhbgiqnASvW+WKQIgoY2r +aHgOXi7RNFtTcCgk0UZISWOY7ORLy8Xu6vKrLRjDhyfIbGlqnAs= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC1E80qCHhZ1gaZTYB7pN/Yxt3ehpEj+5hCbpop5iTWLuDjULS9 +WjA1wP+p02kZQ2dqL8pqT1qcc5jKmk2jvMeB/cQ7zNDg1NCmQMqx0KptRByMZ+GN +Zcqc7D4jl6vhGP4zAzV/lxvBvxtgeJI+ZdrHN0vT9I1cYADKz9SzCDCRTwIDAQAB +AoGAfKHocKnrzEmXuSSy7meI+vfF9kfA1ndxUSg3S+dwK0uQ1mTSQhI1ZIo2bnlo +uU6/e0Lxm0KLJ2wZGjoifjSNTC8pcxIfAQY4kM9fqoUcXVSBVSS2kByTunhNSVZQ +yQyc+UTq9g1zBnJsZAltn7/PaihU4heWgP/++lposuShqmECQQDaG+7l0qul1xak +9kuZgc88BSTfn9iMK2zIQRcVKuidK4dT3QEp0wmWR5Ue8jq8lvTmVTGNGZbHcheh +KhoZfLgLAkEA1IjwAw/8z02yV3lbc2QUjIl9m9lvjHBoE2sGuSfq/cZskLKrGat+ +CVj3spqVAg22tpQwVBuHiipBziWVnEtiTQJAB9FKfchQSLBt6lm9mfHyKJeSm8VR +8Kw5yO+0URjpn4CI6DOasBIVXOKR8LsD6fCLNJpHHWSWZ+2p9SfaKaGzwwJBAM31 +Scld89qca4fzNZkT0goCrvOZeUy6HVE79Q72zPVSFSD/02kT1BaQ3bB5to5/5aD2 +6AKJjwZoPs7bgykrsD0CQBzU8U/8x2dNQnG0QeqaKQu5kKhZSZ9bsawvrCkxSl6b +WAjl/Jehi5bbQ07zQo3cge6qeR38FCWVCHQ/5wNbc54= +-----END RSA PRIVATE KEY----- diff --git a/test/data/untrusted-server.crt b/test/data/untrusted-server.crt new file mode 100644 index 000000000..62e586018 --- /dev/null +++ b/test/data/untrusted-server.crt @@ -0,0 +1,32 @@ +# untrusted-interm.crt, self-signed +-----BEGIN CERTIFICATE----- +MIICdTCCAd4CCQDRSKOnIMbTgDANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJB +VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRQwEgYDVQQLEwtJTlRFUk0gVU5JVDEhMB8GA1UEAxMYT1JHIFdJ +VEggSU5URVJNRURJQVRFIENBMCAXDTE1MDYyMDAxMzY0M1oYDzIxMTUwNTI3MDEz +NjQzWjB+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UE +ChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQLEwtJTlRFUk0gVU5J +VDEhMB8GA1UEAxMYT1JHIFdJVEggSU5URVJNRURJQVRFIENBMIGfMA0GCSqGSIb3 +DQEBAQUAA4GNADCBiQKBgQC1E80qCHhZ1gaZTYB7pN/Yxt3ehpEj+5hCbpop5iTW +LuDjULS9WjA1wP+p02kZQ2dqL8pqT1qcc5jKmk2jvMeB/cQ7zNDg1NCmQMqx0Kpt +RByMZ+GNZcqc7D4jl6vhGP4zAzV/lxvBvxtgeJI+ZdrHN0vT9I1cYADKz9SzCDCR +TwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAGbObAMEajCz4kj7OP2/DB5SRy2+H/G3 +8Qvc43xlMMNQyYxsDuLOFL0UMRzoKgntrrm2nni8jND+tuMt+hv3ZlBcJlYJ6ynR +sC1ITTC/1SwwwO0AFIyduUEIJYr/B3sgcVYPLcEfeDZgmEQc9Tnc01aEu3lx2+l9 +0JTSPL2L9LdA +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC1E80qCHhZ1gaZTYB7pN/Yxt3ehpEj+5hCbpop5iTWLuDjULS9 +WjA1wP+p02kZQ2dqL8pqT1qcc5jKmk2jvMeB/cQ7zNDg1NCmQMqx0KptRByMZ+GN +Zcqc7D4jl6vhGP4zAzV/lxvBvxtgeJI+ZdrHN0vT9I1cYADKz9SzCDCRTwIDAQAB +AoGAfKHocKnrzEmXuSSy7meI+vfF9kfA1ndxUSg3S+dwK0uQ1mTSQhI1ZIo2bnlo +uU6/e0Lxm0KLJ2wZGjoifjSNTC8pcxIfAQY4kM9fqoUcXVSBVSS2kByTunhNSVZQ +yQyc+UTq9g1zBnJsZAltn7/PaihU4heWgP/++lposuShqmECQQDaG+7l0qul1xak +9kuZgc88BSTfn9iMK2zIQRcVKuidK4dT3QEp0wmWR5Ue8jq8lvTmVTGNGZbHcheh +KhoZfLgLAkEA1IjwAw/8z02yV3lbc2QUjIl9m9lvjHBoE2sGuSfq/cZskLKrGat+ +CVj3spqVAg22tpQwVBuHiipBziWVnEtiTQJAB9FKfchQSLBt6lm9mfHyKJeSm8VR +8Kw5yO+0URjpn4CI6DOasBIVXOKR8LsD6fCLNJpHHWSWZ+2p9SfaKaGzwwJBAM31 +Scld89qca4fzNZkT0goCrvOZeUy6HVE79Q72zPVSFSD/02kT1BaQ3bB5to5/5aD2 +6AKJjwZoPs7bgykrsD0CQBzU8U/8x2dNQnG0QeqaKQu5kKhZSZ9bsawvrCkxSl6b +WAjl/Jehi5bbQ07zQo3cge6qeR38FCWVCHQ/5wNbc54= +-----END RSA PRIVATE KEY----- diff --git a/test/test_proxy.py b/test/test_proxy.py index d1e72f75c..77051eddf 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -9,6 +9,8 @@ from libpathod import test from netlib import http, tcp import mock +from OpenSSL import SSL + def test_proxy_error(): p = ProxyError(111, "msg") @@ -133,6 +135,20 @@ class TestProcessProxyOptions: "--singleuser", "test") + def test_verify_upstream_cert(self): + p = self.assert_noerr("--verify-upstream-cert") + assert p.openssl_verification_mode_server == SSL.VERIFY_PEER + + def test_upstream_trusted_cadir(self): + expected_dir = "/path/to/a/ca/dir" + p = self.assert_noerr("--upstream-trusted-cadir", expected_dir) + assert p.openssl_trusted_cadir_server == expected_dir + + def test_upstream_trusted_ca(self): + expected_file = "/path/to/a/cert/file" + p = self.assert_noerr("--upstream-trusted-ca", expected_file) + assert p.openssl_trusted_ca_server == expected_file + class TestProxyServer: # binding to 0.0.0.0:1 works without special permissions on Windows diff --git a/test/test_server.py b/test/test_server.py index 8cf4095b9..3726ec27a 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -9,6 +9,7 @@ import tutils import tservers from libmproxy.protocol import KILL, Error from libmproxy.protocol.http import CONTENT_MISSING +from OpenSSL import SSL """ Note that the choice of response code in these tests matters more than you @@ -348,6 +349,65 @@ class TestHTTPSCertfile(tservers.HTTPProxTest, CommonMixin): assert self.pathod("304") +class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxTest): + """ + Test upstream server certificate verification with a trusted server cert. + """ + ssl = True + ssloptions = pathod.SSLOptions( + cn = "trusted-cert", + certs = [ + ("trusted-cert", tutils.test_data.path("data/trusted-server.crt")) + ]) + + def test_verification_w_cadir(self): + self.config.openssl_verification_mode_server = SSL.VERIFY_PEER + self.config.openssl_trusted_cadir_server = tutils.test_data.path( + "data/trusted-cadir/") + + self.pathoc() + + def test_verification_w_pemfile(self): + self.config.openssl_verification_mode_server = SSL.VERIFY_PEER + self.config.openssl_trusted_ca_server = tutils.test_data.path( + "data/trusted-cadir/trusted-ca.pem") + + self.pathoc() + + +class TestHTTPSUpstreamServerVerificationWBadCert(tservers.HTTPProxTest): + """ + Test upstream server certificate verification with an untrusted server cert. + """ + ssl = True + ssloptions = pathod.SSLOptions( + cn = "untrusted-cert", + certs = [ + ("untrusted-cert", tutils.test_data.path("data/untrusted-server.crt")) + ]) + + def test_default_verification_w_bad_cert(self): + """Should use no verification.""" + self.config.openssl_trusted_ca_server = tutils.test_data.path( + "data/trusted-cadir/trusted-ca.pem") + + self.pathoc() + + def test_no_verification_w_bad_cert(self): + self.config.openssl_verification_mode_server = SSL.VERIFY_NONE + self.config.openssl_trusted_ca_server = tutils.test_data.path( + "data/trusted-cadir/trusted-ca.pem") + + self.pathoc() + + def test_verification_w_bad_cert(self): + self.config.openssl_verification_mode_server = SSL.VERIFY_PEER + self.config.openssl_trusted_ca_server = tutils.test_data.path( + "data/trusted-cadir/trusted-ca.pem") + + tutils.raises("SSL handshake error", self.pathoc) + + class TestHTTPSNoCommonName(tservers.HTTPProxTest): """ Test what happens if we get a cert without common name back.